autotel-cloudflare
Version:
The #1 OpenTelemetry package for Cloudflare Workers - complete bindings coverage, native CF OTel integration, advanced sampling
251 lines (225 loc) • 6.8 kB
text/typescript
/**
* 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.
*/
import {
trace,
context as api_context,
propagation,
SpanStatusCode,
SpanKind,
} from '@opentelemetry/api';
import type { ConfigurationOption } from 'autotel-edge';
import { createInitialiser, setConfig, WorkerTracer } from 'autotel-edge';
import { wrap } from '../bindings/common';
// Durable Object types
type DOFetchFn = (request: Request) => Response | Promise<Response>;
type DOAlarmFn = () => void | Promise<void>;
/**
* Track cold starts per DO class
*/
const coldStarts = new WeakMap<any, boolean>();
function isColdStart(doClass: any): boolean {
if (!coldStarts.has(doClass)) {
coldStarts.set(doClass, true);
return true;
}
return false;
}
/**
* Instrument a Durable Object fetch method
*/
function instrumentDOFetch(
fetchFn: DOFetchFn,
id: DurableObjectId,
doClass: any,
): DOFetchFn {
return async function instrumentedFetch(
this: any,
request: Request,
): Promise<Response> {
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
// Extract parent context from request headers
const parentContext = propagation.extract(
api_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(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(error as 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: DOAlarmFn,
id: DurableObjectId,
doClass: any,
): DOAlarmFn {
return async function instrumentedAlarm(this: any): Promise<void> {
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
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(doClass),
},
},
async (span) => {
try {
await alarmFn.call(this);
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.recordException(error as 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: any,
state: DurableObjectState,
_env: any,
doClass: any,
): any {
const instanceHandler: ProxyHandler<any> = {
get(target, prop) {
const value = Reflect.get(target, prop);
if (prop === 'fetch' && typeof value === 'function') {
return instrumentDOFetch(value.bind(target), state.id, doClass);
}
if (prop === 'alarm' && typeof value === 'function') {
return instrumentDOAlarm(value.bind(target), state.id, doClass);
}
// Bind other methods to the target
if (typeof value === 'function') {
return value.bind(target);
}
return value;
},
};
return wrap(doInstance, instanceHandler);
}
/**
* 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
*/
export function instrumentDO<C extends new (state: DurableObjectState, env: any) => any>(
doClass: C,
config: ConfigurationOption,
): C {
const initialiser = createInitialiser(config);
const classHandler: ProxyHandler<C> = {
construct(target, [state, env]: [DurableObjectState, any]) {
// Initialize config for this DO instance
const trigger = {
id: state.id.toString(),
name: state.id.name,
};
const doConfig = initialiser(env, trigger);
const context = setConfig(doConfig);
// Create the DO instance within the config context
const doInstance = api_context.with(context, () => {
return new target(state, env);
});
// Instrument the instance
return instrumentDOInstance(doInstance, state, env, doClass);
},
};
return wrap(doClass, classHandler);
}