diff --git a/.changeset/thirty-ties-float.md b/.changeset/thirty-ties-float.md new file mode 100644 index 0000000..83e0aaa --- /dev/null +++ b/.changeset/thirty-ties-float.md @@ -0,0 +1,15 @@ +--- +'@microlabs/otel-cf-workers': minor +--- + +add support for `email` handlers + +Example usage: + +```ts +export default { + async email(message, env, ctx) { + // this is running in a trace! + }, +}; +``` diff --git a/README.md b/README.md index d50c706..5c09b73 100644 --- a/README.md +++ b/README.md @@ -266,7 +266,7 @@ One of the advantages of using Open Telemetry is that it makes it easier to do d Triggers: -- [ ] Email (`handler.email`) +- [x] Email (`handler.email`) - [x] HTTP (`handler.fetch`) - [x] Queue (`handler.queue`) - [x] Cron (`handler.scheduled`) diff --git a/package.json b/package.json index 8b8dc07..b01557d 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "scripts": { "clean": "rimraf ./dist versions.json", "format": "prettier --ignore-unknown --write .", - "build:src": "tsup src/index.ts --format cjs,esm --dts --clean --sourcemap", + "build:src": "tsup", "build:versions": "pnpm version --json > versions.json", "build": "run-s -l build:versions build:src", "cs-version": "changeset version", diff --git a/src/instrumentation/email.ts b/src/instrumentation/email.ts new file mode 100644 index 0000000..646d838 --- /dev/null +++ b/src/instrumentation/email.ts @@ -0,0 +1,76 @@ +import { setConfig, type Initialiser } from '../config' +import { wrap } from '../wrap' +import { exportSpans, proxyExecutionContext } from './common' +import { context as api_context, Exception, SpanKind, type SpanOptions, trace } from '@opentelemetry/api' +import { instrumentEnv } from './env' +import { versionAttributes } from './version' +import { + ATTR_FAAS_TRIGGER, + ATTR_MESSAGING_DESTINATION_NAME, + ATTR_RPC_MESSAGE_ID, +} from '@opentelemetry/semantic-conventions/incubating' + +type EmailHandler = EmailExportedHandler +export type EmailHandlerArgs = Parameters + +export function createEmailHandler(emailFn: EmailHandler, initialiser: Initialiser): EmailHandler { + const emailHandler: ProxyHandler = { + async apply(target, _thisArg, argArray: Parameters): Promise { + const [message, orig_env, orig_ctx] = argArray + const config = initialiser(orig_env as Record, message) + const env = instrumentEnv(orig_env as Record) + const { ctx, tracker } = proxyExecutionContext(orig_ctx) + const context = setConfig(config) + + try { + const args: EmailHandlerArgs = [message, env, ctx] + return await api_context.with(context, executeEmailHandler, undefined, target, args) + } catch (error) { + throw error + } finally { + orig_ctx.waitUntil(exportSpans(tracker)) + } + }, + } + return wrap(emailFn, emailHandler) +} + +/** + * Converts the message headers into a record ready to be injected + * as OpenTelemetry attributes + * + * @example + * ```ts + * const headers = new Headers({ "Subject": "Hello!", From: "hello@example.com" }) + * headerAttributes({ headers }) + * // => {"email.header.Subject": "Hello!", "email.header.From": "hello@example.com"} + * ``` + */ +function headerAttributes(message: { headers: Headers }): Record { + return Object.fromEntries([...message.headers].map(([key, value]) => [`email.header.${key}`, value] as const)) +} + +async function executeEmailHandler(emailFn: EmailHandler, [message, env, ctx]: EmailHandlerArgs): Promise { + const tracer = trace.getTracer('emailHandler') + const options = { + attributes: { + [ATTR_FAAS_TRIGGER]: 'other', + [ATTR_RPC_MESSAGE_ID]: message.headers.get('Message-Id') ?? undefined, + [ATTR_MESSAGING_DESTINATION_NAME]: message.to, + }, + kind: SpanKind.CONSUMER, + } satisfies SpanOptions + Object.assign(options.attributes!, headerAttributes(message), versionAttributes(env)) + const promise = tracer.startActiveSpan(`emailHandler ${message.to}`, options, async (span) => { + try { + const result = await emailFn(message, env, ctx) + span.end() + return result + } catch (error) { + span.recordException(error as Exception) + span.end() + throw error + } + }) + return promise +} diff --git a/src/sdk.ts b/src/sdk.ts index dd28c89..a015c63 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -12,10 +12,12 @@ import { DOClass, instrumentDOClass } from './instrumentation/do.js' import { createScheduledHandler } from './instrumentation/scheduled.js' //@ts-ignore import * as versions from '../versions.json' +import { createEmailHandler } from './instrumentation/email.js' type FetchHandler = ExportedHandlerFetchHandler type ScheduledHandler = ExportedHandlerScheduledHandler type QueueHandler = ExportedHandlerQueueHandler +type EmailHandler = EmailExportedHandler export type ResolveConfigFn = (env: Env, trigger: Trigger) => TraceConfig export type ConfigurationOption = TraceConfig | ResolveConfigFn @@ -106,6 +108,12 @@ export function instrument( const queuer = unwrap(handler.queue) as QueueHandler handler.queue = createQueueHandler(queuer, initialiser) } + + if (handler.email) { + const emailer = unwrap(handler.email) as EmailHandler + handler.email = createEmailHandler(emailer, initialiser) + } + return handler } diff --git a/src/types.ts b/src/types.ts index 035881b..ebd61e3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -73,4 +73,10 @@ export interface DOConstructorTrigger { name?: string } -export type Trigger = Request | MessageBatch | ScheduledController | DOConstructorTrigger | 'do-alarm' +export type Trigger = + | Request + | MessageBatch + | ScheduledController + | DOConstructorTrigger + | 'do-alarm' + | ForwardableEmailMessage diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..b7ee126 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + clean: true, + sourcemap: true, +})