Issue
Stop hook exits cleanly and reports "parsed N turn(s)" but no trace ever reaches Langfuse (self-hosted): startObservation spans never attached to the registered NodeTracerProvider
Summary
On a self-hosted Langfuse instance (v4.15.0 OSS), the tracing plugin's Stop hook runs to completion with no thrown error, correctly parses the rollout transcript, and reports [langfuse-codex] parsed 1 turn(s) from <file> in debug mode, but no trace ever appears in Langfuse and no HTTP request reaches the Langfuse web container's ingestion endpoint. shutdown() (which calls spanProcessor.forceFlush()) also completes without error.
Environment
- OS: Windows 11, PowerShell
- Codex CLI version: 0.148.0
- Node.js: v22.23.2
- Plugin:
langfuse/codex-observability-plugin, marketplace confirmed up to date, plugin version 0.1.0
- Langfuse: self-hosted v4.15.0 OSS via
docker compose (default compose file from the langfuse/langfuse repo, only Postgres host port remapped to avoid a local collision)
@langfuse/otel version bundled in dist/index.mjs: 5.4.1
Steps to reproduce
- Self-host Langfuse via Docker Compose, confirm the web UI is reachable at
http://localhost:3000.
codex plugin marketplace add langfuse/codex-observability-plugin
codex plugin add tracing@codex-observability-plugin
- Enable in
~/.codex/config.toml:
[features]
plugin_hooks = true
[plugins."tracing@codex-observability-plugin"]
enabled = true
- Set env vars:
$env:TRACE_TO_LANGFUSE = "true"
$env:LANGFUSE_PUBLIC_KEY = "pk-lf-..."
$env:LANGFUSE_SECRET_KEY = "sk-lf-..."
$env:LANGFUSE_BASE_URL = "http://localhost:3000"
$env:LANGFUSE_CODEX_DEBUG = "true"
- Run a Codex session, send two turns. Codex reports:
Stop hook (failed)
error: hook exited with code 1
- Invoking the compiled hook directly with a correctly-shaped
HookInput JSON payload on stdin (matching src/types.ts) runs cleanly:
'{"transcript_path":"<path-to-rollout>.jsonl"}' | node dist/index.mjs
Output:
[langfuse-codex] parsed 1 turn(s) from rollout-basic-main.jsonl
Exit code: 0. No error is logged despite LANGFUSE_CODEX_DEBUG=true.
- No sidecar file (
*.langfuse*) is written next to the rollout.
- No trace appears in the Langfuse UI under Tracing.
docker logs langfuse-langfuse-web-1 --since 5m shows zero incoming requests during the test window — the ingestion endpoint is never hit.
Isolation performed
To rule out the obvious causes before filing:
- Auth/keys/endpoint verified independently. A manual
Invoke-WebRequest to http://localhost:3000/api/public/otel/v1/traces with a correctly Base64-encoded Authorization: Basic <public>:<secret> header returns 200 OK. Without the header it correctly returns 401. This confirms the credentials, base URL, and endpoint path are all correct and reachable.
- Raw Node fetch verified.
node -e "fetch('http://127.0.0.1:3000/api/public/otel/v1/traces', {...}).then(...)" returns status: 401 instantly, confirming Node's networking stack on this machine has no issue reaching the container (ruling out IPv6/localhost resolution quirks).
- Config resolution verified correct.
src/config.ts correctly reads LANGFUSE_BASE_URL (with LANGFUSE_CODEX_BASE_URL taking precedence per the documented pattern), and the hook does not log the "missing LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY" debug line, confirming all four required config values resolve correctly from the environment.
OTEL_LOG_LEVEL=debug produces no additional diagnostic output from the bundled OTel SDK.
- Endpoint construction inside the bundle verified correct, matches
${baseUrl}/api/public/otel/v1/traces, i.e. the same path confirmed reachable above.
Root cause (suspected)
src/trace.ts creates spans via startObservation imported from @langfuse/tracing:
import {
createTraceId,
propagateAttributes,
startObservation,
...
} from "@langfuse/tracing";
Per @langfuse/tracing's own documentation, startObservation resolves its tracer via getLangfuseTracerProvider(), which returns an isolated provider only if one was explicitly set via setLangfuseTracerProvider(provider); otherwise it falls back to whatever is registered as the OpenTelemetry global tracer provider.
src/instrumentation.ts constructs a NodeTracerProvider with the LangfuseSpanProcessor attached and calls:
which sets it as the global OTel provider — but setLangfuseTracerProvider(provider) is never called anywhere in the codebase. Confirmed via:
Select-String -Path src\instrumentation.ts -Pattern "setLangfuseTracerProvider" # no matches
Select-String -Path dist\index.mjs -Pattern "setLangfuseTracerProvider" # no matches — not even imported into the bundle
If @langfuse/tracing's provider resolution doesn't reliably fall through to the OTel global registry in this execution context (short-lived CLI process, ESM bundling via tsdown, possible module-instance mismatch between the bundled copies of @opentelemetry/api used by @langfuse/otel vs. @langfuse/tracing), spans created by startObservation are silently attached to a no-op tracer. This would explain every symptom observed: clean parse, clean (no-op) flush, zero network traffic, zero errors, at every log level.
Suggested fix
Call setLangfuseTracerProvider(provider) (from @langfuse/tracing) immediately after provider.register() in src/instrumentation.ts, so startObservation deterministically uses the same provider instance the LangfuseSpanProcessor is attached to, regardless of global-registry fallback behavior. Reference: @langfuse/tracing's own isolated-provider docs recommend this exact pairing for standalone Node scripts.
Additional notes
- Node version (
v22.23.2) satisfies the plugin's stated Node.js 22+ requirement, so this isn't a Node-version issue.
- The Claude Code sibling plugin (
langfuse-observability) is working correctly for me on the same machine against the same self-hosted Langfuse instance, so the self-hosted backend itself is confirmed functional end-to-end; the issue is isolated to codex-observability-plugin.
- Happy to test a patched build if useful.
Issue
Stop hook exits cleanly and reports "parsed N turn(s)" but no trace ever reaches Langfuse (self-hosted):
startObservationspans never attached to the registeredNodeTracerProviderSummary
On a self-hosted Langfuse instance (v4.15.0 OSS), the
tracingplugin's Stop hook runs to completion with no thrown error, correctly parses the rollout transcript, and reports[langfuse-codex] parsed 1 turn(s) from <file>in debug mode, but no trace ever appears in Langfuse and no HTTP request reaches the Langfuse web container's ingestion endpoint.shutdown()(which callsspanProcessor.forceFlush()) also completes without error.Environment
langfuse/codex-observability-plugin, marketplace confirmed up to date, plugin version0.1.0docker compose(default compose file from thelangfuse/langfuserepo, only Postgres host port remapped to avoid a local collision)@langfuse/otelversion bundled indist/index.mjs:5.4.1Steps to reproduce
http://localhost:3000.codex plugin marketplace add langfuse/codex-observability-plugincodex plugin add tracing@codex-observability-plugin~/.codex/config.toml:HookInputJSON payload on stdin (matchingsrc/types.ts) runs cleanly:0. No error is logged despiteLANGFUSE_CODEX_DEBUG=true.*.langfuse*) is written next to the rollout.docker logs langfuse-langfuse-web-1 --since 5mshows zero incoming requests during the test window — the ingestion endpoint is never hit.Isolation performed
To rule out the obvious causes before filing:
Invoke-WebRequesttohttp://localhost:3000/api/public/otel/v1/traceswith a correctly Base64-encodedAuthorization: Basic <public>:<secret>header returns200 OK. Without the header it correctly returns401. This confirms the credentials, base URL, and endpoint path are all correct and reachable.node -e "fetch('http://127.0.0.1:3000/api/public/otel/v1/traces', {...}).then(...)"returnsstatus: 401instantly, confirming Node's networking stack on this machine has no issue reaching the container (ruling out IPv6/localhost resolution quirks).src/config.tscorrectly readsLANGFUSE_BASE_URL(withLANGFUSE_CODEX_BASE_URLtaking precedence per the documented pattern), and the hook does not log the "missing LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY" debug line, confirming all four required config values resolve correctly from the environment.OTEL_LOG_LEVEL=debugproduces no additional diagnostic output from the bundled OTel SDK.${baseUrl}/api/public/otel/v1/traces, i.e. the same path confirmed reachable above.Root cause (suspected)
src/trace.tscreates spans viastartObservationimported from@langfuse/tracing:Per
@langfuse/tracing's own documentation,startObservationresolves its tracer viagetLangfuseTracerProvider(), which returns an isolated provider only if one was explicitly set viasetLangfuseTracerProvider(provider); otherwise it falls back to whatever is registered as the OpenTelemetry global tracer provider.src/instrumentation.tsconstructs aNodeTracerProviderwith theLangfuseSpanProcessorattached and calls:which sets it as the global OTel provider — but
setLangfuseTracerProvider(provider)is never called anywhere in the codebase. Confirmed via:If
@langfuse/tracing's provider resolution doesn't reliably fall through to the OTel global registry in this execution context (short-lived CLI process, ESM bundling viatsdown, possible module-instance mismatch between the bundled copies of@opentelemetry/apiused by@langfuse/otelvs.@langfuse/tracing), spans created bystartObservationare silently attached to a no-op tracer. This would explain every symptom observed: clean parse, clean (no-op) flush, zero network traffic, zero errors, at every log level.Suggested fix
Call
setLangfuseTracerProvider(provider)(from@langfuse/tracing) immediately afterprovider.register()insrc/instrumentation.ts, sostartObservationdeterministically uses the same provider instance theLangfuseSpanProcessoris attached to, regardless of global-registry fallback behavior. Reference:@langfuse/tracing's own isolated-provider docs recommend this exact pairing for standalone Node scripts.Additional notes
v22.23.2) satisfies the plugin's statedNode.js 22+requirement, so this isn't a Node-version issue.langfuse-observability) is working correctly for me on the same machine against the same self-hosted Langfuse instance, so the self-hosted backend itself is confirmed functional end-to-end; the issue is isolated tocodex-observability-plugin.