Skip to content

Commit bbec84d

Browse files
authored
feat: propagate W3C trace context (TRACEPARENT) to CLI subprocess (#821)
## What If the SDK caller has an active OpenTelemetry span, inject its W3C trace context into the CLI subprocess env as `TRACEPARENT`/`TRACESTATE`. The CLI reads these in headless mode and parents its `claude_code.interaction` spans under the caller's distributed trace. - Optional: `pip install claude-agent-sdk[otel]` pulls `opentelemetry-api`. Gracefully no-ops if not installed. - `setdefault` so `ClaudeAgentOptions(env={"TRACEPARENT": ...})` wins. ## Why Agent SDK customers see CLI traces as disconnected roots in their collector. This is the minimal change to enable distributed tracing end-to-end without adding a `tracer` option (consistent with the env-var-only config decision). Relates to #452. This PR does not add in-process SDK spans (#530/#542) — only context propagation to the CLI. ## Requires `claude` CLI **≥ v2.1.110** for the propagation to take effect — earlier versions ignore `TRACEPARENT` (harmless no-op, spans just stay as roots). ## Proof - `pytest tests/test_transport.py` — 62 pass (3 new) - `ruff check` / `ruff format --check` / `mypy src/` — clean
1 parent aaac538 commit bbec84d

3 files changed

Lines changed: 417 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ dev = [
3939
"mypy>=1.0.0",
4040
"ruff>=0.1.0",
4141
]
42+
otel = [
43+
"opentelemetry-api>=1.20.0",
44+
]
4245

4346
[project.urls]
4447
Homepage = "https://github.com/anthropics/claude-agent-sdk-python"
@@ -84,6 +87,12 @@ warn_no_return = true
8487
warn_unreachable = true
8588
strict_equality = true
8689

90+
[[tool.mypy.overrides]]
91+
# opentelemetry-api is an optional dependency (the [otel] extra); the
92+
# import in subprocess_cli.py is guarded by try/except ImportError.
93+
module = ["opentelemetry", "opentelemetry.*"]
94+
ignore_missing_imports = true
95+
8796
[tool.ruff]
8897
target-version = "py310"
8998
line-length = 88

src/claude_agent_sdk/_internal/transport/subprocess_cli.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,32 @@ async def connect(self) -> None:
361361
"CLAUDE_AGENT_SDK_VERSION": __version__,
362362
}
363363

364+
# Propagate active OTEL trace context to the CLI so its spans
365+
# parent under the caller's distributed trace. No-op if
366+
# opentelemetry-api is not installed or there's no active span.
367+
try:
368+
from opentelemetry import propagate
369+
370+
carrier: dict[str, str] = {}
371+
propagate.inject(carrier)
372+
if "traceparent" in carrier:
373+
# Active span present: scrub stale inherited W3C context
374+
# (CI/k8s ambient env) before writing the fresh values, so
375+
# an inherited TRACESTATE isn't paired with a new
376+
# TRACEPARENT. Explicit ClaudeAgentOptions.env always wins.
377+
# Gate on the traceparent key (not carrier truthiness) so a
378+
# baggage-only / non-W3C carrier doesn't scrub a valid
379+
# inherited TRACEPARENT.
380+
for key in ("TRACEPARENT", "TRACESTATE"):
381+
if key not in self._options.env:
382+
process_env.pop(key, None)
383+
for k, v in carrier.items():
384+
key = k.upper()
385+
if key not in self._options.env:
386+
process_env[key] = v
387+
except Exception: # noqa: BLE001 - best-effort tracing must never break connect()
388+
logger.debug("OTEL trace context injection failed", exc_info=True)
389+
364390
# Enable file checkpointing if requested
365391
if self._options.enable_file_checkpointing:
366392
process_env["CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING"] = "true"

0 commit comments

Comments
 (0)