Skip to content

Cloudflare Workers

autotel-cloudflare provides feature-targeted Cloudflare instrumentation and covers every binding (KV, R2, D1, Durable Objects, Workers AI, Vectorize, Hyperdrive, Service Bindings, Queues, Analytics Engine, Email) with automatic instrumentation. It also provides tail sampling and three API styles compatible with @microlabs/otel-cf-workers and workers-honeycomb-logger.

Built on autotel-edge. Total bundle ~45KB.

Terminal window
npm install autotel-cloudflare

Pair Cloudflare’s native OTel destinations with autotel’s sampling. Set head_sampling_rate = 1.0 so autotel handles tail sampling itself:

[observability.traces]
enabled = true
destinations = ["honeycomb"] # configure in CF dashboard
head_sampling_rate = 1.0

If you’d rather export OTLP yourself, skip the [observability] block and pass an exporter.url to wrapModule() (see Configuration).

import { wrapModule, trace } from 'autotel-cloudflare';
const processOrder = trace(async (orderId: string, kv: KVNamespace) => {
const order = await kv.get(orderId); // auto-instrumented
return order;
});
export default wrapModule(
{
service: { name: 'my-worker' },
instrumentBindings: true,
sampling: 'adaptive',
},
{
async fetch(req, env, ctx) {
return Response.json(await processOrder('123', env.ORDERS_KV));
},
},
);

Every call to processOrder produces a span; every env.ORDERS_KV.get(...) inside it produces a child span automatically.

Pick the style that matches the ecosystem you came from. All three accept the same config object.

Section titled “wrapModule(config, handler) — recommended”

Compatible with workers-honeycomb-logger. Config first, handler second:

import { wrapModule } from 'autotel-cloudflare';
export default wrapModule(
{ service: { name: 'my-worker' } },
{
async fetch(req, env, ctx) { return new Response('Hello'); },
async scheduled(event, env, ctx) { /* cron */ },
async queue(batch, env, ctx) { /* queue consumer */ },
async email(message, env, ctx) { /* email handler */ },
},
);

Compatible with @microlabs/otel-cf-workers. Handler first, config second:

import { instrument } from 'autotel-cloudflare';
export default instrument(
{ async fetch(req, env, ctx) { return new Response('Hello'); } },
{ service: { name: 'my-worker' } },
);

For inner business logic. Combine with wrapModule for full coverage:

import { trace, span } from 'autotel-cloudflare';
export const processPayment = trace((ctx) => async (amount: number) => {
ctx.setAttribute('amount', amount);
await span({ name: 'validate.card' }, () => validateCard());
await span({ name: 'charge.card' }, () => chargeCard(amount));
return { success: true };
});

Filter which routes are traced and map URL patterns to per-route service names. Useful for an edge gateway that forwards to multiple downstreams:

export default wrapModule(
{
service: { name: 'edge-gateway' },
handlers: {
fetch: {
include: ['/api/**'],
exclude: ['/api/internal/**', '/health'],
routes: {
'/api/auth/**': { service: 'auth-service' },
'/api/**': { service: 'api-service' },
},
postProcess: (span, { request, response }) => {
const url = new URL(request.url);
span.setAttribute('api.endpoint', url.pathname);
},
},
},
},
handler,
);

All bindings are auto-instrumented when instrumentBindings: true. Each binding operation produces a span named <Binding> <NAME>: <op> (e.g. KV ORDERS_KV: get).

BindingOperations
KVget, put, delete, list, getWithMetadata
R2head, get, put, delete, list, createMultipartUpload
D1prepare, batch, exec, dump
Durable Objectsfetch, alarm (use wrapDurableObject for the class itself)
Workflowsget, create, getInstance
Workers AIrun
Vectorizeinsert, query, getByIds, deleteByIds, upsert
HyperdriveAll queries
Service Bindingfetch
Queuesend, sendBatch
Analytics EnginewriteDataPoint
Emailsend, forward

Global instrumentations are enabled with instrumentation.instrumentGlobalFetch and instrumentation.instrumentGlobalCache.

Wrap the DO class itself so its fetch and alarm methods become root spans:

import { wrapDurableObject } from 'autotel-cloudflare';
class Counter implements DurableObject {
async fetch(request: Request) {
const count = (await this.state.storage.get('count')) ?? 0;
await this.state.storage.put('count', count + 1);
return new Response(String(count + 1));
}
async alarm() {
// span "Counter: alarm"
}
}
export const CounterDO = wrapDurableObject(
{ service: { name: 'counter-do' } },
Counter,
);

For the Agents SDK, set the agent’s observability field. createOtelObservability converts the full Agents SDK event stream into spans: RPC, chat recovery/transcript events, queue/schedule, workflow, email, lifecycle, and MCP operations.

import { Agent } from 'agents';
import { createOtelObservability } from 'autotel-cloudflare/agents';
class MyAgent extends Agent<Env> {
observability = createOtelObservability({
service: { name: 'my-agent' },
exporter: { url: env.OTLP_ENDPOINT },
});
}

createOtelObservabilityFromEnv() reads OTEL_* variables when you would rather not pass config inline.

For the Actors model, tracedHandler wraps the class, sets up telemetry, and opens a root span per request.

