Email SDK
PluginsBuilt-in

Observability plugin

Emit redacted sent/retry/error events to your logger, metrics, and tracer — counts and tag names, never recipients or bodies.

observabilityPlugin turns the send pipeline into structured events you can wire to any logger, metrics client, or tracer. Events are redacted by default: they carry counts, flags, and tag names — never recipient addresses, bodies, attachment content, or metadata values — so they are safe to ship to a log aggregator as-is.

lib/email.ts
import { createEmailClient } from "@opencoredev/email-sdk";
import { observabilityPlugin } from "@opencoredev/email-sdk/plugins/observability";
import { resend } from "@opencoredev/email-sdk/resend";

export const email = createEmailClient({
  adapters: [resend({ apiKey: process.env.RESEND_API_KEY! })],
  plugins: [
    observabilityPlugin({
      log(event) {
        // any structured logger works the same way (pino, winston, console)
        logger.info({ ...event }, event.type);
      },
      metric(event) {
        metrics.increment(`email.${event.type}`, { provider: event.provider });
      },
    }),
  ],
});

Options

Prop

Type

log, metric, and trace are not filtered by event type — all three receive every event (dispatched via Promise.allSettled). Route inside each callback if you only want errors in one sink. Callback failures are swallowed so a broken logger never masks a provider failure.

Events

EventFires whenExtra fields
email.sentA provider returns a normalized response.responseId, messageId
email.retryThe SDK schedules another attempt on the same adapter.nextAttempt, delayMs, error
email.errorAn adapter route fails after exhausting its retries.error

Every event also carries provider, attempt, send-scope metadata, and the redacted message:

type RedactedEmailMessage = {
  subject: string;
  toCount: number;
  ccCount: number;
  bccCount: number;
  hasHtml: boolean;
  hasText: boolean;
  attachmentCount: number;
  tagNames: string[];
  metadataKeys: string[];
};

Tag names and metadata keys are included; their values are not.

Custom redaction

Pass redactMessage when your team needs a different summary — for example, a template name from metadata:

observabilityPlugin({
  redactMessage(message) {
    return {
      subject: message.subject,
      toCount: Array.isArray(message.to) ? message.to.length : 1,
      template: message.metadata?.template ?? "unknown",
      hasAttachments: Boolean(message.attachments?.length),
    };
  },
  log: (event) => logger.info({ ...event }, event.type),
});

You own redaction once you override it

A custom redactMessage replaces the default entirely. Whatever it returns goes to every sink — keep recipient lists, bodies, and secrets out unless your logging pipeline is built to protect them.

Next

On this page