Description
HTTP CLIENT spans created by opentelemetry-instrumentation-httpx (and other transport instrumentors like opentelemetry-instrumentation-urllib) appear as siblings of anthropic.chat instead of children, because _wrap and _instrumented_converse_stream use tracer.start_span() rather than tracer.start_as_current_span().
When start_span() is used, the span is created but not attached to the OTel context. Any child spans created during the underlying HTTP call (by the httpx instrumentor using start_as_current_span) inherit the ambient context — which is the parent of anthropic.chat, not anthropic.chat itself.
Expected trace tree
invoke_agent
└── anthropic.chat ← gen_ai span
└── POST /v1/messages ← HTTP CLIENT span (currently a sibling, not a child)
Actual trace tree
invoke_agent
├── anthropic.chat ← gen_ai span
└── POST /v1/messages ← HTTP CLIENT span (sibling instead of child)
Root cause
In opentelemetry-instrumentation-anthropic, _wrap (line ~555):
span = tracer.start_span(name, kind=SpanKind.CLIENT, attributes={...})
# span is NOT the current context from here
...
response = wrapped(*args, **kwargs) # HTTP call happens here — wrong parent
In opentelemetry-instrumentation-bedrock, _instrumented_converse_stream (line ~334):
span = tracer.start_span(...)
# span is NOT the current context
try:
response = fn(*args, **kwargs) # boto3/httpx call — wrong parent
Note: _instrumented_converse (non-streaming) correctly uses start_as_current_span — so the nesting bug only affects the streaming path in the bedrock instrumentor and all paths in the anthropic instrumentor.
Minimal fix
Attach the span to context for the duration of the underlying call, then detach (keeping the span open for streaming):
# anthropic _wrap — around response = wrapped(*args, **kwargs)
ctx_token = context_api.attach(context_api.set_value(SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY, True))
# ACTUALLY: attach the span itself
from opentelemetry.trace import set_span_in_context
ctx_token = context_api.attach(set_span_in_context(span))
try:
response = wrapped(*args, **kwargs)
except Exception as e:
...
raise
finally:
context_api.detach(ctx_token)
Same pattern in _instrumented_converse_stream.
context.attach + detach is safe for streaming: it makes the span current during the HTTP handshake (so child spans nest correctly) but detaches immediately after wrapped() returns, while the span itself remains open for the streaming body to be consumed.
Environment
opentelemetry-instrumentation-anthropic 0.62.1
opentelemetry-instrumentation-bedrock 0.62.1
- Python 3.12
Impact
With opentelemetry-instrumentation-httpx installed alongside these instrumentors, all HTTP CLIENT spans emitted during LLM calls appear at the wrong level in the trace tree, making it impossible to correlate the HTTP request latency and status code with the specific LLM call that triggered it.
Description
HTTP CLIENT spans created by
opentelemetry-instrumentation-httpx(and other transport instrumentors likeopentelemetry-instrumentation-urllib) appear as siblings ofanthropic.chatinstead of children, because_wrapand_instrumented_converse_streamusetracer.start_span()rather thantracer.start_as_current_span().When
start_span()is used, the span is created but not attached to the OTel context. Any child spans created during the underlying HTTP call (by the httpx instrumentor usingstart_as_current_span) inherit the ambient context — which is the parent ofanthropic.chat, notanthropic.chatitself.Expected trace tree
Actual trace tree
Root cause
In
opentelemetry-instrumentation-anthropic,_wrap(line ~555):In
opentelemetry-instrumentation-bedrock,_instrumented_converse_stream(line ~334):Note:
_instrumented_converse(non-streaming) correctly usesstart_as_current_span— so the nesting bug only affects the streaming path in the bedrock instrumentor and all paths in the anthropic instrumentor.Minimal fix
Attach the span to context for the duration of the underlying call, then detach (keeping the span open for streaming):
Same pattern in
_instrumented_converse_stream.context.attach+detachis safe for streaming: it makes the span current during the HTTP handshake (so child spans nest correctly) but detaches immediately afterwrapped()returns, while the span itself remains open for the streaming body to be consumed.Environment
opentelemetry-instrumentation-anthropic0.62.1opentelemetry-instrumentation-bedrock0.62.1Impact
With
opentelemetry-instrumentation-httpxinstalled alongside these instrumentors, all HTTP CLIENT spans emitted during LLM calls appear at the wrong level in the trace tree, making it impossible to correlate the HTTP request latency and status code with the specific LLM call that triggered it.