UNPKG

autotel-cloudflare

Version:

The #1 OpenTelemetry package for Cloudflare Workers - complete bindings coverage, native CF OTel integration, advanced sampling

382 lines (379 loc) 12 kB
import { _ as trapArgs, f as member, r as asFunction } from "./values-BC6rdpGG.js"; import { o as wrap, t as toException } from "./exception-D3xOCdBW.js"; import { t as workerTracer } from "./tracer-z8zwRBVS.js"; import { createInitialiser, setConfig } from "autotel-edge"; import { SpanKind, SpanStatusCode, context, propagation } from "@opentelemetry/api"; //#region src/handlers/durable-objects.ts /** * Durable Objects instrumentation for Cloudflare Workers * * Note: This file uses Cloudflare Workers types (DurableObjectId, DurableObjectState, etc.) * which are globally available via @cloudflare/workers-types when listed in tsconfig.json. * These types are devDependencies only - they're not runtime dependencies. * At runtime, Cloudflare Workers runtime provides the actual implementations. */ /** * Track cold starts per DO class */ const coldStarts$1 = /* @__PURE__ */ new WeakMap(); function isColdStart$1(doClass) { if (!coldStarts$1.has(doClass)) { coldStarts$1.set(doClass, true); return true; } return false; } /** * Instrument a Durable Object fetch method */ function instrumentDOFetch(fetchFn, id, doClass) { return async function instrumentedFetch(request) { const tracer = workerTracer("autotel-edge"); const parentContext = propagation.extract(context.active(), request.headers); const url = new URL(request.url); const spanName = `DO ${id.name || id.toString()}: ${request.method} ${url.pathname}`; return tracer.startActiveSpan(spanName, { kind: SpanKind.SERVER, attributes: { "http.request.method": request.method, "url.full": request.url, "do.id": id.toString(), "do.id.name": id.name || "", "faas.trigger": "http", "faas.coldstart": isColdStart$1(doClass) } }, parentContext, async (span) => { try { const response = await fetchFn.call(this, request); span.setAttributes({ "http.response.status_code": response.status }); if (response.ok) span.setStatus({ code: SpanStatusCode.OK }); else span.setStatus({ code: SpanStatusCode.ERROR }); return response; } catch (error) { span.recordException(toException(error)); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); throw error; } finally { span.end(); } }); }; } /** * Instrument a Durable Object alarm method */ function instrumentDOAlarm(alarmFn, id, doClass) { return async function instrumentedAlarm() { const tracer = workerTracer("autotel-edge"); const spanName = `DO ${id.name || id.toString()}: alarm`; return tracer.startActiveSpan(spanName, { kind: SpanKind.INTERNAL, attributes: { "do.id": id.toString(), "do.id.name": id.name || "", "faas.trigger": "timer", "faas.coldstart": isColdStart$1(doClass) } }, async (span) => { try { await alarmFn.call(this); span.setStatus({ code: SpanStatusCode.OK }); } catch (error) { span.recordException(toException(error)); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); throw error; } finally { span.end(); } }); }; } /** * Instrument a Durable Object instance */ function instrumentDOInstance(doInstance, state, _env, doClass) { return wrap(doInstance, { get(target, prop) { const value = member(target, prop); const method = asFunction(value); if (prop === "fetch" && method) return instrumentDOFetch(method.bind(target), state.id, doClass); if (prop === "alarm" && method) return instrumentDOAlarm(method.bind(target), state.id, doClass); return method ? method.bind(target) : value; } }); } /** * Instrument a Durable Object class * * This wraps the DO class to automatically trace all fetch and alarm calls, * as well as initialize the telemetry configuration. * * **Usage:** * ```typescript * import { DurableObject } from 'cloudflare:workers' * import { instrumentDO } from 'autotel-edge' * * export class Counter extends DurableObject<Env> { * async fetch(request: Request) { * // Your DO logic here * return new Response('OK') * } * } * * // Wrap the class before exporting * export const CounterDO = instrumentDO(Counter, (env: Env) => ({ * exporter: { * url: env.OTLP_ENDPOINT, * headers: { 'x-api-key': env.API_KEY } * }, * service: { * name: 'my-durable-object', * version: '1.0.0' * } * })) * ``` * * **What you get:** * - 🎯 Automatic spans for fetch() calls with HTTP attributes * - ⏰ Automatic spans for alarm() calls * - 🥶 Cold start tracking * - 🔗 Context propagation from incoming requests * - ⚡ Automatic span lifecycle management * * @param doClass - The Durable Object class to instrument * @param config - Configuration or configuration function * @returns Instrumented Durable Object class */ function instrumentDO(doClass, config) { const initialiser = createInitialiser(config); return wrap(doClass, { construct(target, [state, env]) { const trigger = { id: state.id.toString(), name: state.id.name }; const context$2 = setConfig(initialiser(env, trigger)); return instrumentDOInstance(context.with(context$2, () => { return new target(state, env); }), state, env, doClass); } }); } //#endregion //#region src/handlers/workflows.ts /** * Cloudflare Workflows instrumentation for autotel-edge * * Instruments WorkflowEntrypoint classes to automatically trace workflow execution, * step operations, retries, and sleeps. * * Based on Cloudflare Workflows API: * https://developers.cloudflare.com/workflows/ */ /** * Track cold starts per Workflow class */ const coldStarts = /* @__PURE__ */ new WeakMap(); function isColdStart(workflowClass) { if (!coldStarts.has(workflowClass)) { coldStarts.set(workflowClass, true); return true; } return false; } /** * Proxy the step object to instrument step.do(), step.sleep(), and step.sleepUntil() calls */ function instrumentWorkflowStep(step, workflowName) { return wrap(step, { get(target, prop) { const value = member(target, prop); const method = asFunction(value); if (prop === "do" && method) return new Proxy(method, { apply: (fnTarget, thisArg, args) => { const [stepName] = trapArgs(args); return workerTracer("autotel-edge").startActiveSpan(`Workflow ${workflowName}: ${stepName}`, { kind: SpanKind.INTERNAL, attributes: { "workflow.step.name": stepName, "workflow.name": workflowName } }, async (span) => { try { const result = await fnTarget.apply(thisArg, args); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.recordException(toException(error)); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); throw error; } finally { span.end(); } }); } }); if (prop === "sleep" && method) return new Proxy(method, { apply: (fnTarget, thisArg, args) => { const [sleepName, duration] = trapArgs(args); return workerTracer("autotel-edge").startActiveSpan(`Workflow ${workflowName}: sleep ${sleepName}`, { kind: SpanKind.INTERNAL, attributes: { "workflow.sleep.name": sleepName, "workflow.sleep.duration": String(duration), "workflow.name": workflowName } }, async (span) => { try { const result = await fnTarget.apply(thisArg, args); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.recordException(toException(error)); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); throw error; } finally { span.end(); } }); } }); if (prop === "sleepUntil" && method) return new Proxy(method, { apply: (fnTarget, thisArg, args) => { const [sleepName, timestamp] = trapArgs(args); const tracer = workerTracer("autotel-edge"); const wakeAt = timestamp instanceof Date ? timestamp.toISOString() : new Date(timestamp).toISOString(); return tracer.startActiveSpan(`Workflow ${workflowName}: sleepUntil ${sleepName}`, { kind: SpanKind.INTERNAL, attributes: { "workflow.sleep.name": sleepName, "workflow.sleep.until": wakeAt, "workflow.name": workflowName } }, async (span) => { try { const result = await fnTarget.apply(thisArg, args); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.recordException(toException(error)); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); throw error; } finally { span.end(); } }); } }); if (method !== void 0) return method.bind(target); return value; } }); } /** * Instrument a Workflow run method */ function instrumentWorkflowRun(runFn, workflowName, workflowClass) { return async function instrumentedRun(event, step) { const tracer = workerTracer("autotel-edge"); const instrumentedStep = instrumentWorkflowStep(step, workflowName); const spanName = `Workflow ${workflowName}: run`; return tracer.startActiveSpan(spanName, { kind: SpanKind.INTERNAL, attributes: { "workflow.name": workflowName, "workflow.instance_id": event.instanceId, "faas.trigger": "workflow", "faas.coldstart": isColdStart(workflowClass) } }, async (span) => { try { const result = await runFn.call(this, event, instrumentedStep); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.recordException(toException(error)); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); throw error; } finally { span.end(); } }); }; } /** * Instrument a Workflow instance */ function instrumentWorkflowInstance(workflowInstance, workflowName, workflowClass) { return wrap(workflowInstance, { get(target, prop) { const value = member(target, prop); const method = asFunction(value); if (prop === "run" && method) return instrumentWorkflowRun(method.bind(target), workflowName, workflowClass); return method ? method.bind(target) : value; } }); } /** * Instrument a Cloudflare Workflow class * * This wraps the WorkflowEntrypoint class to automatically trace workflow execution, * step operations, retries, and sleeps. * * **Usage:** * ```typescript * import { WorkflowEntrypoint } from 'cloudflare:workers' * import { instrumentWorkflow } from 'autotel-cloudflare/handlers' * * class MyWorkflow extends WorkflowEntrypoint { * async run(event, step) { * await step.do('submit payment', async () => { * return await submitToPaymentProcessor(event.payload.payment) * }) * * await step.sleep('wait for feedback', '2 days') * * await step.do('send feedback email', sendFeedbackEmail) * } * } * * export const CheckoutWorkflow = instrumentWorkflow( * MyWorkflow, * 'checkout-workflow', * (env: Env) => ({ * exporter: { * url: env.OTLP_ENDPOINT, * headers: { 'x-api-key': env.API_KEY } * }, * service: { * name: 'checkout-workflow', * version: '1.0.0' * } * }) * ) * ``` * * @param workflowClass - The WorkflowEntrypoint class to instrument * @param workflowName - The name of the workflow (used in span names) * @param config - Configuration or configuration function * @returns Instrumented Workflow class */ function instrumentWorkflow(workflowClass, workflowName, config) { const initialiser = createInitialiser(config); return wrap(workflowClass, { construct(target, args) { const env = args[args.length - 1] || {}; const context$1 = setConfig(initialiser(env, { type: "workflow", name: workflowName })); return instrumentWorkflowInstance(context.with(context$1, () => { return new target(...args); }), workflowName, workflowClass); } }); } //#endregion export { instrumentDO as n, instrumentWorkflow as t };