import { Actor } from '@cloudflare/actors';
import { tracedHandler } from 'autotel-cloudflare/actors';
class MyActor extends Actor<Env> {
protected onRequest(request: Request) {
return new Response('Hello');
}
}
export default tracedHandler(MyActor, (env) => ({
service: { name: 'my-actor-service' },
exporter: { url: env.OTLP_ENDPOINT },
}));

instrumentActorStorage, instrumentActorAlarms, and instrumentActorSockets trace storage reads/writes, alarms, and sockets when you want finer control.

wrapModule instruments every handler defined on the module. No extra setup needed:

export default wrapModule(
{ service: { name: 'consumer' } },
{
async queue(batch, env, ctx) {
// root span "queue.consumer"; each message processed inside
for (const msg of batch.messages) {
await processMessage(msg.body);
msg.ack();
}
},
async scheduled(event, env, ctx) {
// root span "scheduled.cron"
},
async email(message, env, ctx) {
// root span "email.received"
},
},
);

Tail sampling decides after the trace has been recorded, so you can keep 100% of errors and slow requests while sampling the rest at any rate.

import { SamplingPresets } from 'autotel-cloudflare/sampling';
wrapModule(
{
service: { name: 'my-worker' },
sampling: { tailSampler: SamplingPresets.production() },
// shorthand: sampling: 'adaptive'
},
handler,
);
PresetBehaviour
SamplingPresets.development()100% sampling
SamplingPresets.production()10% baseline + 100% errors + slow >1s
SamplingPresets.highTraffic()1% baseline + 100% errors + slow >1s
SamplingPresets.debugging()Errors only

Shorthand strings: 'adaptive' → production preset, 'error-only' → debugging preset.

import { createCustomTailSampler } from 'autotel-cloudflare/sampling';
import { SpanStatusCode } from '@opentelemetry/api';
const sampler = createCustomTailSampler((trace) => {
const span = trace.localRootSpan;
if (span.attributes['http.route']?.toString().startsWith('/api/')) return true;
if (span.status.code === SpanStatusCode.ERROR) return true;
const durationMs = (span.endTime[0] - span.startTime[0]) / 1_000_000;
if (durationMs > 1000) return true;
return Math.random() < 0.1;
});

createWorkersLogger() returns a request-scoped logger pre-populated with http.method, url.path, cf-ray, traceparent, and CF context:

import { createWorkersLogger, wrapModule } from 'autotel-cloudflare';
export default wrapModule(
{ service: { name: 'checkout-worker' } },
{
async fetch(request) {
const log = createWorkersLogger(request, {
headers: ['x-request-id'],
});
log.set({ checkout: { stage: 'validated' } });
log.info('checkout.started');
log.emitNow({ status: 200 });
return new Response('ok');
},
},
);

Outgoing env.MY_SERVICE.fetch(...) calls are auto-instrumented and propagate W3C trace headers automatically. No extra configuration required.

Emit product/business events through a request-scoped subscribers helper with active trace context attached:

import { wrapModule } from 'autotel-cloudflare';
import { getEdgeSubscribers } from 'autotel-cloudflare/events';
export default wrapModule(
{
service: { name: 'my-worker' },
subscribers: [
async (event) => {
await fetch('https://analytics.example.com/events', {
method: 'POST',
body: JSON.stringify(event),
});
},
],
},
{
async fetch(req, env, ctx) {
const subscribers = getEdgeSubscribers(ctx);
await subscribers.trackEvent('order.completed', {
orderId: 'abc',
amount: 99.99,
});
return new Response('OK');
},
},
);

Full surface, including dynamic config that closes over env:

export default wrapModule(
(env, trigger) => ({
service: {
name: env.SERVICE_NAME ?? 'my-worker',
version: '1.0.0',
namespace: env.ENVIRONMENT,
},
instrumentBindings: true,
instrumentation: {
instrumentGlobalFetch: true,
instrumentGlobalCache: true,
disabled: false,
},
sampling: {
tailSampler: env.ENVIRONMENT === 'production'
? SamplingPresets.production()
: SamplingPresets.development(),
},
exporter: {
url: env.OTEL_ENDPOINT,
headers: { 'x-api-key': env.API_KEY },
},
}),
handler,
);

autotel-cloudflare does not expose dedicated runtime testing helpers. Use your existing Worker test runner (Vitest/Miniflare/Wrangler) and assert on responses plus exporter output in integration tests.

ImportContents
autotel-cloudflareFunctional API, handler wrappers, bindings instrumentation
autotel-cloudflare/bindingsinstrumentKV, instrumentR2, etc.
autotel-cloudflare/handlersinstrumentDO, handler wrappers
autotel-cloudflare/loggercreateEdgeLogger, createWorkersLogger, runWithLogLevel
autotel-cloudflare/samplingSamplingPresets, createCustomTailSampler, etc.
autotel-cloudflare/eventsgetEdgeSubscribers, createEdgeSubscribers
autotel-cloudflare/agentscreateOtelObservability, createOtelObservabilityFromEnv for the Agents SDK
autotel-cloudflare/actorstracedHandler, instrumentActor, and storage/alarm/socket instrumenters
autotel-cloudflare/testingPlaceholder entrypoint (currently no runtime helpers)

autotel-cloudflare now uses shared route-matching utilities from autotel-edge, so include/exclude glob behavior is consistent across edge integrations.

  • cloudflare-example — KV, R2, D1, AI, Queues, Durable Objects, scheduled, email, custom sampling.