npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@inference-net/otel-cf-workers

v2.4.0

Published

OpenTelemetry instrumentation for Cloudflare Workers with automatic **tracing** and **logging** for handlers, bindings, and distributed traces.

Downloads

10,820

Readme

otel-cf-workers

OpenTelemetry instrumentation for Cloudflare Workers with automatic tracing and logging for handlers, bindings, and distributed traces.

Installation

yarn add @inference-net/otel-cf-workers @opentelemetry/api

Requirements

Add the nodejs_compat compatibility flag to your wrangler.toml:

compatibility_flags = ["nodejs_compat"]

Quick Start

Tracing Only

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)

Tracing + Logging

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)

Durable Objects

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)

OpenTelemetry Features

✅ Fully Supported

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 keys
    • db.system.name - Database system identification
    • db.operation.name - Operation types
    • db.operation.batch.size - Batch operation tracking
    • http.* - HTTP request/response attributes
    • faas.* - 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

Cloudflare-Specific Attributes

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)

Cloudflare Platform Support

Triggers & Handlers

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

Bindings

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

Global APIs

| API | Status | Notes | | --------- | ------ | ----------------------------------------------- | | fetch() | ✅ | Global fetch calls with trace context injection | | caches | ✅ | Cache API operations |

Cloudflare Modules

| Module | Status | | -------------------- | ------ | | cloudflare:email | ❌ | | cloudflare:sockets | ❌ |

Configuration

Basic Configuration

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)

Sampling

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

Trace Context Propagation

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'
			},
		},
	},
})

Post-Processing

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

Custom Propagator

const config: ResolveConfigFn = (env, trigger) => ({
	// ... exporter config
	propagator: new MyCustomPropagator(),
})

Manual Instrumentation

Adding Attributes

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')
	},
}

Creating Custom Spans

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()
			}
		})
	},
}

Limitations

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

Examples

See the examples directory for complete working examples:

Tracing:

Logging:

Resources

License

BSD-3-Clause

Contributing

Contributions welcome! This is a fork maintained by @context-labs, originally from evanderkoogh/otel-cf-workers.