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.