Effect HttpApi handlers built during Cloudflare Worker initialization capture
Alchemy's console logger and override the request-scoped OTel logger. Request
traces and logs emitted outside the HttpApi handler reach the collector, but
logs emitted inside the handler appear only in the local console.
Versions and scope
alchemy@2.0.0-beta.80
effect@4.0.0
- Node.js
24.18.0, Linux
- Reproduced with
alchemy dev, a local Cloudflare Worker, and Axiom's OTLP
endpoints via Axiom.Telemetry.
Observed in local dev mode; deployed Workers and other runtimes have not been
tested for this failure.
Reproduction
Build the router once during Worker initialization. The reduced example below
uses a generic OTLP collector; replace the URL and supply any required headers.
The live reproduction used Axiom.Telemetry({ token, traces, logs }), which
delegates to the same binding. This generic variant has not been live-tested.
HttpApi definition
// api.ts
import { Schema } from "effect"
import {
HttpApi,
HttpApiEndpoint,
HttpApiGroup,
HttpApiSchema
} from "effect/http-api"
class Health extends HttpApiGroup.make("health").add(
HttpApiEndpoint.get("health", "/health", {
success: Schema.Literal("ok").pipe(HttpApiSchema.asText())
})
) {}
export class Api extends HttpApi.make("Api").add(Health) {}
Worker and handlers
// worker.ts
import * as Alchemy from "alchemy"
import * as Cloudflare from "alchemy/Cloudflare"
import * as Http from "alchemy/Http"
import { Effect, Layer } from "effect"
import { HttpRouter } from "effect/http"
import { HttpApiBuilder } from "effect/http-api"
import { Api } from "./api.ts"
const handlers = HttpApiBuilder.group(Api, "health", (handlers) =>
handlers.handle("health", () =>
Effect.logInfo("inside-httpapi-handler").pipe(
Effect.as("ok" as const),
Effect.withSpan("health.handler")
)
)
)
const api = HttpApiBuilder.layer(Api).pipe(
Layer.provide(handlers),
Layer.provide(Http.Platform)
)
export class Worker extends Cloudflare.Worker<Worker>()("Worker") {}
export default Worker.make(
{ main: import.meta.url },
Effect.gen(function* () {
const fetch = yield* HttpRouter.toHttpEffect(api)
return {
fetch: fetch.pipe(
Effect.tap(() => Effect.logInfo("outside-httpapi-handler"))
)
}
}).pipe(
Effect.provide(
Alchemy.Telemetry.layerOtlp({
url: "https://collector.example.com"
})
)
)
)
Stack
// alchemy.run.ts
import * as Alchemy from "alchemy"
import * as Cloudflare from "alchemy/Cloudflare"
import { Effect } from "effect"
import layerWorker, { Worker } from "./worker.ts"
export default Alchemy.Stack(
"httpapi-telemetry-repro",
{ providers: Cloudflare.providers(), state: Cloudflare.state() },
Effect.gen(function* () {
const worker = yield* Worker
return { url: worker.url.as<string>() }
}).pipe(Effect.provide(layerWorker))
)
With provider credentials and state configured:
- Run
alchemy dev --stage dev_otel_repro.
- Request
/health on the printed Worker URL.
- Query the collector for
inside-httpapi-handler and
outside-httpapi-handler over the reproduction time window.
- Repeat the request to check subsequent requests on the same Worker instance.
Expected vs. actual
Both messages should reach the collector with request-scoped trace/span IDs.
Instead, /health returns 200 OK and the HTTP request span and
outside-handler log reach Axiom, but the inside-handler log appears only in the
local console. Repeated Axiom API queries confirmed the missing log.
Source analysis
The installed source indicates a context-lifetime mismatch:
- Alchemy's
WorkerBridge.ts builds the Worker instance with
Logger.layer([Logger.consolePretty()]) in its platform context.
- Effect's
HttpApiBuilder.group captures the current context, omitting only
Scope.Scope, when constructing routes.
- Alchemy's
WorkerBridge.processEvent installs fresh request-scoped OTel
exporters through buildEventTelemetry.
- Effect's
handlerToHttpEffect applies the captured initialization context
through Effect.provideContext(context). The captured
Logger.CurrentLoggers shadows the request's OTel loggers.
Telemetry.layerOtlp configures bindings during initialization; the actual
exporters are built per request. Providing the binding at initialization is the
documented usage.
Relevant source files:
- Alchemy:
packages/alchemy/src/Cloudflare/Workers/WorkerBridge.ts
- Alchemy:
packages/alchemy/src/TelemetryRuntime.ts
- Alchemy:
packages/alchemy/src/Telemetry.ts
- Effect:
packages/effect/src/http-api/HttpApiBuilder.ts
Verified workaround
Omitting initialization-time telemetry while constructing the router allows
handlers to inherit request telemetry:
import { Context, Effect, Logger, type Scope, Tracer } from "effect"
const makeHttpEffect = HttpRouter.toHttpEffect(api).pipe(
Effect.updateContext((context: Context.Context<Scope.Scope>) =>
Context.omit(
Logger.CurrentLoggers,
Tracer.Tracer,
Tracer.ParentSpan
)(context)
)
)
Yield makeHttpEffect during initialization instead of the original router
construction. This restored handler logs and correctly correlated them with the
child span and HTTP request trace in Axiom.
The directly observed failure concerns loggers. We have not independently
demonstrated a failure for each of Tracer.Tracer and Tracer.ParentSpan;
those were also omitted to avoid capturing initialization-time tracing services.
Constructing the router per request also restored telemetry, but requires
rebuilding the routing layer for every request.
Desired resolution
Support one-time router construction while preserving request-scoped telemetry,
without application-level context stripping. The appropriate fix may be in
Alchemy's initialization context or Effect's HttpApi context capture; explicit
application logger/tracer overrides should remain supported.
A regression test should initialize the router once and verify that successive
requests export handler logs and child spans with their own telemetry contexts
and IDs.
Effect HttpApi handlers built during Cloudflare Worker initialization capture
Alchemy's console logger and override the request-scoped OTel logger. Request
traces and logs emitted outside the HttpApi handler reach the collector, but
logs emitted inside the handler appear only in the local console.
Versions and scope
alchemy@2.0.0-beta.80effect@4.0.024.18.0, Linuxalchemy dev, a local Cloudflare Worker, and Axiom's OTLPendpoints via
Axiom.Telemetry.Observed in local dev mode; deployed Workers and other runtimes have not been
tested for this failure.
Reproduction
Build the router once during Worker initialization. The reduced example below
uses a generic OTLP collector; replace the URL and supply any required headers.
The live reproduction used
Axiom.Telemetry({ token, traces, logs }), whichdelegates to the same binding. This generic variant has not been live-tested.
HttpApi definition
Worker and handlers
Stack
With provider credentials and state configured:
alchemy dev --stage dev_otel_repro./healthon the printed Worker URL.inside-httpapi-handlerandoutside-httpapi-handlerover the reproduction time window.Expected vs. actual
Both messages should reach the collector with request-scoped trace/span IDs.
Instead,
/healthreturns200 OKand the HTTP request span andoutside-handler log reach Axiom, but the inside-handler log appears only in the
local console. Repeated Axiom API queries confirmed the missing log.
Source analysis
The installed source indicates a context-lifetime mismatch:
WorkerBridge.tsbuilds the Worker instance withLogger.layer([Logger.consolePretty()])in its platform context.HttpApiBuilder.groupcaptures the current context, omitting onlyScope.Scope, when constructing routes.WorkerBridge.processEventinstalls fresh request-scoped OTelexporters through
buildEventTelemetry.handlerToHttpEffectapplies the captured initialization contextthrough
Effect.provideContext(context). The capturedLogger.CurrentLoggersshadows the request's OTel loggers.Telemetry.layerOtlpconfigures bindings during initialization; the actualexporters are built per request. Providing the binding at initialization is the
documented usage.
Relevant source files:
packages/alchemy/src/Cloudflare/Workers/WorkerBridge.tspackages/alchemy/src/TelemetryRuntime.tspackages/alchemy/src/Telemetry.tspackages/effect/src/http-api/HttpApiBuilder.tsVerified workaround
Omitting initialization-time telemetry while constructing the router allows
handlers to inherit request telemetry:
Yield
makeHttpEffectduring initialization instead of the original routerconstruction. This restored handler logs and correctly correlated them with the
child span and HTTP request trace in Axiom.
The directly observed failure concerns loggers. We have not independently
demonstrated a failure for each of
Tracer.TracerandTracer.ParentSpan;those were also omitted to avoid capturing initialization-time tracing services.
Constructing the router per request also restored telemetry, but requires
rebuilding the routing layer for every request.
Desired resolution
Support one-time router construction while preserving request-scoped telemetry,
without application-level context stripping. The appropriate fix may be in
Alchemy's initialization context or Effect's HttpApi context capture; explicit
application logger/tracer overrides should remain supported.
A regression test should initialize the router once and verify that successive
requests export handler logs and child spans with their own telemetry contexts
and IDs.