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).

| 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,
);

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,
);

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/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.