[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
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=1OTEL_LOGS_EXPORTER=otlpOTEL_LOG_USER_PROMPTS=1OTEL_LOG_TOOL_DETAILS=1OTEL_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.interactionspan:parentSpanID = af90b0d5d83dfe10✅ (TRACEPARENTpropagated)claude_code.tool/claude_code.llm_requestspans: parented correctly ✅claude_code.user_prompt/api_request_body/tool_decision/hook_execution_completelog 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 onexecution.idortrace_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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