LLM Observability: Tracing Agents with OpenTelemetry GenAI

ObservabilityOpenTelemetryLLMAgents

An LLM agent that misbehaves in production is a special kind of debugging pain. The answer was wrong, but why? Was it bad retrieval, a tool that returned garbage, a prompt that got truncated, or the model just fumbling? Without tracing, you're reading logs and guessing. With proper observability, you open one trace and see the entire decision path — every model call, every tool result, every token, and where the time and money went.

OpenTelemetry's GenAI semantic conventions make this vendor-neutral. Instead of locking into one LLM-monitoring SaaS, you instrument with standard OTel spans and attributes and send them anywhere — Jaeger, Grafana Tempo, Datadog, Langfuse, Phoenix. The same discipline that made microservices debuggable now applies to agents, and it's the difference between "the AI is flaky" and "retrieval returned stale chunks for queries matching this pattern."

Traces, spans, and why agents map onto them cleanly

A trace represents one end-to-end request; spans are the nested operations inside it. Agents map onto this model naturally because an agent run is a tree of operations:

Trace: handle_support_query                        [1,840ms, $0.021]
├─ span: agent.run                                 [1,835ms]
│  ├─ span: retrieval.search        (vector db)    [ 120ms]
│  ├─ span: gen_ai.chat  gpt-4o     (plan)         [ 410ms, 1.2k tok]
│  ├─ span: tool.get_order_status                  [  90ms]
│  ├─ span: gen_ai.chat  gpt-4o     (answer)       [ 980ms, 2.4k tok]
│  └─ span: guardrail.output_moderation            [  35ms]

Read top to bottom and the agent's reasoning is laid bare: it retrieved, planned, called a tool, composed an answer, and checked it. When something's wrong, the offending span is right there with its inputs and outputs.

The GenAI semantic conventions

The value of the OTel GenAI conventions is agreement on names, so tooling can compute cost, token usage, and error rates without custom parsing. The core span for a model call uses operation names like gen_ai.chat and attributes such as:

Attribute Example Purpose
gen_ai.system openai provider
gen_ai.request.model gpt-4o requested model
gen_ai.response.model gpt-4o-2024-08-06 model that answered
gen_ai.usage.input_tokens 1180 prompt tokens
gen_ai.usage.output_tokens 240 completion tokens
gen_ai.request.temperature 0.2 sampling params

Token counts on every span are what let you build a cost view for free — sum them, multiply by per-model rates, and you've got per-request and per-feature cost attribution, which feeds directly into cutting LLM costs.

Instrumenting in practice

The fastest path is auto-instrumentation. OpenLLMetry (from Traceloop) and OpenInference (from Arize Phoenix) both patch popular SDKs to emit GenAI-convention spans with almost no code:

from traceloop.sdk import Traceloop

Traceloop.init(app_name="support-agent")  # auto-instruments OpenAI, Anthropic, etc.
# your existing LLM calls now emit spans automatically

For the parts auto-instrumentation doesn't cover — your own agent loop, retrieval, tools — add manual spans so the tree is complete:

from opentelemetry import trace
tracer = trace.get_tracer("support-agent")

with tracer.start_as_current_span("tool.get_order_status") as span:
    span.set_attribute("tool.name", "get_order_status")
    span.set_attribute("order.id", order_id)
    result = get_order_status(order_id)
    span.set_attribute("tool.result_size", len(result))

Wrap tools and retrieval like this and every step lands in the same trace, correlated by trace ID with the rest of your backend. That last point is underrated: because it's plain OpenTelemetry, an LLM span sits in the same trace as the HTTP request, database query, and cache lookup around it. You see the whole request, not just the AI slice — the same observability and SLO discipline you'd apply to any service.

What to actually watch

Instrumentation is the means; here's what the data is for:

The privacy and cost decision on content

The one genuinely hard call is whether to log prompt and completion content. It's the single most useful thing for debugging — you can't diagnose a bad answer you can't see — but it carries real weight:

A workable default: record metadata (model, tokens, latency, cost) on 100% of requests, and content on a sampled subset plus every failure. You keep debuggability where you need it without logging every user's data forever.

Set this up early — retrofitting tracing onto an agent already misbehaving in production is miserable, while having it from day one turns "the AI is flaky" into a trace you can actually read. Standard conventions mean you're never locked into one vendor's dashboard, and the traces live right alongside the rest of your system's telemetry.

Resources

Frequently asked questions

What is OpenTelemetry GenAI?

It's a set of OpenTelemetry semantic conventions that standardize how you trace generative AI operations — naming spans, attributes for model, token counts, cost, and tool calls. Using them means your LLM traces work in any OTel-compatible backend instead of a proprietary format.

Why do LLM agents need distributed tracing?

A single user request can trigger a dozen model calls, retrievals, and tool executions. Without a trace tying them together, debugging a bad answer means guessing. A trace shows the exact sequence, inputs, outputs, latency, and cost of every step, so failures become visible instead of mysterious.

What should I record on an LLM span?

The model name, request parameters, prompt and completion token counts, latency, and cost, plus tool-call details and errors. Be deliberate about logging prompt and response content — it's invaluable for debugging but carries privacy and cost implications, so sample or redact.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →