Skip to content

util-genai & langchain: nest async/graph child spans under their caller - #528

Open
sidsri14 wants to merge 4 commits into
open-telemetry:mainfrom
sidsri14:fix/langchain-async-span-nesting
Open

util-genai & langchain: nest async/graph child spans under their caller#528
sidsri14 wants to merge 4 commits into
open-telemetry:mainfrom
sidsri14:fix/langchain-async-span-nesting

Conversation

@sidsri14

@sidsri14 sidsri14 commented Sep 1, 2026

Copy link
Copy Markdown

Fixes #513 (narrow slice).

What

LangChain async/graph traces currently emit each step as its own root
trace because child callbacks run in LangChain's own task loop where the
ambient OpenTelemetry context doesn't carry the parent span. This makes
child spans (tool, chat, agent, workflow) start with an empty parent and
become their own root traces.

This PR:

  • Adds an optional parent_context parameter to GenAIInvocation and the
    langchain-used handler factories (workflow, invoke_local_agent,
    inference, tool). When provided, the invocation's span parents
    explicitly to it instead of relying on the ambient context.
  • In the langchain callback handler, resolves the nearest live ancestor
    invocation's span from parent_run_id (walking up the run hierarchy past
    intermediate chain nodes that emit no telemetry) and passes it to the
    factories. Child spans now nest under their caller and share its trace id.

The change is fully backward-compatible: when no parent_context is given
(or no ancestor invocation is found), behavior is unchanged.

Known gaps

  • Scoped to LangChain's four span-emitting handlers. Other Instrumentation
    libraries and invocation/factory types are untouched.
  • The cross-Context attach/detach error OTel logs internally on async
    paths is not eliminated by this PR (it is logged, not raised); parenting
    is now correct regardless.
  • The agent/workflow span in LangGraph's create_agent scenario is
    still classified as a workflow rather than an agent in some configurations;
    this PR fixes nesting, not the agent/workflow classification.

@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Sep 2, 2026

Copy link
Copy Markdown

Pull request dashboard status

Waiting on the author · refreshed 2026-09-05 17:16 UTC

Resolve merge conflicts.

Respond to 1 review item (e.g. link a commit, explain why not, ask a follow-up):

  • Inline threads: 1
Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Should this be with reviewers? Comment /dashboard route:reviewers to route it to them.
  • Anything wrong — including the routing? Report it with what you expected; it helps us improve the dashboard.

visited.add(current)
parent = self._invocation_manager.get_invocation(current)
if parent is not None and parent.span.is_recording():
return set_span_in_context(parent.span)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The is_recording() gate discards valid non-recording parents and can produce orphan sampled children and incorrect span depth. Maybe use get_span_context().is_valid.

meta = metadata or {}
provider = meta.get("ls_vector_store_provider") or None
request_model = meta.get("ls_embedding_model") or None
retrieval = self._telemetry_handler.retrieval(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retrieval invocation path is missing parent_context

Allow GenAIInvocation and the langchain-used handler factories
(workflow, invoke_local_agent, inference, tool) to accept an explicit
parent Context. The langchain callback handler resolves the nearest live
ancestor invocation's span from parent_run_id and passes it in, so child
spans nest correctly under their caller instead of becoming their own
root trace when callbacks run outside the ambient context (e.g. async
LangGraph task loops).

Fixes open-telemetry#513

Signed-off-by: Siddharth Srivastava <128143077+sidsri14@users.noreply.github.com>

Assisted-by: Claude
Assisted-by: opencode
Signed-off-by: Siddharth Srivastava <128143077+sidsri14@users.noreply.github.com>

Assisted-by: Claude
Assisted-by: opencode
@sidsri14
sidsri14 force-pushed the fix/langchain-async-span-nesting branch from 1421333 to 5d0ac47 Compare September 3, 2026 10:28
Use get_span_context().is_valid instead of span.is_recording() when
resolving the parent context, so a valid parent whose span is not
recorded (e.g. downstream sampling) still parents the child and keeps
the trace tree's shape and depth correct.

Also thread parent_context through the retrieval invocation path
(handler.retrieval and the langchain on_retriever_start callback) for
consistency with the other span-emitting operations.

Signed-off-by: Siddharth Srivastava <128143077+sidsri14@users.noreply.github.com>

Assisted-by: Claude
Assisted-by: opencode
@sidsri14
sidsri14 marked this pull request as ready for review September 5, 2026 13:45
@sidsri14
sidsri14 requested a review from a team as a code owner September 5, 2026 13:45
Copilot AI lite review requested due to automatic review settings September 5, 2026 13:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The explicit parent_context path parents spans correctly but currently builds/attaches the invocation’s _span_context without using that provided parent context as the base, which can drop other context values (e.g., baggage) and make propagation inconsistent.

Pull request overview

This PR addresses broken span nesting in async LangChain/LangGraph runs by allowing GenAI invocations to explicitly parent spans to a provided OpenTelemetry Context, and by teaching the LangChain callback handler to resolve and pass the nearest emitted ancestor span context derived from parent_run_id.

Changes:

  • Add optional parent_context support to opentelemetry-util-genai invocation factories and wire it into span creation.
  • Implement ancestor span-context lookup in the LangChain callback handler (walks run hierarchy past non-emitting nodes) and pass it to span-emitting factories.
  • Add/extend tests to verify explicit parenting and correct nesting under masked ambient contexts.
File summaries
File Description
util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py Adds parent_context parameter to selected handler factories and threads it into invocation construction.
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_invocation.py Uses parent_context to explicitly parent spans when starting an invocation.
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_workflow_invocation.py Threads parent_context into workflow invocation base initialization.
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py Threads parent_context into tool invocation base initialization.
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_retrieval_invocation.py Threads parent_context into retrieval invocation base initialization.
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_inference_invocation.py Threads parent_context into inference invocation base initialization.
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_agent_invocation.py Threads parent_context into agent invocation base initialization.
util/opentelemetry-util-genai/tests/test_handler_workflow.py Adds a unit test asserting workflow spans parent to an explicit context.
util/opentelemetry-util-genai/tests/test_handler_retrieval.py Adds a unit test asserting retrieval spans parent to an explicit context.
instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py Resolves ancestor span context from parent_run_id and passes it to factories to ensure nesting across task boundaries.
instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py Updates expectations for new parameter and adds tests verifying nesting under masked ambient context.
instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/528.fixed Changelog fragment documenting the nesting fix.
Review details

Suppressed comments (1)

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

  • When parent_context is provided, the span is correctly parented via start_span(context=...), but the invocation then builds/attaches _span_context using set_span_in_context(self.span) without using the provided parent_context as the base. This can drop any non-span values carried in parent_context (e.g., baggage or other context keys) from the attached context and from subsequent metric recording that uses context=self._span_context. Consider building the span context from the explicit parent context so context propagation is consistent with the explicit parenting behavior.
        if self._parent_context is not None:
            self.span = self._tracer.start_span(
                name=self._span_name,
                kind=self._span_kind,
                attributes=attributes,
                context=self._parent_context,
            )
        else:
            self.span = self._tracer.start_span(
                name=self._span_name,
                kind=self._span_kind,
                attributes=attributes,
            )
        self._span_context = set_span_in_context(self.span)
        self._monotonic_start_s = timeit.default_timer()
        self._context_token = attach(self._span_context)

  • Files reviewed: 12/12 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Build the attached span context on top of the provided parent context

instead of the ambient context, so baggage and other non-span values

carried in the parent context survive into span and metric recording.

Assisted-by: Claude Code by Anthropic
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants