Skip to content

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)
Terminal window
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:

Terminal window
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:8080/v1/otlp/traces
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
export 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.

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 tool
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
from openinference.instrumentation.langchain import LangChainInstrumentor
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)
LangChainInstrumentor().instrument(tracer_provider=provider)
@tool
def 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.

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.

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.system and llm.model_name, the billed snapshot such as gpt-4.1-mini-2025-04-14.
  • Input, output and cached tokens from llm.token_count.prompt, llm.token_count.completion and llm.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.* and llm.output_messages.* attributes, including the tool calls the model asked for and the tool results sent back.
  • Finish reason from llm.finish_reason: tool_calls on the turn that called the tool, stop on 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.

  • 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.

  • 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. Set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT to the full /v1/otlp/traces URL.
  • A trace has more metadata keys than you set. They are LangGraph’s langgraph_* and LangChain’s ls_* keys. If they push the trace past 32 keys, the keys after the 32nd are dropped and counted.