Skip to content

fix(langchain): nest client spans under runs, instrument fetch (undici), close init race, correct span kinds - #191

Draft
Jackson Weber (JacksonWeber) wants to merge 5 commits into
microsoft:mainfrom
JacksonWeber:jacksonweber/langchain-init-race-fix
Draft

fix(langchain): nest client spans under runs, instrument fetch (undici), close init race, correct span kinds#191
Jackson Weber (JacksonWeber) wants to merge 5 commits into
microsoft:mainfrom
JacksonWeber:jacksonweber/langchain-init-race-fix

Conversation

@JacksonWeber

@JacksonWeber Jackson Weber (JacksonWeber) commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Improves the LangChain / GenAI tracing story in the distro:

  • Captures HTTP client spans for fetch-based clients (the OpenAI SDK used by LangChain).
  • Nests those client spans under the LLM / tool run that issued them, instead of leaving them as disconnected root traces.
  • Closes a startup race that could drop the top-level invoke_agent span.
  • Corrects GenAI span kinds so agent and tool spans surface correctly in Azure Monitor / the "AI agents (preview)" experience.

What changed

Features Added

  • Instrument outgoing fetch (undici) requests so HTTP client spans are captured for fetch-based clients such as the OpenAI SDK. The distro previously only registered @opentelemetry/instrumentation-http (Node core http/https); LLM calls issued via global fetch produced no client spans. When the fetch-based A365 exporter is active, a merged undici ignoreRequestHook skips its export origin so telemetry traffic is not self-traced.
  • Nest client spans under their originating run via the LangChain-core wrapRunExecution callback hook. LangChainTracer now implements the hook: core invokes it around a run body (a chat model's _generate, each streaming step, and a tool's _call), and we make that run's span the active OTel context for the duration, so client instrumentations (HTTP / fetch / undici) nest under the run's span.
  • Deterministic nesting — force awaitHandlers = true on the tracer. Span creation happens in the async onRunCreate callback; with LangChain's default background callbacks that work is queued, so a run body's fetch could execute before the span existed and client spans nested only intermittently. Awaiting guarantees the span is registered before the body runs, independent of the host app's LANGCHAIN_CALLBACKS_BACKGROUND setting.
  • whenGenAIInstrumentationsReady() so ESM apps can await GenAI (LangChain / OpenAI Agents) instrumentation setup before their first invocation, eliminating a startup race that could drop the top-level invoke_agent span.

Bugs Fixed

  • LangChain: emit invoke_agent spans as INTERNAL (not SERVER) so Azure Monitor records them as dependencies and they surface in the Application Insights "AI agents (preview)" experience.
  • LangChain: emit execute_tool spans as INTERNAL (not CLIENT) to match the GenAI "execute tool" semantic convention and the Python distro (in-process tool execution).

Relationship to langchain-ai/langchainjs#11211

The client-span nesting behavior is delivered jointly by two changes in two repos:

langchain-ai/langchainjs#11211 (core) this PR (distro)
Adds optional wrapRunExecution(runId, fn) hook on the callback-handler contract; BaseRunManager.withRunContext / withRunContextAsyncIterable that invoke it around chat _generate, streaming steps, and tool _call implements wrapRunExecution on LangChainTracer to activate the run's span as the current context; forces awaitHandlers=true so the span exists in time
Role provides & calls the hook consumes the hook to propagate OTel context

Core explicitly no-ops the hook for handlers that don't implement it, and only a handler that keeps the run's span active can make client spans nest — so neither change nests spans on its own.

Dependency: the nesting behavior requires langchain-ai/langchainjs#11211 to be merged and released. Until then, core does not call the hook, so it is an inert no-op: HTTP / fetch client spans are still captured (via the undici instrumentation above) but remain disconnected root traces rather than nesting under their chat / tool run. All other changes in this PR — undici instrumentation, whenGenAIInstrumentationsReady(), and the span-kind corrections — are independent of #11211.

Verification

Ran a LangGraph react-agent sample (@langchain/core from #11211 + this distro) against Azure Monitor across 9 agent invocations. Every trace produced the expected shape and all 18 LLM HTTP spans nested under their chat span and all 18 tool HTTP spans nested under their execute_tool span (0% → 100% vs. the distro change alone):

invoke_agent LangGraph [Dependency]
├─ chat gpt-4o-mini [Dependency]     → POST /openai/.../chat/completions
├─ execute_tool get_current_weather  → GET /v1/forecast
├─ execute_tool get_current_weather  → GET /v1/forecast
└─ chat gpt-4o-mini [Dependency]     → POST /openai/.../chat/completions

Testing

  • test/internal/unit/distro/instrumentations.test.ts — undici registration wiring, ignore-hook behavior, and A365 origin resolution.
  • test/internal/unit/genai/langchain/tracer.test.ts — span kinds, the wrapRunExecution run-context hook, and the awaitHandlers guarantee.
  • test/internal/functional/genai-distro.test.ts and test/internal/unit/main.test.ts — updated for the wiring above.

The distro only registered @opentelemetry/instrumentation-http (Node core
http/https). The OpenAI SDK used by LangChain issues requests via the global
fetch (undici) on Node 18+, so LLM HTTP calls produced no client spans.

Register @opentelemetry/instrumentation-undici (enabled by default). When the
fetch-based A365 exporter is active, a merged undici ignoreRequestHook skips
its export origin so telemetry traffic is not self-traced (undici does not
honor tracing suppression). Adds unit tests for the registration wiring, the
ignore-hook behavior, and A365 origin resolution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add whenGenAIInstrumentationsReady() so ESM apps can await GenAI
  instrumentation setup before their first invocation, eliminating a
  startup race that could drop the top-level invoke_agent span
- Emit invoke_agent spans as INTERNAL (not SERVER) so Azure Monitor
  records them as dependencies for the AI agents (preview) experience
- Emit execute_tool spans as INTERNAL (not CLIENT) per the GenAI
  execute tool semantic convention and the Python distro

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement the optional wrapRunExecution callback hook on LangChainTracer.
LangChain-core invokes it around a run body (chat model _generate, streaming
steps, tool _call) after the run's span is opened, so we activate that span as
the current OTel context for the duration. Client instrumentations firing inside
the body (HTTP/fetch/undici) then nest their spans under the run's span instead
of forming disconnected root traces.

Relies on the wrapRunExecution hook added in langchain-ai/langchainjs#11211;
no-op until that change ships (core skips handlers that don't implement it).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ministic

Span creation runs in the async onRunCreate callback. With LangChain's default
background callbacks (LANGCHAIN_CALLBACKS_BACKGROUND !== 'false') that work is
queued, so a run body's fetch could execute before the span existed, leaving
wrapRunExecution with no span to activate and client (HTTP/fetch) spans nesting
only intermittently. Force awaitHandlers=true on the tracer so the span is
registered before the run body runs, making nesting deterministic regardless of
the host app's callback configuration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@JacksonWeber Jackson Weber (JacksonWeber) changed the title fix(langchain): close init race, correct span kinds, and instrument fetch (undici) fix(langchain): nest client spans under runs, instrument fetch (undici), close init race, correct span kinds Jul 16, 2026
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.

1 participant