---
title: "Trace LangGraph and LangChain"
description: "Trace LangGraph and LangChain agents in Python with OpenInference's LangChain instrumentor."
sidebar:
  label: "LangGraph and LangChain"
seo:
  title: "Trace LangGraph and LangChain | Judgment Docs"
  description: "Trace LangGraph and LangChain agents in Python with OpenInference's LangChain instrumentor."
---

The instrumentor connects the same way as every other [OpenTelemetry library](/documentation/connect/integrations/otel-instrumentation-libraries). To keep sending runs to LangSmith as well, see [Keep your existing tool](/documentation/connect/integrations/keep-your-existing-tool).

## Register the OpenInference instrumentor

`LangChainInstrumentor` hooks LangChain's callback system, which LangGraph runs on, and creates a span for each chain, model call, and tool call. Judgment's ingest maps OpenInference attributes (span kind, input, output, model, tokens) to its own keys.

```bash
pip install judgeval langchain langchain-openai openinference-instrumentation-langchain
```

```python title="agent.py"
from judgeval import Tracer
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from openinference.instrumentation.langchain import LangChainInstrumentor

Tracer.init(project_name="lunch-order-agent")
Tracer.registerOTELInstrumentation(LangChainInstrumentor())

@tool
def search_nearby_restaurants(cuisine: str) -> list[str]:
    """Find restaurants near the office that serve a cuisine."""
    return ["Olive & Thyme", "Falafel House", "Cedar Grill"]

agent = create_agent(
    ChatOpenAI(model="gpt-5.2"),
    tools=[search_nearby_restaurants],
)
result = agent.invoke(
    {"messages": [("user", "order me mediterranean food to feed 30 hungry engineers")]}
)
print(result["messages"][-1].content)
Tracer.force_flush()
```

Register the instrumentor once, right after `Tracer.init`, before any graph runs. Don't also `wrap()` the OpenAI client underneath `ChatOpenAI`: the instrumentor already records that call.

## Don't use the `Langgraph` helper yet

judgeval also ships `Langgraph.initialize()` in `judgeval.integrations.langgraph`. It only sets environment variables that turn on LangSmith's OpenTelemetry mode (`LANGSMITH_OTEL_ENABLED`, `LANGSMITH_TRACING`, and, by default, `LANGSMITH_OTEL_ONLY`).

In judgeval 1.3.3 those spans don't reach Judgment, even after `JudgmentTracerProvider.install_as_global_tracer_provider()`. LangSmith creates its OpenTelemetry spans on a background thread. Judgment's provider sends each span to the tracer activated in the current context, and a new thread has none, so the spans go to a no-op tracer. Use the instrumentor above.

## Trace work outside the graph

Code that runs outside LangChain (a direct HTTP call, a database write after the graph returns) needs its own span. Decorate it:

```python
from judgeval import Tracer

@Tracer.observe(span_type="tool")
def estimate_order(restaurant: str, headcount: int) -> float:
    return 612.0
```

If the decorated function calls the graph, the graph's spans nest under it.

## If something looks wrong

- No spans after `Langgraph.initialize()` is expected in judgeval 1.3.3. Switch to the instrumentor.
- If you see duplicate model spans, remove `wrap()` or any OpenAI instrumentor; LangChain already records the call.
- If spans land in the wrong project, check the project name in `Tracer.init`; it decides. See [Set up tracing](/documentation/connect/set-up-tracing).
