util-genai & langchain: nest async/graph child spans under their caller - #528
util-genai & langchain: nest async/graph child spans under their caller#528sidsri14 wants to merge 4 commits into
Conversation
Pull request dashboard statusWaiting 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):
Status above doesn't look right?
|
| 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) |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
1421333 to
5d0ac47
Compare
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
There was a problem hiding this comment.
🔵 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_contextsupport toopentelemetry-util-genaiinvocation 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_contextis provided, the span is correctly parented viastart_span(context=...), but the invocation then builds/attaches_span_contextusingset_span_in_context(self.span)without using the providedparent_contextas the base. This can drop any non-span values carried inparent_context(e.g., baggage or other context keys) from the attached context and from subsequent metric recording that usescontext=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
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:
parent_contextparameter toGenAIInvocationand thelangchain-used handler factories (
workflow,invoke_local_agent,inference,tool). When provided, the invocation's span parentsexplicitly to it instead of relying on the ambient context.
invocation's span from
parent_run_id(walking up the run hierarchy pastintermediate 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_contextis given(or no ancestor invocation is found), behavior is unchanged.
Known gaps
libraries and invocation/factory types are untouched.
attach/detacherror OTel logs internally on asyncpaths is not eliminated by this PR (it is logged, not raised); parenting
is now correct regardless.
agent/workflowspan in LangGraph'screate_agentscenario isstill classified as a workflow rather than an agent in some configurations;
this PR fixes nesting, not the agent/workflow classification.