open-telemetry / open-telemetry/opentelemetry-python-genai

langchain: spans are not nested — every step becomes its own root span/trace (async), and attach/detach raises "was created in a different Context"

Open
#513 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
39
Forks
63
Avg merge
1d 15h
Merged PRs (30d)
175

Description

Component: opentelemetry-instrumentation-genai-langchain (1.2b0.dev) + opentelemetry-util-genai
Type: bug

Summary

When the LangChain callback handler is driven by an async LangGraph run (graph.ainvoke, and/or sync tools/models invoked under an async graph via a thread-pool), the emitted spans are not nested. Each GenAI step — invoke_workflow, every chat, every execute_tool — is exported as a separate root span with its own trace id (parentId = null). A single user interaction fragments into N one-span traces instead of one connected trace.

At runtime OpenTelemetry also logs:

Failed to detach context
ValueError: <Token var=<ContextVar name='current_context' ...>> was created in a different Context

Root cause

Parent/child linkage relies entirely on the ambient OTel context via attach/detach, and the LangChain parent_run_id — which the handler already receives and stores — is never used to set the span parent.

util/opentelemetry-util-genai/.../_invocation.py:

def _start(self, attributes=None):
    self.span = self._tracer.start_span(      # no context= -> parents from CURRENT contextvar
        name=self._span_name, kind=self._span_kind, attributes=attributes,
    )
    self._span_context = set_span_in_context(self.span)
    self._context_token = attach(self._span_context)   # attach on THIS callback
...
def _finish(self, error=None):
    ...
    detach(context_token)   # detach on a LATER, different callback
    self.span.end()

start_span() is called with no context=, so it takes whatever is in the ambient contextvars at that instant as the parent. attach() happens in on_*_start and the matching detach() happens in on_*_end — a different callback invocation.

instrumentation/.../callback_handler.py receives parent_run_id on every callback but only feeds it to bookkeeping (add_invocation_state, _find_nearest_agent) — it is never turned into a parent span context:

self._invocation_manager.add_invocation_state(run_id, parent_run_id, llm_invocation)

Because the start/finish attach/detach pair is split across separate callbacks, and LangGraph runs nodes as their own asyncio tasks (and dispatches sync models/tools to thread-pool workers), the contextvar set by a parent's attach() is not visible in the child callback's execution context. So:

  1. the child's start_span sees an empty context → new root span → new trace, and
  2. the later detach() runs in a different Context than the attach()ValueError: ... created in a different Context → "Failed to detach context".

attach/detach are LIFO, single-call-stack primitives; using them across separate, possibly-concurrent callbacks that hop asyncio tasks / threads is not sound. (Compare traceloop's opentelemetry-instrumentation-langchain, which keeps its own run_id → span map and explicitly threads the parent via start_span(context=...); under the identical async graph it produces one correctly nested trace.)

Steps to reproduce

Minimal supervisor graph with two react-agent workers, instrumented with LangChainInstrumentor, run with graph.ainvoke:

from opentelemetry.instrumentation.genai.langchain import LangChainInstrumentor
LangChainInstrumentor().instrument(tracer_provider=provider)

# ... build a StateGraph supervisor + weather/finance create_react_agent nodes ...
graph = builder.compile()

await graph.ainvoke({"messages": [{"role": "user",
    "content": "What's the weather in Paris, and convert 100 USD to EUR?"}]})
provider.shutdown()

(Full ~140-line repro available; it's a standard supervisor + create_react_agent weather/finance graph.)

Observed

  • async (graph.ainvoke): Failed to detach context logged; exported spans are all parentId=null, each in its own trace id. One interaction → many single-span traces.
  • Span scope: opentelemetry.util.genai.handler.

Expected

One trace per top-level invocation, with chat / execute_tool / agent spans nested under the workflow/agent span — regardless of sync vs. async execution.

Suggested fix

Don't depend on ambient attach/detach for parent linkage across callbacks. Instead, look up the parent invocation by parent_run_id, and pass its span context explicitly:

parent = self._invocation_manager.get_invocation(parent_run_id)
parent_ctx = parent._span_context if parent else None
span = tracer.start_span(name=..., kind=..., context=parent_ctx, attributes=...)

This makes nesting independent of which asyncio task / thread the callback fires on, and removes the cross-Context detach error. (Also see #491, which is the same attach/detach context-lifetime fragility surfacing via streaming.)

Environment

  • opentelemetry-instrumentation-genai-langchain 1.2b0.dev
  • opentelemetry-util-genai (matching)
  • langgraph V1.x (create_react_agent), langchain-openai, Python 3.14

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with util/opentelemetry-util-genai/.../_invocation.py and instrumentation/.../callback_handler.py, then run the minimal async LangGraph reproduction described in the issue. Trace parent_run_id through invocation state and verify that the async graph produces one nested trace without cross-Context detach errors.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
observability
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.