Skip to content
mcpd
no daemon

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'
one agent run, as mcpd stored it
$ 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.kindmcpd kindWhat it is in a LangChain run
CHAINworkflow_runThe graph, and each node in it
LLMmodel_generationOne chat model call, with tokens and cost
TOOLtool_callOne tool call, with its input and output
RETRIEVERretrievalA retriever query
AGENTagent_runAn 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 seeWhy, 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.

mcpd is free and open source. Start free at mcpd.sh , or run the daemon yourself.