{"id":13469451,"url":"https://github.com/evanderkoogh/otel-cf-workers","last_synced_at":"2025-05-14T20:06:00.242Z","repository":{"id":169659763,"uuid":"622210526","full_name":"evanderkoogh/otel-cf-workers","owner":"evanderkoogh","description":"An OpenTelemetry compatible library for instrumenting and exporting traces for Cloudflare Workers","archived":false,"fork":false,"pushed_at":"2025-01-11T03:05:46.000Z","size":511,"stargazers_count":299,"open_issues_count":30,"forks_count":58,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-04-06T14:03:55.157Z","etag":null,"topics":["cloudflare","observability","otel","workers"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/evanderkoogh.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-04-01T12:58:16.000Z","updated_at":"2025-03-26T05:55:21.000Z","dependencies_parsed_at":"2023-10-24T10:33:48.897Z","dependency_job_id":"83d57f52-5dcd-4656-bca4-aa6ac4ed698b","html_url":"https://github.com/evanderkoogh/otel-cf-workers","commit_stats":null,"previous_names":["evanderkoogh/otel-cf-workers"],"tags_count":24,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/evanderkoogh%2Fotel-cf-workers","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/evanderkoogh%2Fotel-cf-workers/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/evanderkoogh%2Fotel-cf-workers/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/evanderkoogh%2Fotel-cf-workers/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/evanderkoogh","download_url":"https://codeload.github.com/evanderkoogh/otel-cf-workers/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248748967,"owners_count":21155675,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflare","observability","otel","workers"],"created_at":"2024-07-31T15:01:40.623Z","updated_at":"2025-04-13T16:47:37.598Z","avatar_url":"https://github.com/evanderkoogh.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# otel-cf-workers\n\nAn OpenTelemetry compatible library for instrumenting and exporting traces from Cloudflare Workers.\n\n## Getting started\n\n```bash\nnpm install @microlabs/otel-cf-workers @opentelemetry/api\n```\n\n\u003e [!IMPORTANT]\n\u003e To be able to use the Open Telemetry library you have to add the NodeJS compatibility flag in your `wrangler.toml` file.\n\n```\ncompatibility_flags = [ \"nodejs_compat\" ]\n```\n\nFor a simple setup example with configuration examples, have a look at the [Quickstart Example](https://github.com/evanderkoogh/otel-cf-workers/tree/main/examples/worker)\n\n### Code example\n\n```typescript\nimport { trace } from '@opentelemetry/api'\nimport { instrument, ResolveConfigFn } from '@microlabs/otel-cf-workers'\n\nexport interface Env {\n\tHONEYCOMB_API_KEY: string\n\tOTEL_TEST: KVNamespace\n}\n\nconst handler = {\n\tasync fetch(request: Request, env: Env, ctx: ExecutionContext): Promise\u003cResponse\u003e {\n\t\tawait fetch('https://cloudflare.com')\n\n\t\tconst greeting = \"G'day World\"\n\t\ttrace.getActiveSpan()?.setAttribute('greeting', greeting)\n\t\tctx.waitUntil(fetch('https://workers.dev'))\n\t\treturn new Response(`${greeting}!`)\n\t},\n}\n\nconst config: ResolveConfigFn = (env: Env, _trigger) =\u003e {\n\treturn {\n\t\texporter: {\n\t\t\turl: 'https://api.honeycomb.io/v1/traces',\n\t\t\theaders: { 'x-honeycomb-team': env.HONEYCOMB_API_KEY },\n\t\t},\n\t\tservice: { name: 'greetings' },\n\t}\n}\n\nexport default instrument(handler, config)\n```\n\n## Auto-instrumentation\n\n### Workers\n\nWrapping your exporter handler with the `instrument` function is all you need to do to automatically have not just the functions of you handler auto-instrumented, but also the global `fetch` and `caches` and all of the supported bindings in your environment such as KV.\n\nSee the quick start code sample for an example of how it works.\n\n### Durable Objects\n\nInstrumenting Durable Objects work very similar to the regular Worker auto-instrumentation. Instead of wrapping the handler in an `instrument` call, you wrap the Durable Object class with the `instrumentDO` function.\n\n```typescript\nimport { instrumentDO, PartialTraceConfig } from '@microlabs/otel-cf-workers'\n\nconst config: ResolveConfigFn = (env: Env, _trigger) =\u003e {\n\treturn {\n\t\texporter: {\n\t\t\turl: 'https://api.honeycomb.io/v1/traces',\n\t\t\theaders: { 'x-honeycomb-team': env.HONEYCOMB_API_KEY },\n\t\t},\n\t\tservice: { name: 'greetings-do' },\n\t}\n}\n\nclass OtelDO implements DurableObject {\n\tasync fetch(request: Request): Promise\u003cResponse\u003e {\n\t\treturn new Response('Hello World!')\n\t}\n}\n\nconst TestOtelDO = instrumentDO(OtelDO, doConfig)\n\nexport { TestOtelDO }\n```\n\n## Creating custom spans\n\nWhile auto-instrumenting should take care of a lot of the information that you would want to add, there will always be application specific information you want to send along.\n\nYou can get the current active span by doing:\n\n```typescript\nimport {trace} from '@opentelemetry/api'\n\nconst handler = {\n\tasync fetch(request: Request) {\n\t\tconst span = trace.getActiveSpan()\n\t\tif(span) span.setAttributes('name', 'value')\n\t\t....\n\t}\n}\n```\n\nOr if you want to create a new span:\n\n```typescript\nimport { trace } from '@opentelemetry/api'\n\nconst handler = {\n\tasync fetch(request: Request) {\n\t\tconst tracer = trace.getTracer('my_own_tracer_name')\n\t\treturn tracer.startActiveSpan('name', (span) =\u003e {\n\t\t\tconst response = await doSomethingAwesome\n\t\t\tspan.end()\n\t\t\treturn response\n\t\t})\n\t},\n}\n```\n\n## Configuration\n\nFor configuration you can either pass in a [TraceConfig](https://github.com/evanderkoogh/otel-cf-workers/blob/0da125a4e16ff13e49f8e486340eb6080e631eb9/src/types.ts#L24C18-L24C29) or a function that takes the Environment and the trigger for this particular trace and returns a `TraceConfig`.\n\nBecause the configuration function is run separately for every new invocation, it is possible to tailor your configuration for every type of request. So it is for example possible to have a much lower sampling ratio for your healthchecks than actual API requests.\n\n### Exporter\n\nIn the `exporter`, you need to configure where to send spans to. It can take either an instance of a class that implements the standard Open Telemetry `SpanExporter`interface, or an object with the properties `url` and optionally `headers` to configure an exporter for the Open Telemetry format.\n\nExamples:\n\n```typescript\nconst exporter = new ConsoleSpanExporter()\n```\n\n```typescript\nconst exporter = {\n\turl: 'https://api.honeycomb.io/v1/traces',\n\theaders: { 'x-honeycomb-team': env.HONEYCOMB_API_KEY },\n}\n```\n\n### Fetch\n\n`includeTraceContext` is used to specify if outgoing requests should include the TraceContext so that the other service can participate in a distributed trace.\nThe default is `true` for all outgoing requests, but you can turn it off for all requests with `false`, or specify a method that takes the outgoing `Request` method and return a boolean on whether to include the tracing context.\n\nExample:\n\n```typescript\nconst fetchConf = (request: Request): boolean =\u003e {\n\treturn new URL(request.url).hostname === 'example.com'\n}\n```\n\n### Handlers\n\nThe `handlers` field of the configuration overrides the way in which event handlers, such as `fetch` or `queue`, are instrumented.\n\n#### Fetch Handler\n\n`acceptTraceContext` is used to specify if incoming requests handled by `fetch` should accept a TraceContext and participate in a distributed trace.\nThe default is `true` for all incoming requests, but you can turn it off for all requests with `false` or specify a method that takes the incoming `Request` and returns a boolean indicating whether to accept the tracing context.\n\nExample:\n\n```typescript\nconst fetchConf = (request: Request): boolean =\u003e {\n\treturn new URL(request.url).hostname === 'example.com'\n}\n```\n\n### PostProcessor\n\nThe PostProcessor function is called just before exporting the spans and allows you to make any changes to the spans before sending this. For example to remove entire spans, or to remove or redact security or privacy sensitive data.\n\nExample:\n\n```typescript\nconst postProcessor = (spans: ReadableSpan[]): ReadableSpan[] =\u003e {\n\tspans[0].attributes['http.url'] = 'REDACTED'\n\treturn spans\n}\n```\n\n### Sampling\n\nOne of the challenges of tracing is that for sites and applications with a lot of traffic it becomes prohibitively expensive to store every trace. So the question becomes how to store the ones with the most interesting information and drop the ones that are the least interesting. That is where sampling comes in.\n\n#### Head Sampling vs Tail Sampling\n\nThere are two (complimentary) sampling strategies: Head Sampling and Tail Sampling and in a lot of cases you will want to use a combination to get the most information into the least amount of sampled events.\n\nTo understand the difference in head vs tail sampling in our context, we have to understand distributed tracing. A distributed trace is one that spans multiple systems or services. At every point another service is called, we inject a header with the information about the trace, such as the traceId, the parentSpanId and a hint if this trace is sampled.\n\nHead Sampling, as the name implies, is done at the beginning of a span/trace. In our case it is mostly used to signal to downstream systems whether or not to sample a particular trace, because we can always drop the current services portion of a trace during Tail Sampling.\n\nHead Sampling can be configured with any standard Open Telemetry `Sampler` or an object with a `ratio` property and optional `acceptRemote` property. The default is the AlwaysOnSampler, which samples every single request.\n\nExamples:\n\n```typescript\nconst headSampler = new AlwaysOnSampler()\n```\n\n```typescript\nconst headSampler = {\n\tacceptRemote: false //Whether to accept incoming trace contexts\n\tratio: 0.5 //number between 0 and 1 that represents the ratio of requests to sample. 0 is none and 1 is all requests.\n}\n```\n\nTail Sampling on the other hand is done at the end. Because we record every single span, even if it isn't head sampled, it is possible to still sample the local part of a trace in say the event of an error.\n\nExample:\n\n```typescript\nconst tailSampler = (traceInfo: LocalTrace): boolean =\u003e {\n\tconst localRootSpan = traceInfo.localRootSpan as unknown as ReadableSpan\n\treturn (localRootSpan.spanContext().traceFlags \u0026 TraceFlags.SAMPLED) === TraceFlags.SAMPLED\n}\n```\n\nThe default is a tailSampler that samples traces that have been head sampled or if the local root span is marked as an error.\n\n#### Service\n\nService identifies the service and version to help with querying.\n\nExample:\n\n```typescript\nconst service = {\n\tname: 'some_name' //required. The name of your service\n\tversion: '1.0.4' //optional: An opaque version string. Can be a semver or git hash for example\n\tnamespace: 'namespace' //optional: Useful to group multiple services together in one namespace.\n}\n```\n\n### Propagation\n\nRegister a custom propagator with:\n\n```ts\nconst config: ResolveConfigFn = (env: Env, _trigger) =\u003e {\n\treturn {\n\t\tpropagator: new MyCoolPropagator(),\n\t}\n}\n```\n\n## Distributed Tracing\n\nOne of the advantages of using Open Telemetry is that it makes it easier to do distributed tracing through multiple different services. This library will automatically inject the W3C Trace Context headers when making calls to Durable Objects or outbound fetch calls.\n\n## Limitations\n\n- The worker runtime does not expose accurate timing information to protect against side-channel attacks such as Spectre and will only update the clock on IO, so any CPU heavy processing will look like it takes 0 milliseconds.\n- Not everything is auto-instrumented yet. See the lists below for what is and isn't.\n\nTriggers:\n\n- [x] Email (`handler.email`)\n- [x] HTTP (`handler.fetch`)\n- [x] Queue (`handler.queue`)\n- [x] Cron (`handler.scheduled`)\n- [ ] Tail (`handler.tail`)\n- [x] Durable Objects fetch\n- [x] Durable Objects alarm\n- [ ] Durable Objects hibernated WebSocket\n- [x] waitUntil (`ctx.waitUntil`)\n\nGlobals/built-ins:\n\n- [x] Fetch\n- [x] Caches\n- [x] Durable Object Storage\n\nCloudflare modules\n\n- [ ] `cloudflare:email`\n- [ ] `cloudflare:sockets`\n\nBindings:\n\n- [x] KV\n- [x] Queue\n- [x] Durable Objects\n- [ ] R2\n- [x] D1\n- [x] Service Bindings\n- [x] Analytics Engine\n- [ ] Browser Rendering\n- [ ] Workers AI\n- [ ] Email Sending\n- [ ] mTLS\n- [ ] Vectorize\n- [ ] Hyperdrive\n- [ ] Workers for Platforms Dispatch\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fevanderkoogh%2Fotel-cf-workers","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fevanderkoogh%2Fotel-cf-workers","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fevanderkoogh%2Fotel-cf-workers/lists"}