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
JavaScript
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 };