Skip to content

Commit

Permalink
feat: add support for email triggers
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Schniz committed Sep 30, 2024
1 parent 5419e56 commit c74d731
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 2 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
76 changes: 76 additions & 0 deletions src/instrumentation/email.ts
Original file line number Diff line number Diff line change
@@ -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<EmailHandler>

export function createEmailHandler(emailFn: EmailHandler, initialiser: Initialiser): EmailHandler {
const emailHandler: ProxyHandler<EmailHandler> = {
async apply(target, _thisArg, argArray: Parameters<EmailHandler>): Promise<void> {
const [message, orig_env, orig_ctx] = argArray
const config = initialiser(orig_env as Record<string, unknown>, message)
const env = instrumentEnv(orig_env as Record<string, unknown>)
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<string, unknown> {
return Object.fromEntries([...message.headers].map(([key, value]) => [`email.header.${key}`, value] as const))
}

async function executeEmailHandler(emailFn: EmailHandler, [message, env, ctx]: EmailHandlerArgs): Promise<void> {
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
}
13 changes: 13 additions & 0 deletions src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown, unknown>
type ScheduledHandler = ExportedHandlerScheduledHandler<unknown>
type QueueHandler = ExportedHandlerQueueHandler
type EmailHandler = EmailExportedHandler

export type ResolveConfigFn<Env = any> = (env: Env, trigger: Trigger) => TraceConfig
export type ConfigurationOption = TraceConfig | ResolveConfigFn
Expand All @@ -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',
Expand Down Expand Up @@ -106,6 +113,12 @@ export function instrument<E, Q, C>(
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
}

Expand Down
8 changes: 7 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export default defineConfig({
dts: true,
clean: true,
sourcemap: true,
external: ['cloudflare:email'],
})

0 comments on commit c74d731

Please sign in to comment.