| 1 | --- |
| 2 | title: Native Workers tracing |
| 3 | description: Mirror Effect spans into Cloudflare Workers Observability with Cloudflare.Telemetry() — the same Layer shape as Axiom.Telemetry, with Cloudflare owning sampling and export. |
| 4 | sidebar: |
| 5 | order: 15 |
| 6 | --- |
| 7 | |
| 8 | Cloudflare Workers can emit OpenTelemetry traces natively: fetch, KV, R2, |
| 9 | and D1 are auto-instrumented, and custom spans join that waterfall via |
| 10 | `tracing.startActiveSpan`. Alchemy's `Cloudflare.Telemetry()` Layer |
| 11 | forwards `Effect.withSpan` / `Effect.fn` frames into that API so Effect |
| 12 | code shows up next to the platform spans. |
| 13 | |
| 14 | ## Provide the layer |
| 15 | |
| 16 | Pin a compatibility date at or after `2026-07-28` (`startActiveSpan` GA). |
| 17 | The global Worker default is still older, so omit the date and deploy |
| 18 | fails instead of silently dropping Effect spans. |
| 19 | |
| 20 | ```typescript |
| 21 | export default Cloudflare.Worker( |
| 22 | "Worker", |
| 23 | { |
| 24 | main: import.meta.url, |
| 25 | compatibility: { date: "2026-08-25" }, |
| 26 | }, |
| 27 | Effect.gen(function* () { |
| 28 | return { |
| 29 | fetch: Effect.gen(function* () { |
| 30 | yield* doWork().pipe(Effect.withSpan("operation")); |
| 31 | return HttpServerResponse.text("ok"); |
| 32 | }), |
| 33 | }; |
| 34 | }).pipe( |
| 35 | Effect.provide( |
| 36 | Layer.mergeAll( |
| 37 | Cloudflare.R2.ReadWriteBucketBinding, |
| 38 | Cloudflare.Telemetry(), |
| 39 | ), |
| 40 | ), |
| 41 | ), |
| 42 | ); |
| 43 | ``` |
| 44 | |
| 45 | You do **not** set `observability.traces.enabled` on the Worker, write a |
| 46 | Wrangler `observability` block, or configure an OTLP URL. The Layer |
| 47 | enables traces on the host (the same bind path as `Cloudflare.cache()`) |
| 48 | and installs a per-event Effect Tracer. Cloudflare owns sampling and |
| 49 | export — spans submit with the request, no `waitUntil` flush. |
| 50 | |
| 51 | Existing `Effect.withSpan` / `Effect.fn` instrumentation does not change. |
| 52 | |
| 53 | During `alchemy dev` the compatibility date is not gated and the Layer |
| 54 | has no deploy-time effect; whether spans are recorded locally is up to |
| 55 | the local runtime. A Worker opted out of local emulation with |
| 56 | `Alchemy.remote()` traces normally. |
| 57 | |
| 58 | ## What you get |
| 59 | |
| 60 | ### Effect spans in the platform waterfall |
| 61 | |
| 62 | `Effect.withSpan` frames nest with Cloudflare's own spans. A KV read |
| 63 | inside a span shows up as its child, even when the fiber that issued it |
| 64 | was forked or resumed on a later tick. |
| 65 | |
| 66 | ```typescript |
| 67 | const handle = Effect.gen(function* () { |
| 68 | const user = yield* kv.get("user:1").pipe(Effect.withSpan("load.user")); |
| 69 | yield* Effect.all([enrich(user), audit(user)], { concurrency: "unbounded" }); |
| 70 | }).pipe(Effect.withSpan("handle")); |
| 71 | ``` |
| 72 | |
| 73 | ```text |
| 74 | http.server GET |
| 75 | └─ handle |
| 76 | ├─ load.user |
| 77 | │ └─ kv_get |
| 78 | ├─ enrich |
| 79 | └─ audit |
| 80 | ``` |
| 81 | |
| 82 | ### Attributes |
| 83 | |
| 84 | Scalar annotations (`string`, `number`, `boolean`) are forwarded to the |
| 85 | Cloudflare span. Nested objects, span events, and links stay Effect-local. |
| 86 | |
| 87 | ```typescript |
| 88 | yield* Effect.annotateCurrentSpan("user.id", userId); // forwarded |
| 89 | yield* Effect.annotateCurrentSpan("retries", 2); // forwarded |
| 90 | yield* Effect.annotateCurrentSpan("cached", true); // forwarded |
| 91 | yield* Effect.annotateCurrentSpan("input", { nested }); // Effect-local |
| 92 | ``` |
| 93 | |
| 94 | ### Outcome |
| 95 | |
| 96 | Cloudflare has no outcome setter yet, so completion is recorded as the |
| 97 | `effect.exit` attribute: `success`, `failure`, or `interrupted`. |
| 98 | |
| 99 | ```typescript |
| 100 | yield* Effect.fail(new Error("boom")).pipe( |
| 101 | Effect.withSpan("failing"), // effect.exit = "failure" |
| 102 | Effect.ignore, |
| 103 | ); |
| 104 | const fiber = yield* Effect.forkChild( |
| 105 | Effect.sleep("30 seconds").pipe(Effect.withSpan("slow")), |
| 106 | ); |
| 107 | yield* Fiber.interrupt(fiber); // effect.exit = "interrupted" |
| 108 | ``` |
| 109 | |
| 110 | ### Sampling and persistence |
| 111 | |
| 112 | `headSamplingRate` and `persist` are passed straight through to the |
| 113 | Worker's `observability.traces`. Omit them to keep Cloudflare's defaults. |
| 114 | Cloudflare applies the sampling rate when it ingests the trace, so every |
| 115 | invocation still sees a live `Tracer`. |
| 116 | |
| 117 | ```typescript |
| 118 | Cloudflare.Telemetry({ |
| 119 | headSamplingRate: 0.1, // keep 10% of invocations |
| 120 | persist: false, // export only, nothing in the dashboard |
| 121 | }) |
| 122 | ``` |
| 123 | |
| 124 | ## Combining with other exporters |
| 125 | |
| 126 | Effect has a single `Tracer`, and `Cloudflare.Telemetry()` provides it. |
| 127 | Any other exporter you merge in keeps its loggers and metric exporters; |
| 128 | only its `traces` are superseded. With Axiom, for example, leave `traces` |
| 129 | off and the logs and metrics still flow, in either `Layer.mergeAll` |
| 130 | order: |
| 131 | |
| 132 | ```typescript |
| 133 | Effect.provide( |
| 134 | Layer.mergeAll( |
| 135 | Cloudflare.Telemetry(), |
| 136 | Axiom.Telemetry({ token: Ingest, logs: Logs, metrics: Metrics }), |
| 137 | ), |
| 138 | ) |
| 139 | ``` |
| 140 | |
| 141 | The same goes for the generic `Alchemy.Telemetry.layerOtlp({ logs, metrics })`. |
| 142 | |
| 143 | To ship the waterfall (Effect + platform spans) to another backend, |
| 144 | export it from Cloudflare with an |
| 145 | [`ObservabilityDestination`](/cloudflare/observability/axiom-observability) |
| 146 | rather than sending a second Worker-side OTLP trace stream. |
| 147 | |
| 148 | ## Async Workers |
| 149 | |
| 150 | Non-Effect Workers keep the `observability` prop — there is no Tracer to |
| 151 | install: |
| 152 | |
| 153 | ```typescript |
| 154 | yield* Cloudflare.Worker("Api", { |
| 155 | main: "./src/api.ts", |
| 156 | observability: { |
| 157 | traces: { enabled: true }, |
| 158 | }, |
| 159 | }); |
| 160 | ``` |