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
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Improves the LangChain / GenAI tracing story in the distro:
fetch-based clients (the OpenAI SDK used by LangChain).invoke_agentspan.What changed
Features Added
fetch(undici) requests so HTTP client spans are captured forfetch-based clients such as the OpenAI SDK. The distro previously only registered@opentelemetry/instrumentation-http(Node corehttp/https); LLM calls issued via globalfetchproduced no client spans. When the fetch-based A365 exporter is active, a merged undiciignoreRequestHookskips its export origin so telemetry traffic is not self-traced.wrapRunExecutioncallback hook.LangChainTracernow 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.awaitHandlers = trueon the tracer. Span creation happens in the asynconRunCreatecallback; with LangChain's default background callbacks that work is queued, so a run body'sfetchcould 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'sLANGCHAIN_CALLBACKS_BACKGROUNDsetting.whenGenAIInstrumentationsReady()so ESM apps canawaitGenAI (LangChain / OpenAI Agents) instrumentation setup before their first invocation, eliminating a startup race that could drop the top-levelinvoke_agentspan.Bugs Fixed
invoke_agentspans asINTERNAL(notSERVER) so Azure Monitor records them as dependencies and they surface in the Application Insights "AI agents (preview)" experience.execute_toolspans asINTERNAL(notCLIENT) 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:
wrapRunExecution(runId, fn)hook on the callback-handler contract;BaseRunManager.withRunContext/withRunContextAsyncIterablethat invoke it around chat_generate, streaming steps, and tool_callwrapRunExecutiononLangChainTracerto activate the run's span as the current context; forcesawaitHandlers=trueso the span exists in timeCore 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 /
fetchclient 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/corefrom #11211 + this distro) against Azure Monitor across 9 agent invocations. Every trace produced the expected shape and all 18 LLM HTTP spans nested under theirchatspan and all 18 tool HTTP spans nested under theirexecute_toolspan (0% → 100% vs. the distro change alone):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, thewrapRunExecutionrun-context hook, and theawaitHandlersguarantee.test/internal/functional/genai-distro.test.tsandtest/internal/unit/main.test.ts— updated for the wiring above.