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