# Observability plugin (/docs/v/0.6.1/plugins/built-in/observability)



`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.

```ts title="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 [#options]

<TypeTable
  type="{
  id: {
    description: &#x22;Plugin id. Override when mounting multiple instances.&#x22;,
    type: &#x22;string&#x22;,
    default: '&#x22;observability&#x22;',
  },
  log: {
    description: &#x22;Logging callback. Receives every event.&#x22;,
    type: &#x22;(event: EmailObservabilityEvent) => MaybePromise<void>&#x22;,
  },
  metric: {
    description: &#x22;Metrics callback. Receives every event.&#x22;,
    type: &#x22;(event: EmailObservabilityEvent) => MaybePromise<void>&#x22;,
  },
  trace: {
    description: &#x22;Tracing callback. Receives every event.&#x22;,
    type: &#x22;(event: EmailObservabilityEvent) => MaybePromise<void>&#x22;,
  },
  redactMessage: {
    description: &#x22;Replace the default redaction with your own summary shape.&#x22;,
    type: &#x22;(message: EmailMessage) => RedactedEmailMessage&#x22;,
  },
}"
/>

`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 [#events]

| Event         | Fires when                                             | Extra fields                      |
| ------------- | ------------------------------------------------------ | --------------------------------- |
| `email.sent`  | A provider returns a normalized response.              | `responseId`, `messageId`         |
| `email.retry` | The SDK schedules another attempt on the same adapter. | `nextAttempt`, `delayMs`, `error` |
| `email.error` | An adapter route fails after exhausting its retries.   | `error`                           |

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

```ts
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 [#custom-redaction]

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

```ts
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),
});
```

<Callout type="warn" title="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.
</Callout>

## Next [#next]

<Cards>
  <Card title="Hooks" href="/docs/v/0.6.1/concepts/hooks" description="The raw per-attempt callbacks this plugin is built on." />

  <Card title="Production send pipeline" href="/docs/v/0.6.1/guides/production-send-pipeline" description="Wire observability into a full retry/fallback setup." />
</Cards>
