claude-code - 💡(How to fix) Fix [BUG] OTel log events (claude_code.user_prompt, api_request_body, tool_decision, hook_execution_complete) emitted with empty trace_id/span_id while sibling spans correlate correctly

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

When OTEL_LOGS_EXPORTER=otlp is enabled, the CLI emits claude_code.* OTel log events (via the com.anthropic.claude_code.events instrumentation scope) with empty trace_id and span_id fields — even though TRACEPARENT propagation works correctly for spans in the same subprocess invocation. This breaks log↔trace correlation in any backend (SigNoz, Honeycomb, Datadog, Grafana) and forces a DB-side join via session.id to recover execution context.

Root Cause

  • All log↔trace navigation in observability backends is broken for claude_code.* events.
  • Per-tenant / per-execution event filtering requires a DB join via session.id → application session store → thread_id/execution_id, instead of a backend-side filter on execution.id or trace_id.
  • Worth fixing because spans already work — the gap is just in log-event emission paths.

Fix Action

Workaround

Pivot from log events to traces through session.id only, or rely on traces (claude_code.interaction, claude_code.tool, claude_code.llm_request) instead of log events for execution-level correlation.

Code Example

event.name: api_request_body
trace_id: ""
span_id: ""
user.id: <SDK hashed user id, not domain user>
session.id: <SDK auto-generated UUID>
prompt.id: <ok>
RAW_BUFFERClick to expand / collapse

Summary

When OTEL_LOGS_EXPORTER=otlp is enabled, the CLI emits claude_code.* OTel log events (via the com.anthropic.claude_code.events instrumentation scope) with empty trace_id and span_id fields — even though TRACEPARENT propagation works correctly for spans in the same subprocess invocation. This breaks log↔trace correlation in any backend (SigNoz, Honeycomb, Datadog, Grafana) and forces a DB-side join via session.id to recover execution context.

Environment

  • Claude Code CLI: 2.1.153
  • claude-agent-sdk-python: 0.1.81
  • Parent process spawns the CLI via claude_agent_sdk.query() with an active parent OTel span
  • Subprocess env includes:
    • CLAUDE_CODE_ENABLE_TELEMETRY=1
    • OTEL_LOGS_EXPORTER=otlp
    • OTEL_LOG_USER_PROMPTS=1
    • OTEL_LOG_TOOL_DETAILS=1
    • OTEL_LOG_RAW_API_BODIES=1
  • OTLP/HTTP to a sidecar collector → SigNoz

Expected

Each claude_code.* log event carries trace_id and span_id derived from the active span context at emit time, so users can pivot from a log event to the parent trace in one click.

Actual

On the same subprocess invocation:

  • claude_code.interaction span: parentSpanID = af90b0d5d83dfe10 ✅ (TRACEPARENT propagated)
  • claude_code.tool / claude_code.llm_request spans: parented correctly ✅
  • claude_code.user_prompt / api_request_body / tool_decision / hook_execution_complete log events: trace_id = "", span_id = ""

Sample raw event attributes (from com.anthropic.claude_code.events scope):

event.name: api_request_body
trace_id: ""
span_id: ""
user.id: <SDK hashed user id, not domain user>
session.id: <SDK auto-generated UUID>
prompt.id: <ok>

The events do carry session.id, but it's the SDK's auto-generated session UUID, not the parent's execution.id — and there's no on-event way to join back without a DB lookup.

Likely cause

OTel JS LogRecords populate spanContext from context.active() at emit time. If logger.emit(...) is invoked from a callback that has lost AsyncLocalStorage context (event-emitter dispatch, microtask boundary, etc.), the LogRecord ships with empty spanContext. Wrapping each event emission in context.with(trace.setSpan(context.active(), interactionSpan), () => logger.emit(...)) would fix it.

Impact

  • All log↔trace navigation in observability backends is broken for claude_code.* events.
  • Per-tenant / per-execution event filtering requires a DB join via session.id → application session store → thread_id/execution_id, instead of a backend-side filter on execution.id or trace_id.
  • Worth fixing because spans already work — the gap is just in log-event emission paths.

Workaround

Pivot from log events to traces through session.id only, or rely on traces (claude_code.interaction, claude_code.tool, claude_code.llm_request) instead of log events for execution-level correlation.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING