Skip to content

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 #51

Description

@saltwater-tensor

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

  1. Self-host Langfuse via Docker Compose, confirm the web UI is reachable at http://localhost:3000.
  2. codex plugin marketplace add langfuse/codex-observability-plugin
  3. codex plugin add tracing@codex-observability-plugin
  4. Enable in ~/.codex/config.toml:
    [features]
    plugin_hooks = true
    
    [plugins."tracing@codex-observability-plugin"]
    enabled = true
  5. 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"
  6. Run a Codex session, send two turns. Codex reports:
    Stop hook (failed)
      error: hook exited with code 1
    
  7. 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.
  8. No sidecar file (*.langfuse*) is written next to the rollout.
  9. No trace appears in the Langfuse UI under Tracing.
  10. 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:

provider.register();

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions