OpenTelemetry instrumentation for Cloudflare Workers with automatic tracing and logging for handlers, bindings, and distributed traces.
yarn add @inference-net/otel-cf-workers @opentelemetry/apiAdd the nodejs_compat compatibility flag to your wrangler.toml:
compatibility_flags = ["nodejs_compat"]import { trace } from '@opentelemetry/api' import { instrument, ResolveConfigFn } from '@inference-net/otel-cf-workers' export interface Env { SIGNOZ_ENDPOINT: string SIGNOZ_ACCESS_TOKEN: string MY_KV: KVNamespace MY_D1: D1Database } const handler = { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { // Auto-instrumented: HTTP handler await fetch('https://api.example.com') // Auto-instrumented: outbound fetch await env.MY_KV.get('key') // Auto-instrumented: KV operations await env.MY_D1.prepare('SELECT * FROM users').all() // Auto-instrumented: D1 queries // Manual instrumentation: add custom attributes trace.getActiveSpan()?.setAttribute('user.id', '123') return new Response('Hello World!') }, } const config: ResolveConfigFn = (env: Env, _trigger) => { return { service: { name: 'my-worker' }, trace: { exporter: { url: env.SIGNOZ_ENDPOINT, headers: { 'signoz-access-token': env.SIGNOZ_ACCESS_TOKEN }, }, }, } } export default instrument(handler, config)import { trace } from '@opentelemetry/api' import { instrument, getLogger, OTLPTransport, ConsoleTransport } from '@inference-net/otel-cf-workers' const handler = { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { const logger = getLogger('my-app') // Logs automatically include trace context (trace ID, span ID) logger.info('Processing request', { 'http.url': request.url, 'user.id': '123', }) try { await env.MY_KV.get('key') logger.debug('KV operation complete') return new Response('OK') } catch (error) { // Error logs automatically extract exception info logger.error(error as Error) return new Response('Error', { status: 500 }) } }, } const config: ResolveConfigFn = (env: Env, _trigger) => ({ service: { name: 'my-worker' }, trace: { exporter: { url: `${env.OTEL_ENDPOINT}/v1/traces`, headers: { 'x-api-key': env.API_KEY }, }, }, logs: { transports: [ new OTLPTransport({ url: `${env.OTEL_ENDPOINT}/v1/logs`, headers: { 'x-api-key': env.API_KEY }, }), new ConsoleTransport({ pretty: true }), // Also log to console ], }, }) export default instrument(handler, config)import { instrumentDO, ResolveConfigFn } from '@inference-net/otel-cf-workers' class MyDurableObject implements DurableObject { async fetch(request: Request): Promise<Response> { // Auto-instrumented: DO fetch handler await this.ctx.storage.get('key') // Auto-instrumented: DO storage await this.ctx.storage.sql.exec('SELECT * FROM data') // Auto-instrumented: DO SQL return new Response('Hello from DO!') } async alarm(): Promise<void> { // Auto-instrumented: DO alarm handler } } const config: ResolveConfigFn = (env, _trigger) => ({ exporter: { url: env.OTEL_ENDPOINT }, service: { name: 'my-durable-object' }, }) export const MyDO = instrumentDO(MyDurableObject, config)Tracing:
- Distributed Tracing: Automatic W3C Trace Context propagation across services
- Semantic Conventions: Full support for OpenTelemetry semantic conventions (v1.28.0+)
db.query.text- Database queries and keysdb.system.name- Database system identificationdb.operation.name- Operation typesdb.operation.batch.size- Batch operation trackinghttp.*- HTTP request/response attributesfaas.*- FaaS trigger and execution attributes
- Custom Spans: Create manual spans with
trace.getTracer() - Span Attributes: Set custom attributes on active spans
- Context Propagation: Async context management across Workers runtime
- Sampling: Both head and tail sampling strategies
- Exporters: OTLP/HTTP (JSON) format
- Span Processors: Custom trace-based batch processing
Logging:
- Structured Logging: OpenTelemetry Logs API with convenience methods
- Automatic Trace Correlation: Logs include trace ID and span ID from active spans
- Child Loggers: Inherit attributes from parent loggers for context propagation
- Multiple Transports: Send logs to OTLP backends, console, or custom destinations
- Batching Strategies: Configurable batching (immediate or size-based)
- Severity Levels: Standard OpenTelemetry severity levels (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)
- Console Instrumentation: Optional capture of
console.log(),console.error(), etc. - Custom Transports: Extensible transport interface for custom log destinations
📖 See LOGS.md for complete logging documentation
In addition to OpenTelemetry standard attributes, we capture Cloudflare-specific metadata:
cloudflare.*- Platform-specific attributes (ray ID, colo, script version)geo.*- Request geolocation data- Response metadata (TTL, cache status, rows read/written)
- Binding-specific attributes (KV keys, D1 query stats, R2 checksums)
| Feature | Status | Notes |
|---|---|---|
HTTP Handler (fetch) | ✅ | Full support with geo, headers, user-agent parsing |
Scheduled Handler (scheduled) | ✅ | Cron trigger instrumentation |
Queue Consumer (queue) | ✅ | Message batch processing with ack/retry tracking |
Email Handler (email) | ✅ | Incoming email processing |
Durable Object fetch | ✅ | DO HTTP requests |
Durable Object alarm | ✅ | DO alarm triggers |
ctx.waitUntil | ✅ | Background promise tracking |
Tail Handler (tail) | ❌ | Not yet supported |
| DO Hibernated WebSocket | ❌ | Not yet supported |
| Binding | Status | Operations Instrumented |
|---|---|---|
| KV Namespace | ✅ | get, put, delete, list, getWithMetadata |
| R2 Bucket | ✅ | head, get, put, delete, list, createMultipartUpload, resumeMultipartUpload |
| D1 Database | ✅ | prepare, exec, batch, all, run, first, raw |
| Durable Objects | ✅ | Stub fetch calls |
| DO Storage (KV) | ✅ | get, put, delete, list, getAlarm, setAlarm, deleteAlarm |
| DO Storage (SQL) | ✅ | exec, execBatch |
| Queue Producer | ✅ | send, sendBatch |
| Service Bindings | ✅ | Worker-to-worker calls |
| Analytics Engine | ✅ | writeDataPoint |
| Images | ✅ | get, list, delete |
| Rate Limiting | ✅ | limit |
| Workers AI | ❌ | Not yet supported |
| Vectorize | ❌ | Not yet supported |
| Hyperdrive | ❌ | Not yet supported |
| Browser Rendering | ❌ | Not yet supported |
| Email Sending | ❌ | Not yet supported |
| mTLS | ❌ | Not yet supported |
| API | Status | Notes |
|---|---|---|
fetch() | ✅ | Global fetch calls with trace context injection |
caches | ✅ | Cache API operations |
| Module | Status |
|---|---|
cloudflare:email | ❌ |
cloudflare:sockets | ❌ |
const config: ResolveConfigFn = (env: Env, trigger) => ({ service: { name: 'my-service', version: '1.0.0', // Optional namespace: 'production', // Optional }, trace: { exporter: { url: env.SIGNOZ_ENDPOINT, headers: { 'signoz-access-token': env.SIGNOZ_ACCESS_TOKEN }, }, }, // Logs are optional logs: { transports: [new OTLPTransport({ url: env.LOGS_ENDPOINT })], }, })Note: Both trace and logs are optional. You can configure:
- Tracing only
- Logging only
- Both tracing and logging
- Neither (no telemetry)
const config: ResolveConfigFn = (env, trigger) => ({ // ... exporter config sampling: { // Head sampling: sample 10% of requests at start headSampler: { ratio: 0.1, acceptRemote: true, // Accept parent trace decisions }, // Tail sampling: always keep errors even if not head-sampled tailSampler: (trace) => { const rootSpan = trace.localRootSpan return ( rootSpan.status.code === SpanStatusCode.ERROR || (rootSpan.spanContext().traceFlags & TraceFlags.SAMPLED) !== 0 ) }, }, })const config: ResolveConfigFn = (env, trigger) => ({ // ... exporter config // Control outbound trace context fetch: { includeTraceContext: (request) => { // Only propagate to same-origin requests return new URL(request.url).hostname === 'api.example.com' }, }, // Control inbound trace context handlers: { fetch: { acceptTraceContext: (request) => { // Accept trace context from trusted origins return request.headers.get('x-trusted') === 'true' }, }, }, })Redact sensitive data before export:
const config: ResolveConfigFn = (env, trigger) => ({ // ... exporter config postProcessor: (spans) => { return spans.map((span) => { // Redact URLs with tokens if (span.attributes['http.url']) { span.attributes['http.url'] = span.attributes['http.url'].replace(/token=[^&]+/, 'token=REDACTED') } // Remove sensitive headers delete span.attributes['http.request.header.authorization'] return span }) }, })const config: ResolveConfigFn = (env, trigger) => ({ // ... exporter config propagator: new MyCustomPropagator(), })import { trace } from '@opentelemetry/api' const handler = { async fetch(request: Request, env: Env) { const span = trace.getActiveSpan() if (span) { span.setAttribute('user.id', '123') span.setAttribute('user.role', 'admin') } return new Response('OK') }, }import { trace, SpanStatusCode } from '@opentelemetry/api' const handler = { async fetch(request: Request, env: Env) { const tracer = trace.getTracer('my-app') return await tracer.startActiveSpan('process-request', async (span) => { span.setAttribute('request.id', crypto.randomUUID()) try { const result = await doWork() span.setStatus({ code: SpanStatusCode.OK }) return new Response(result) } catch (error) { span.recordException(error) span.setStatus({ code: SpanStatusCode.ERROR }) throw error } finally { span.end() } }) }, }- Timing Accuracy: The Workers runtime does not expose accurate timing information to protect against Spectre attacks. CPU-bound work may show 0ms duration. The clock only updates on I/O operations.
- RPC-Style DO Calls: Direct RPC method calls to Durable Objects (e.g.,
await stub.myMethod()) are not auto-instrumented. Use fetch-style calls (await stub.fetch(request)) for automatic tracing.
See the examples directory for complete working examples:
Tracing:
- Basic Worker - HTTP handler with KV and D1
- Quickstart Guide - Step-by-step tutorial
Logging:
- Basic Logging - Simple logging setup
- Advanced Logging - Traces + logs correlation
- Logs Only - Logging without tracing
- Child Loggers - Context inheritance with child loggers
- Logging Documentation - Complete guide to OpenTelemetry Logs
- OpenTelemetry Documentation
- Cloudflare Workers Documentation
- Semantic Conventions
BSD-3-Clause
Contributions welcome! This is a fork maintained by @context-labs, originally from evanderkoogh/otel-cf-workers.