LangGraph
LangGraph runs on LangChain’s callbacks, so the OpenInference LangChain instrumentation (the one Arize Phoenix uses) traces a LangGraph agent with no LangGraph-specific package: every graph node, tool and model call becomes a span. Spanlight reads the spans’ openinference.span.kind and llm.* attributes for the kind, the model, the token counts and the messages. Send them to Spanlight’s OTLP endpoint with an OpenTelemetry tracer provider.
Before you start
- A running Spanlight instance that your application can reach
- A project API key with the ingest:write scope (the default). Creating one needs the member, admin or owner role
- Python 3.13 (the version this page was tested with)
Install
Section titled “Install”pip install "langgraph==1.2.16" "openinference-instrumentation-langchain==0.1.79" "langchain-core==1.6.12" "langchain-openai==1.7.2" "openai==3.35.1" "opentelemetry-sdk==1.45.1" "opentelemetry-exporter-otlp-proto-http==1.45.1"Point the OpenTelemetry exporter at Spanlight with the standard variables:
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:8080/v1/otlp/tracesexport OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobufexport OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer%20spl_live_..."Use the traces-specific endpoint variable with the full path. Configure an exporter explains why. OpenInference records the messages on the span by default, so no other setting is needed.
Set up tracing
Section titled “Set up tracing”Create a tracer provider that exports over OTLP, pass it to the LangChain instrumentor, then build and run the graph as before:
from langchain_core.tools import toolfrom langchain_openai import ChatOpenAIfrom langgraph.prebuilt import create_react_agentfrom openinference.instrumentation.langchain import LangChainInstrumentorfrom opentelemetry import tracefrom opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporterfrom opentelemetry.sdk.trace import TracerProviderfrom opentelemetry.sdk.trace.export import BatchSpanProcessor
provider = TracerProvider()provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))trace.set_tracer_provider(provider)LangChainInstrumentor().instrument(tracer_provider=provider)
@tooldef get_weather(city: str, unit: str = "celsius") -> str: """Get the current weather for a city.""" return "18 degrees and sunny"
agent = create_react_agent(ChatOpenAI(model="gpt-4.1-mini"), [get_weather], name="weather_agent")agent.invoke({"messages": [("user", "What is the weather in Paris and Tokyo? Use the tool.")]})provider.force_flush()A long-running server does not need provider.force_flush(); a short script does, or its last spans are lost.
Check it worked
Section titled “Check it worked”Open Traces in the dashboard and pick the project that owns your key. The recorded run gives a trace named after the graph, weather_agent, with 17 spans: an agent step for each model turn, a tools step for each tool call, the ChatOpenAI model calls and the get_weather tool calls. Open a ChatOpenAI span: it shows the messages, the answer, the token counts and the cost. The trace’s token total is the sum of its two model calls.
What is recorded
Section titled “What is recorded”| Span in the recorded run | OpenInference span kind | Spanlight kind |
|---|---|---|
The graph (weather_agent) and each agent node step |
AGENT |
agent |
tools node steps, RunnableSequence, call_model, should_continue, Prompt |
CHAIN |
chain |
get_weather |
TOOL |
tool |
ChatOpenAI |
LLM |
llm |
On each model call:
- Provider and model from
llm.systemandllm.model_name, the billed snapshot such asgpt-4.1-mini-2025-04-14. - Input, output and cached tokens from
llm.token_count.prompt,llm.token_count.completionandllm.token_count.prompt_details.cache_read. Only model calls carry token counts, so each call is counted once and the graph and node spans add nothing to the totals. - Messages and answer from the flattened
llm.input_messages.*andllm.output_messages.*attributes, including the tool calls the model asked for and the tool results sent back. - Finish reason from
llm.finish_reason:tool_callson the turn that called the tool,stopon the answer. - Cost, computed by Spanlight from the model and the token counts; see How cost is calculated.
Node, tool and chain spans keep input.value and output.value as their input and output.
LangGraph and LangChain add run metadata to the metadata attribute of each span, and Spanlight puts it in the trace metadata. The recorded trace gets langgraph_node, langgraph_step, langgraph_checkpoint_ns, checkpoint_ns and ls_integration from the graph steps, and ls_provider, ls_model_name and ls_model_type from the model calls. They count towards the trace’s 32 metadata keys.
Which attribute fills which field is listed in OpenInference.
What is not recorded
Section titled “What is not recorded”- Time to first token. The recorded run did not stream. On a stream, OpenInference sends the first token as a span event, which Spanlight does not read.
- Cache writes. The recorded calls went to an OpenAI stand-in, which reports none.
- Streaming, subgraphs, checkpointers and the LangGraph Platform. Only a prebuilt ReAct agent run with
invoke()was recorded, so this page makes no claim about the others. - Exporter-computed cost. Spanlight never reads one; it prices every span itself.
OTLP compatibility shows the recorded scenario field by field.
Troubleshooting
Section titled “Troubleshooting”- No trace arrives. Instrument LangChain before you build the graph, pass the tracer provider to
instrument(), and flush it before a script exits. Then work through No trace arrives. - The exporter gets
422 VALIDATION_ERROR. It sent to/v1/traces. SetOTEL_EXPORTER_OTLP_TRACES_ENDPOINTto the full/v1/otlp/tracesURL. - A trace has more metadata keys than you set. They are LangGraph’s
langgraph_*and LangChain’sls_*keys. If they push the trace past 32 keys, the keys after the 32nd are dropped and counted.