How to trace LangChain and LangGraph agents
The OpenInference instrumentor turns every chain, model call and tool call of a LangChain or LangGraph run into an OpenTelemetry span. mcpd reads those spans with no mcpd code in your agent.
Last updated
When you finish, each agent run is one trace in mcpd: the graph, every model call with its prompt, answer, tokens and cost, and every tool call with its arguments and result. You need Python 3.10 or later, a LangChain 1.x agent, and a running mcpd daemon on port 4317 (see the Quickstart).
1. Install the packages
Install the OpenInference instrumentor for LangChain and the OpenTelemetry SDK with its OTLP over HTTP exporter. The instrumentor covers LangGraph too, because a LangGraph run goes through the same callbacks.
install
pip install langchain langchain-openai \
openinference-instrumentation-langchain \
opentelemetry-sdk opentelemetry-exporter-otlp-proto-http 2. Point the exporter at mcpd
mcpd takes OTLP on the same port as everything else, 4317, over HTTP and gRPC. The HTTP exporter defaults to port 4318, so name the endpoint.
environment
# The base URL. The exporter adds /v1/traces itself.
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# Only when mcpd runs with --ingest-key.
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_INGEST_KEY" 3. Instrument LangChain before the agent runs
Create the tracer provider and call the instrumentor once, at the start of
the process. The service name becomes the run's service_name in
mcpd.
tracing setup
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from openinference.instrumentation.langchain import LangChainInstrumentor
# Before any chain or agent runs.
provider = TracerProvider(resource=Resource.create({"service.name": "support-agent"}))
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)
LangChainInstrumentor().instrument(tracer_provider=provider) 4. Run the agent with a session and a user
Nothing in the agent changes. Wrap the call in
using_attributes to put a session id and a user id on every span
of the run. mcpd reads session.id as the run's session and
user.id as its user, which lets you
read a whole conversation
and add up cost per user.
agent.py
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from openinference.instrumentation import using_attributes
@tool
def lookup_order(order_id: str) -> str:
"""Look up where an order is."""
return f"Order {order_id} is at the Leeds depot."
agent = create_agent(
ChatOpenAI(model="gpt-4.1-mini"),
tools=[lookup_order],
system_prompt="You answer questions about orders.",
)
# Every span inside the block carries the session and the user.
with using_attributes(session_id="chat-42", user_id="customer-7"):
result = agent.invoke({"messages": [{"role": "user", "content": "Where is order 4417?"}]})
print(result["messages"][-1].content)
# A script exits before the batch is sent unless you flush. A server does not need this.
provider.shutdown() 5. Read the trace
Open localhost:4317
and select the newest run in Runs. The root span is named
LangGraph. You can also read it from the API:
read the run
TRACE=$(curl -s "http://localhost:4317/v1/api/traces?name=LangGraph&limit=1" | jq -r '.traces[0].trace_id')
curl -s "http://localhost:4317/v1/api/traces/$TRACE" \
| jq -r '.spans[] | [.name, .kind, .model, .usage.input_tokens, .cost.total_cost] | @tsv' $ curl -s "http://localhost:4317/v1/api/traces/$TRACE" | jq -r '.spans[] | [.name, .kind, .model, .usage.input_tokens, .cost.total_cost] | @tsv'LangGraph workflow_run model workflow_run ChatOpenAI model_generation gpt-4.1-mini 812 0.00019199999999999998tools workflow_run lookup_order tool_call model workflow_run ChatOpenAI model_generation gpt-4.1-mini 812 0.00019199999999999998 mcpd maps each OpenInference span kind onto its own:
openinference.span.kind | mcpd kind | What it is in a LangChain run |
|---|---|---|
CHAIN | workflow_run | The graph, and each node in it |
LLM | model_generation | One chat model call, with tokens and cost |
TOOL | tool_call | One tool call, with its input and output |
RETRIEVER | retrieval | A retriever query |
AGENT | agent_run | An agent span, where the integration emits one |
The model name comes from llm.model_name, the token counts from
llm.token_count.*, and cached prompt tokens from
llm.token_count.prompt_details.cache_read. The full list is on the
OpenTelemetry page.
Troubleshooting
| What you see | Why, and what to do |
|---|---|
| No trace arrives |
A script exited before the batch processor sent its spans. Call
provider.shutdown() or provider.force_flush() before exit.
If that is not it, check that OTEL_EXPORTER_OTLP_ENDPOINT names port 4317.
|
| Spans arrive, but not the chain or model spans | The instrumentor ran after the code it should wrap had already run. Call it at startup. |
| The exporter logs 401 or 403 |
The daemon has ingest keys. Set OTEL_EXPORTER_OTLP_HEADERS to
Authorization=Bearer and one of them.
|
| Tokens are stored, but the cost is empty |
The model id is not in mcpd's price book. Check it with
mcpd prices show <model>, and add rates as
Cost accounting shows.
|
Related guides
- How to group LLM traces into conversations
- How to track LLM cost per user
- How to read agent traces in Claude Code and Cursor
mcpd is free and open source. Start free at mcpd.sh , or run the daemon yourself.