Use this file to discover all available pages before exploring further.
LangChain.js is the JavaScript/TypeScript port of LangChain — a framework for composing LLM calls, tools, and retrieval into chains and agents. Arize AX captures every chain, prompt, tool call, and LLM call by manually instrumenting the @langchain/core/callbacks/manager module via the @arizeai/openinference-instrumentation-langchain package.
// example.ts// Importing instrumentation first ensures tracing is set up before any// LangChain client is created.import { provider } from "./instrumentation";import { ChatOpenAI } from "@langchain/openai";import { ChatPromptTemplate } from "@langchain/core/prompts";import { StringOutputParser } from "@langchain/core/output_parsers";// ChatOpenAI reads OPENAI_API_KEY from the environment.const model = new ChatOpenAI({ model: "gpt-5" });const prompt = ChatPromptTemplate.fromTemplate( "Answer the question concisely.\nQuestion: {question}\nAnswer:",);const chain = prompt.pipe(model).pipe(new StringOutputParser());const result = await chain.invoke({ question: "Why is the ocean salty? Answer in two sentences.",});console.log(result);// Flush any pending spans before the process exits.await provider.forceFlush();
Arize AX tracing initialized for LangChain.js.The ocean is salty because rivers continuously dissolve mineral salts from rocks and soil and carry them to the sea, where they accumulate over millions of years. Water leaves the ocean through evaporation but the salts remain, steadily concentrating until reaching today's roughly 3.5% salinity.
Open your Arize AX space and select project langchain-js-tracing-example.
You should see a new trace within ~30 seconds containing a RunnableSequence parent span (CHAIN) wrapping ChatPromptTemplate (CHAIN), ChatOpenAI (LLM, model gpt-5), and StrOutputParser (CHAIN) child spans, with the prompt, response, and token usage attached to the LLM span.
The basic setup above exports every span the tracer provider receives. When LangChain runs alongside other instrumentations (@opentelemetry/instrumentation-http, Next.js’s built-in tracing, framework HTTP middleware), those instrumentations emit POST / GET spans for every fetch, and the LangChain spans nest under those HTTP roots. OpenInference-tagged spans carry an openinference.span.kind attribute that the LangChain instrumentor sets — checking for it is enough to drop non-OpenInference spans. Swap SimpleSpanProcessor in instrumentation.ts for a filtering subclass:
import { ReadableSpan, SimpleSpanProcessor,} from "@opentelemetry/sdk-trace-base";import { SemanticConventions } from "@arizeai/openinference-semantic-conventions";function isOpenInferenceSpan(span: ReadableSpan): boolean { return ( typeof span.attributes[SemanticConventions.OPENINFERENCE_SPAN_KIND] === "string" );}class OpenInferenceFilteringSpanProcessor extends SimpleSpanProcessor { onEnd(span: ReadableSpan): void { if (!isOpenInferenceSpan(span)) return; super.onEnd(span); }}// then in spanProcessors:new OpenInferenceFilteringSpanProcessor( new OTLPTraceExporter({ /* ... */ }),);
The trade-off: filtering removes the HTTP root spans, which orphans the surviving LangChain spans on the Traces tab (no parent to anchor them) — they remain visible on the Spans tab. If you also need a clean trace tree on the Traces tab, swap the filter for a span processor that promotes the first LangChain span to root by clearing its parent ID:
// root-aware-processor.tsimport { Context } from "@opentelemetry/api";import { BatchSpanProcessor, ReadableSpan, Span, SpanExporter,} from "@opentelemetry/sdk-trace-base";import { getSession } from "@arizeai/openinference-core";import { SemanticConventions, SESSION_ID,} from "@arizeai/openinference-semantic-conventions";import { LRUCache } from "lru-cache";// Top-level LangChain / LangGraph runnable names. `LangGraph` is the// `lc_name()` of a compiled Pregel graph (returned by `createReactAgent`// and friends), `RunnableSequence` is the typical chain root for// `prompt.pipe(model).pipe(parser)`-style chains, and `AgentExecutor`// covers legacy LangChain agents. Match on the first whitespace-delimited// token so suffixed run names (e.g. `RunnableSequence my-chain`) still hit.const ROOT_OI_SPAN_PREFIXES = [ "LangGraph", "RunnableSequence", "AgentExecutor",];function isRootOISpanByName(spanName: string): boolean { const head = spanName.split(" ")[0]; return ROOT_OI_SPAN_PREFIXES.some( (prefix) => head === prefix || head.startsWith(prefix + " "), );}function isOpenInferenceSpan(span: ReadableSpan): boolean { return ( typeof span.attributes[SemanticConventions.OPENINFERENCE_SPAN_KIND] === "string" );}interface RootAwareConfig { exporter: SpanExporter; /** LRU size for tracking which traces have a promoted root. */ cacheSize?: number;}/** * Filters non-OpenInference spans (HTTP, fetch, etc.) and promotes the * first LangChain span in each trace to root by clearing its parent IDs. * Also propagates session ids from context onto every emitted span. */export class RootAwareOpenInferenceProcessor extends BatchSpanProcessor { private traceIds: LRUCache<string, boolean>; constructor(config: RootAwareConfig) { super(config.exporter); this.traceIds = new LRUCache({ max: config.cacheSize ?? 1000 }); } onStart(span: Span, parentContext: Context): void { const session = getSession(parentContext); if (session?.sessionId) { span.setAttribute(SESSION_ID, session.sessionId); } const traceId = span.spanContext().traceId; if ( isRootOISpanByName(span.name) && !this.traceIds.has(traceId) ) { // parentSpanId is readonly on the public Span type; cast to clear. (span as unknown as { parentSpanId?: string }).parentSpanId = undefined; (span as unknown as { parentSpanContext?: unknown }) .parentSpanContext = undefined; this.traceIds.set(traceId, true); } super.onStart(span, parentContext); } onEnd(span: ReadableSpan): void { if (!isOpenInferenceSpan(span)) return; super.onEnd(span); } shutdown(): Promise<void> { this.traceIds.clear(); return super.shutdown(); }}
Wire it in by replacing the OpenInferenceFilteringSpanProcessor in instrumentation.ts:
import { RootAwareOpenInferenceProcessor } from "./root-aware-processor";spanProcessors: [ new RootAwareOpenInferenceProcessor({ exporter: new OTLPTraceExporter({ url: "https://otlp.arize.com/v1/traces", headers: { "arize-space-id": process.env.ARIZE_SPACE_ID ?? "", "arize-api-key": process.env.ARIZE_API_KEY ?? "", }, }), }),],
The recipe needs two additional dependencies: npm install @arizeai/openinference-core lru-cache.
No traces in Arize AX. Confirm ARIZE_SPACE_ID and ARIZE_API_KEY are set in the same shell that runs example.ts. Enable OpenTelemetry debug logs with export OTEL_LOG_LEVEL=debug and re-run.
LangChain spans missing but other spans present.instrumentation.manuallyInstrument(CallbackManagerModule) must run before any code creates a LangChain client. Make sure import { provider } from "./instrumentation" (or a side-effect-only import "./instrumentation") is the first import in your entry point.
401 from OpenAI. Verify OPENAI_API_KEY is set and has access to gpt-5. Swap for a model your key can call.
Process exits before spans flush. Spans are exported asynchronously; always await provider.forceFlush() (or provider.shutdown()) before the process exits to avoid losing trailing spans.
LangChain spans orphaned on the Traces tab. Expected when isOpenInferenceSpan is the only filter — see Span filter above for the RootAwareOpenInferenceProcessor recipe that promotes the first LangChain span to root.
Instrumentation >=1.0.0 supports both attribute masking and context attribute propagation. The matrix below tracks instrumentor support across LangChain core releases: