Skip to content

feat(telemetry): OpenTelemetry export across all runtimes via Effect's OTLP exporters - #860

Open
sam-goodwin wants to merge 6 commits into
mainfrom
claude/opentelemetry-effect-integration-9a2bd9
Open

feat(telemetry): OpenTelemetry export across all runtimes via Effect's OTLP exporters#860
sam-goodwin wants to merge 6 commits into
mainfrom
claude/opentelemetry-effect-integration-9a2bd9

Conversation

@sam-goodwin

@sam-goodwin sam-goodwin commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Add OpenTelemetry export for deployed apps, built on Effect's OTLP exporters (effect/unstable/observability) and wired into every runtime: Worker, Durable Object, Workflow, Lambda (per-event), and Cloudflare Containers, ECS Task, EC2 hosted, Lambda microVM (per-process).

Telemetry is configured like everything else in alchemy — a Layer, wired to resources through the binding infrastructure and composed into the Function/Worker's single Effect.provide. Everything is exported functions from alchemy/Telemetry (tree-shakeable), namespaced at the barrel:

import * as Axiom from "alchemy/Axiom";
import { Ingest, Logs, Traces } from "./observability.ts";

export default Cloudflare.Worker(
  "Worker",
  { main: import.meta.url },
  Effect.gen(function* () {
    // ...
  }).pipe(
    Effect.provide(
      Layer.mergeAll(
        Cloudflare.R2.ReadWriteBucketBinding,
        Axiom.Telemetry({ token: Ingest, traces: Traces, logs: Logs }),
      ),
    ),
  ),
);

Axiom.Telemetry is sugar over the generic Alchemy.Telemetry.layerOtlp binding layer (any OTLP backend), and exporters compose — merge several and every destination receives the telemetry:

Layer.mergeAll(
  Axiom.Telemetry({ token: Ingest, traces: Traces, logs: Logs }),
  Alchemy.Telemetry.layerOtlp({ url: "https://api.honeycomb.io", headers: { "x-honeycomb-team": apiKey } }),
  // or any custom exporter Layer:
  Alchemy.Telemetry.layer(myExporterLayer),
)

How it works

  • Telemetry.layerOtlp is a binding layer: url/header values accept resource Outputs; each layer build appends its destination to a single bound destination list (Redacted values through the secret channel), and the runtime exporter reads the list back. No telemetry layer provided → no export; Effect's no-op tracer keeps instrumentation free.
  • Multiple destinations share ONE exporter set: telemetry is serialized once and fanned out per signal via a tee HttpClient, so trace/span ids agree across destinations; a failing destination is skipped and logged while a healthy one keeps the exporter alive. Telemetry.layer custom layers compose too (loggers/metrics merge; a custom Tracer replaces the built-in one).
  • The per-event bridges build the exporter Layer into each event's request scope, so the batching fiber lives inside the event's I/O context (required on workerd) and buffered telemetry flushes when the scope closes — registered with ctx.waitUntil on Workers, settled inline on Lambda.
  • Server processes use provideProcessTelemetry in their entry templates: built once into the root scope, standard export intervals, final flush at graceful shutdown.
  • Fetch paths get an http.server root span with incoming traceparent continuation — Workers already had HttpMiddleware.tracer via HttpEffect.toHandled; Lambda's HTTP handler now applies it too.
  • Lambda invocations now run with the captured sandbox context provided (mirroring the Worker bridge), so reference-backed services (reified ConfigProvider, loggers) resolve identically at request time.
  • The built-in per-event exporter disables interval batching: Effect's OtlpExporter splices its buffer before posting, so an interval export in flight when the scope closes is interrupted and silently dropped. Lambda invocations regularly outlive the logger's 1-second default interval, which is how this surfaced. The scope-close flush delivers everything instead; the process default keeps standard intervals.

Docs

  • Tutorial Part 6: Observability (cloudflare/tutorial/part-6) — datasets, ingest token, and the Axiom.Telemetry binding layer, plus custom spans and per-request flush semantics. (Cloudflare's own OTel tracing has no OTLP ingest endpoint — runtime spans only — so Axiom is the worked example.)
  • New concepts page /infrastructure-as-effects/telemetry — binding layers, per-event vs per-process flushing, custom exporters.
  • Rewrote /testing/observability, /cloudflare/observability/axiom-observability, and /axiom/data/ingest wiring sections for the binding layer (no hand-rolled OtlpTracer.layer).

Tests

Live e2e for Workers, Lambda, and Axiom (test/Cloudflare/Workers/Telemetry.test.ts, test/AWS/Lambda/Telemetry.test.ts, test/Axiom/Telemetry.test.ts): a Durable-Object-backed OTLP collector worker receives exports from a traced worker (Telemetry.layerOtlp binding + custom Telemetry.layer) and a traced Lambda (mixed AWS+Cloudflare providers), and a Worker bound with Axiom.Telemetry exports to real Axiom datasets, verified out-of-band via an APL query. Server-runtime templates are covered by type-check and entry-bundle validation.

🤖 Generated with Claude Code

…s OTLP exporters

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alchemy-version-bot

alchemy-version-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

alchemy

bun add alchemy@https://pkg.ing/alchemy/c7b38f1

@alchemy.run/better-auth

bun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/c7b38f1

@alchemy.run/pr-package

bun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/c7b38f1

sam-goodwin and others added 5 commits July 17, 2026 14:25
…metry concepts page, per-signal OTEL env support + Axiom.otlpHeaders

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Telemetry.otlp is now a binding layer (Input/Output props bound onto the
host at deploy time); Axiom.Telemetry binds datasets + ingest token;
removed Telemetry.disabled and the env-var-first docs framing. Single
Effect.provide with Layer.mergeAll everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ently in observability guides

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merged Telemetry.otlp/Axiom.Telemetry layers accumulate into a single
bound destination list; the runtime serializes telemetry once and fans
each signal out to every destination via a tee HttpClient, keeping
trace/span ids consistent. Telemetry.layer custom layers now compose
with the OTLP destinations instead of replacing them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Telemetry.otlp/Telemetry.layer were methods on an Object.assign'd
reference, defeating tree-shaking. Now flat exports from
alchemy/Telemetry (layerOtlp, layer, the Telemetry reference),
namespaced at the barrel via export * as Telemetry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Butch78

Butch78 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Sick, love this 👌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants