// --------------------------------------------------------------------------- // Effect → OTEL → Axiom bridge // --------------------------------------------------------------------------- // // Two callers, two setups: // // - `TelemetryLive` (fetch path): reads the global `TracerProvider` that // `@microlabs/otel-cf-workers`' `instrument(...)` installs in `server.ts`. // Flushing is handled by `instrument()` via `ctx.waitUntil` at request end. // // - `DoTelemetryLive` (Durable Object path): the DO runs in a separate // isolate and we deliberately avoid `instrumentDO` (it wraps DO methods // in a way that breaks `this` binding on `WorkerTransport`'s stream // primitives — every MCP request 500s with "Illegal invocation"). // // We install a `WebTracerProvider` once per isolate as the global // provider (lazy on first `DoTelemetryLive` provide, not at module // load — `env` from `cloudflare:workers` is reliably populated at // request time but we keep the lazy gate as a defensive cheap no-op). // Once installed, the provider lives for the entire isolate lifetime, // so deferred MCP SDK callbacks — which fire after the request Effect // has resolved — still hit a live `SimpleSpanProcessor` + exporter. // // Previously the WebSdk layer was scoped per-request: when the outer // `Effect.runPromise(...)` resolved, the layer's scope closed and // `processor.shutdown()` ran. Engine / runtime spans created from // deferred SDK callbacks (which captured the old runtime + tracer) // then silently failed to export, even though they showed up in // `Effect.currentSpan` traces during execution. The DO has been // missing every `executor.code.exec.*` and `executor.runtime.*` span // since `DoTelemetryLive` first started shipping spans. // --------------------------------------------------------------------------- // Subpath imports — the barrel `@effect/opentelemetry` re-exports `NodeSdk`, // which eagerly imports `@opentelemetry/sdk-trace-node` and its // `context-async-hooks` dep. Under vitest-pool-workers that crashes module // load (no `async_hooks` in workerd). Production bundles tree-shake the // unused NodeSdk; vitest does not. import * as Resource from "@effect/opentelemetry/Resource"; import * as OtelTracer from "@effect/opentelemetry/Tracer"; import { trace } from "@opentelemetry/api"; // Force the browser platform entry — the package's conditional export would // otherwise resolve to the Node build, which uses `https.request` / `node:http`. // Under workerd + unenv's nodejs_compat, `https.request` isn't implemented // (surfaces as `[unenv] https.request is not implemented yet!` at export // time) and every DO span fails to ship. The browser build uses `fetch()`, // which workerd does support. import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http/build/esm/platform/browser/index.js"; import { resourceFromAttributes } from "@opentelemetry/resources"; import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base"; import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions"; import { env } from "cloudflare:workers"; import { Effect, Layer } from "effect"; const SERVICE_NAME = "executor-cloud"; const SERVICE_VERSION = "1.0.0"; export const TelemetryLive: Layer.Layer = OtelTracer.layerGlobal.pipe( Layer.provide(Resource.layer({ serviceName: SERVICE_NAME, serviceVersion: SERVICE_VERSION })), ); // Module-scope: one provider per DO isolate, never shut down. The provider // holds the SimpleSpanProcessor + OTLP exporter, so any tracer reference // the engine/runtime spans hold (via captured Effect runtimes) keeps // finding a live exporter even after the request Effect has resolved. let installed = false; const ensureGlobalTracerProvider = (): boolean => { if (installed) return true; if (!env.AXIOM_TOKEN) return false; const provider = new WebTracerProvider({ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: SERVICE_NAME, [ATTR_SERVICE_VERSION]: SERVICE_VERSION, }), spanProcessors: [ new SimpleSpanProcessor( new OTLPTraceExporter({ url: env.AXIOM_TRACES_URL ?? "https://api.axiom.co/v1/traces", headers: { Authorization: `Bearer ${env.AXIOM_TOKEN}`, "X-Axiom-Dataset": env.AXIOM_DATASET ?? "executor-cloud", }, }), ), ], }); // Skip `provider.register()` — its StackContextManager / W3C propagator // setup wires the global OTel context API, but Effect's tracer goes // through `OtelTracer.layerGlobal` which only needs the global provider, // not the OTel context machinery. trace.setGlobalTracerProvider(provider); installed = true; return true; }; export const DoTelemetryLive: Layer.Layer = Layer.unwrap( Effect.sync(() => ensureGlobalTracerProvider() ? OtelTracer.layerGlobal.pipe( Layer.provide( Resource.layer({ serviceName: SERVICE_NAME, serviceVersion: SERVICE_VERSION }), ), ) : Layer.empty, ), );