Why a recipe and not the whole spec
The OpenTelemetry GenAI Semantic Conventions are good. They're public, they're evolving, and any AI observability stack you'd pick today should be reading them natively. If you want the canonical reference, the OTel maintainers keep it at `opentelemetry.io/docs/specs/semconv/gen-ai/` and it's worth a read.
What we want to talk about here is something narrower: when you actually plug an OTel exporter into a LangChain agent, the callback surface is huge. Some of the callbacks produce spans you absolutely want. A few produce spans that look useful, fire a thousand times a second on a busy agent, and you regret later. Here's the recipe we use for our own agents and the design partners we work with.
The five spans you actually want
These five carry almost all of the debugging value.
1. `llm.start` → `llm.end` — the model call. Attribute the standard GenAI fields:
That's the canonical model call. It's the most-queried span in any agent observability tool. Get this one right and 80% of your downstream queries work.
2. `tool.start` → `tool.end` — the tool invocation. This is what LLM-only observability misses entirely. Attribute:
You will reference this span every time the agent drifts. "What did the tool return? Was it the empty string? Was it a 4xx? Did the model see the error?" The answers live here.
3. `chain.start` → `chain.end` — the runnable boundary. Every LCEL `.pipe()` is a chain. Every `AgentExecutor.invoke` is a chain. You want these because they're the structural skeleton of the trace — without them, the LLM and tool spans float in a flat sea.
Don't over-attribute the chain. Just the inputs hash and outputs hash, plus the runnable name. The detail lives on the children.
4. `agent.action` — the decision step. When the agent decides which tool to call, that's an action. Attribute the chosen tool name, the reasoning trace if available, and the call args. This is the most useful single span when you're trying to explain agent behaviour to a non-engineer — "the agent decided to call lookup_account_tier because the user asked about billing."
5. `agent.finish` — the terminal decision. When the agent decides to stop and return a result. The attribute that matters most: how many iterations did it take?
The three spans you can usually skip
`text.start` / `text.end`. The legacy completion API. If your LangChain agent is still using `OpenAI()` instead of `ChatOpenAI()`, the callbacks fire `text.start` instead of `llm.start`. Map both to the same canonical `gen_ai` attributes — don't emit two separate span types. Future you, querying for "all model calls," will thank present you for collapsing the two.
`retriever.start` / `retriever.end`. Useful if your agent does retrieval, but mostly noisy if it's a fast in-memory vector store. Sample these aggressively, or emit them only when the retrieval latency exceeds some threshold.
`llm.new_token`. Fires once per token in a streaming response. Spans are too expensive a primitive for individual tokens. If you want per-token data, emit it as events on the parent `llm.start` span, not as separate spans.
Sending the spans to Trefur
Whatever emits the spans, your own LangChain callbacks or an OpenTelemetry instrumentation library, point its OTLP/HTTP trace exporter at Trefur with a telemetry key from the app:
```bash
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://api.trefur.com/v1/traces
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer%20trf_obs_..."
```
Wiring the recipe yourself with raw OTel and LangChain callbacks is a couple of hundred lines, and worth writing once to understand what's happening.
What this gets you
Once these five spans are flowing, queries that previously required custom logging just work:
These are the queries you actually want to run when an agent misbehaves. They land at fraction-of-a-second latency on any OTel-native backend.
What's still missing from the spec
A few things the GenAI conventions don't (yet) name well:
If you're starting a new agent observability project, pick a backend that's OTel-native and watch the GenAI working group output. The portability is real and it compounds.
Try it
The [quickstart](/docs/quickstart) shows how to create a telemetry key and send your first OTLP trace.