From 5419e56db3b2265018a9835c3be9f60e6a58d6b4 Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Mon, 30 Sep 2024 10:09:50 +0300 Subject: [PATCH 1/4] Convert tsup to have a configuration file instead of inline arguments --- package.json | 2 +- tsup.config.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 tsup.config.ts 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/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, +}) From c74d73131f0208e603d3ba6d72495a2a92851994 Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Mon, 30 Sep 2024 10:09:13 +0300 Subject: [PATCH 2/4] feat: add support for email triggers Added support for email triggers. The `createEmailHandler` function is used to create an email handler that can be used to instrument email triggers. The `isEmailMessage` function is used to check if a trigger is an email message. The `instrument` function has been updated to support email handlers. Marked `cloudflare:email` as external --- README.md | 2 +- src/instrumentation/email.ts | 76 ++++++++++++++++++++++++++++++++++++ src/sdk.ts | 13 ++++++ src/types.ts | 8 +++- tsup.config.ts | 1 + 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/instrumentation/email.ts 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/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..64ac365 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -12,10 +12,13 @@ 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' +import { EmailMessage } from 'cloudflare:email' 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 @@ -32,6 +35,10 @@ export function isAlarm(trigger: Trigger): trigger is 'do-alarm' { return trigger === 'do-alarm' } +export function isEmailMessage(trigger: Trigger): trigger is ForwardableEmailMessage { + return !!(trigger instanceof EmailMessage && trigger.headers && trigger.forward) +} + const createResource = (config: ResolvedTraceConfig): Resource => { const workerResourceAttrs = { 'cloud.provider': 'cloudflare', @@ -106,6 +113,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 index b7ee126..fc5f993 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -6,4 +6,5 @@ export default defineConfig({ dts: true, clean: true, sourcemap: true, + external: ['cloudflare:email'], }) From 82a2ff8f7eb3531a7a3fd0affbf067041adadedc Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Mon, 30 Sep 2024 10:40:16 +0300 Subject: [PATCH 3/4] add changeset --- .changeset/thirty-ties-float.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .changeset/thirty-ties-float.md 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! + }, +}; +``` From 4ac6f73887ee814b54d74b397ab34ed35f1b4c2f Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Mon, 30 Sep 2024 19:33:40 +0300 Subject: [PATCH 4/4] remove isEmailAddress because it breaks tests (and probably irrelevant?) --- src/sdk.ts | 5 ----- tsup.config.ts | 1 - 2 files changed, 6 deletions(-) diff --git a/src/sdk.ts b/src/sdk.ts index 64ac365..a015c63 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -13,7 +13,6 @@ import { createScheduledHandler } from './instrumentation/scheduled.js' //@ts-ignore import * as versions from '../versions.json' import { createEmailHandler } from './instrumentation/email.js' -import { EmailMessage } from 'cloudflare:email' type FetchHandler = ExportedHandlerFetchHandler type ScheduledHandler = ExportedHandlerScheduledHandler @@ -35,10 +34,6 @@ export function isAlarm(trigger: Trigger): trigger is 'do-alarm' { return trigger === 'do-alarm' } -export function isEmailMessage(trigger: Trigger): trigger is ForwardableEmailMessage { - return !!(trigger instanceof EmailMessage && trigger.headers && trigger.forward) -} - const createResource = (config: ResolvedTraceConfig): Resource => { const workerResourceAttrs = { 'cloud.provider': 'cloudflare', diff --git a/tsup.config.ts b/tsup.config.ts index fc5f993..b7ee126 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -6,5 +6,4 @@ export default defineConfig({ dts: true, clean: true, sourcemap: true, - external: ['cloudflare:email'], })