// ---------------------------------------------------------------------------// 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<never> = 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<never> = Layer.unwrapEffect( Effect.sync(() => ensureGlobalTracerProvider() ? OtelTracer.layerGlobal.pipe( Layer.provide( Resource.layer({ serviceName: SERVICE_NAME, serviceVersion: SERVICE_VERSION }), ), ) : Layer.empty, ),);
// ---------------------------------------------------------------------------
// 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<never> = 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<never> = Layer.unwrapEffect(
Effect.sync(() =>
ensureGlobalTracerProvider()
? OtelTracer.layerGlobal.pipe(
Layer.provide(
Resource.layer({ serviceName: SERVICE_NAME, serviceVersion: SERVICE_VERSION }),
),
)
: Layer.empty,
),
);