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, span, withTracing } 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.

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, withTracing, span } from 'autotel-cloudflare';
export const processPayment = withTracing({})(
(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).

Binding Operations
KV get, put, delete, list, getWithMetadata
R2 head, get, put, delete, list, createMultipartUpload
D1 prepare, batch, exec, dump
Durable Objects fetch, alarm (use wrapDurableObject for the class itself)
Workflows get, create, getInstance
Workers AI run
Vectorize insert, query, getByIds, deleteByIds, upsert
Hyperdrive All queries
Service Binding fetch
Queue send, sendBatch
Analytics Engine writeDataPoint
Email send, 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,
);
Preset Behaviour
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,
);

Cloudflare Workers ship native tracing in beta. Turn it on and the platform instruments fetch, KV, R2, D1, and Durable Objects for you, then exports OTLP to whichever destination you configure in the dashboard. No exporter code in the Worker.

Autotel detects it. The same trace(), span(), and enterSpan() calls you already wrote nest inside Cloudflare’s native waterfall when native tracing is on, and fall back to autotel’s own OTLP pipeline everywhere else: other edge runtimes, native off, local wrangler dev.

[observability.traces]
enabled = true
# head_sampling_rate = 0.1
# destinations = ["honeycomb-traces"]

Keep your handler wrapper as it is. On each request the wrapper spots ctx.tracing, wraps it as a tracer, and installs it into the active context. Your trace() calls route to Cloudflare’s tracer, including calls buried inside utility functions and libraries.

When native tracing is active, autotel steps aside. It does not proxy-instrument bindings, because Cloudflare already traces them, and it registers no second provider or exporter, because Cloudflare exports everything. You get one set of spans.

Control it with nativeTracing, which defaults to 'auto':

Value Behaviour
'auto' Native when ctx.tracing is present, otherwise OTLP
'on' Prefer native; warn once and fall back to OTLP when it is absent
'off' Always use autotel’s OTLP exporter, even on Workers
export default wrapModule(
{ service: { name: 'my-worker' }, nativeTracing: 'auto' },
handler,
);

wrangler dev does not export native traces to a local receiver, so local development uses the OTLP fallback. Start devtools and point exporter.url at it:

Terminal window
npx autotel-devtools

The runtime-agnostic seam lives in autotel-edge; this package supplies the Cloudflare adapter.

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.

Import Contents
autotel-cloudflare Functional API, handler wrappers, bindings instrumentation
autotel-cloudflare/bindings instrumentKV, instrumentR2, etc.
autotel-cloudflare/handlers instrumentDO, handler wrappers
autotel-cloudflare/logger createEdgeLogger, createWorkersLogger, runWithLogLevel
autotel-cloudflare/sampling SamplingPresets, createCustomTailSampler, etc.
autotel-cloudflare/events getEdgeSubscribers, createEdgeSubscribers
autotel-cloudflare/agents createOtelObservability, createOtelObservabilityFromEnv for the Agents SDK
autotel-cloudflare/actors tracedHandler, instrumentActor, and storage/alarm/socket instrumenters
autotel-cloudflare/testing Placeholder 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.