Skip to content

All lessons

Area 04 // EvalsLesson 74 / 14015 min read

Tracing and OpenTelemetry for LLMs

Tracing records every step of a request (retrieval, each model call with its prompt, tokens and latency, each tool call) as a tree of timed spans, and OpenTelemetry is the open standard for producing and shipping those traces, with shared GenAI attribute names so any tool can read them.

Think of it like this

Analogy

A parcel tracking page shows every step of a delivery: picked up 09:12, sorted at the hub 11:40, out for delivery 14:05, delivered 15:31. If a parcel is late, you can see exactly where it got stuck.

A trace is that tracking page for one AI request. Each step (search, model call, tool call) is a span with its time, details and result.

The problem it solves

A user says "the assistant gave me a wrong answer yesterday". Without tracing, you have the question and maybe the answer, but not what was retrieved, which prompt version ran, what the tool returned, how many tokens it used or which step was slow. Debugging becomes guesswork, evals can't use real failures, and cost or latency problems are invisible. Agents with dozens of steps make this far worse.

How it works: tracing

  1. A trace represents one request (or one agent task) end to end.
  2. It contains spans: timed steps with a name, start and end, a parent (so they form a tree), a status (ok or error) and attributes (key-value details).
  3. For LLM apps, record per span: inputs and outputs (prompts, completions, retrieved chunk IDs), model name, parameters, token counts, cost, latency, tool names and arguments, errors, and identifiers (user, session, prompt version, app version).
  4. Instrument your code: wrap each step in a span, manually or automatically with libraries that instrument LLM SDKs and frameworks.
  5. Export spans to a backend (Langfuse, LangSmith, Arize Phoenix, Datadog, Grafana Tempo, Honeycomb…), where you search, filter, view trees and build dashboards.
  6. Use traces everywhere: debugging, error analysis, agent evals, cost and latency metrics, and adding real failures to golden sets.
  7. Protect data: redact secrets and personal information, sample if volume is high, and control retention.

How it works: OpenTelemetry for LLMs

  1. OpenTelemetry (OTel) is the open, vendor-neutral standard (API, SDKs, and the OTLP wire protocol) for traces, metrics and logs, used across the software industry.
  2. Instrument once, export anywhere: a tracer creates spans; an exporter sends them (via OTLP) to any compatible backend, so switching tools doesn't mean re-instrumenting.
  3. GenAI semantic conventions define standard attribute names for model calls, such as gen_ai.operation.name (e.g. chat), gen_ai.provider.name, gen_ai.request.model, gen_ai.response.model, gen_ai.usage.input_tokens and gen_ai.usage.output_tokens. As of November 2026 they live in their own OpenTelemetry repository and are still marked "Development" (they may change).
  4. Auto-instrumentation packages (OpenTelemetry contrib and others like OpenLLMetry and OpenInference) wrap popular LLM SDKs and frameworks to emit these spans automatically.
  5. Context propagation links spans across services (a trace ID passed in HTTP headers), so a request through your API, a worker queue and a model call appears as one trace.
  6. Many LLM observability tools accept OTel directly (Langfuse's v5 SDK is built on OpenTelemetry), so the same spans power both general and AI-specific dashboards.
One trace, a tree of spans, exported over OTLP to any backend.

See it in code

Install
pip install opentelemetry-sdk
Needs
Python 3.10+. Runs offline: spans go to an in-memory exporter instead of a backend.

A request traced with OpenTelemetry: a root span, a retrieval span and a model-call span using GenAI attribute names.

otel_trace.py
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
 
exporter = InMemorySpanExporter()                    # in production: an OTLP exporter to your backend
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
tracer = provider.get_tracer("rag-app")
 
def retrieve(q):
    with tracer.start_as_current_span("retrieve") as span:
        span.set_attribute("retrieval.top_k", 3)
        return ["chunk-1", "chunk-2", "chunk-3"]
 
def call_llm(prompt):
    # GenAI semantic conventions: standard attribute names any backend understands.
    with tracer.start_as_current_span("chat claude-haiku-5-5") as span:
        span.set_attribute("gen_ai.operation.name", "chat")
        span.set_attribute("gen_ai.provider.name", "anthropic")
        span.set_attribute("gen_ai.request.model", "claude-haiku-5-5")
        span.set_attribute("gen_ai.usage.input_tokens", 812)
        span.set_attribute("gen_ai.usage.output_tokens", 64)
        return "You get 25 days."
 
with tracer.start_as_current_span("POST /ask") as root:   # one trace = the whole request
    root.set_attribute("user.id", "u42")
    call_llm(retrieve("leave days?"))
 
for s in sorted(exporter.get_finished_spans(), key=lambda s: (s.parent is not None, s.start_time)):
    parent = s.parent.span_id if s.parent else None
    depth = 0 if parent is None else 1
    attrs = {k: v for k, v in s.attributes.items() if k.startswith(("gen_ai.usage", "retrieval", "user"))}
    print("  " * depth + f"{s.name:24} {attrs}")
Output
POST /ask                {'user.id': 'u42'}
  retrieve                 {'retrieval.top_k': 3}
  chat claude-haiku-5-5    {'gen_ai.usage.input_tokens': 812, 'gen_ai.usage.output_tokens': 64}

To send real traces, replace InMemorySpanExporter with OTLPSpanExporter (from opentelemetry-exporter-otlp) pointed at your backend. In real code the token counts come from the API response's usage.

A real example

Example

Finding a 9-second answer in a 2-second app

An assistant's p95 latency jumped from 2 s to 9 s. Traces showed the root span taking 9 s, and inside it a "rewrite query" span of 6.5 s: a recent change had routed query rewriting to a large reasoning model at high effort. Switching that step back to a small model fixed it the same day.

The same traces revealed that 4% of requests retried the main model call after timeouts, which explained a cost increase nobody had noticed.

The easy way

Start here

Use an LLM observability tool's SDK (e.g. Langfuse) or decorator on your main request function and your model calls. Within an hour you'll see every request as a tree with prompts, outputs, tokens and timings.

When you grow: the production setup

Teams instrument with OpenTelemetry (manual spans plus auto-instrumentation for LLM SDKs), propagate context across services and queues, follow GenAI semantic conventions, export via OTLP to both AI-specific and general observability backends, redact sensitive data in processors, sample at high volume, and link traces to evals, feedback and cost dashboards.

Trade-offs

What you gain

  • Makes every request debuggable: what was asked, retrieved, called and answered.
  • Feeds error analysis, evals, cost and latency metrics.
  • OpenTelemetry avoids vendor lock-in.

What it costs you

  • Traces hold sensitive content; privacy and retention need care.
  • Storage and backend costs grow with traffic.
  • GenAI conventions are still evolving.

When not to use it

  • Never skip tracing in production LLM apps; sample or redact instead of turning it off.

Common mistakes

  • Logging only the final answer, not the retrieved context and tool calls.
  • Sending secrets or personal data to a third-party tracing service.
  • Spans without user, session or version identifiers, so you can't filter.
  • Inventing custom attribute names instead of the standard ones.
  • Traces that break across services because context isn't propagated.

Interview questions

Try answering out loud first, then open the model answer.

What would you trace in an LLM application?

Model answer

A root span per request or task with user, session, app and prompt versions; child spans for each retrieval (query, chunk IDs, scores), each model call (model, parameters, input and output, token usage, cost, latency, stop reason), each tool call (name, arguments, result, errors) and guardrail checks. Plus errors and retries. Redact sensitive data and link traces to feedback and eval scores.

Why use OpenTelemetry for LLM observability?

Model answer

It's a vendor-neutral standard: instrument once and export via OTLP to any backend, combine LLM spans with existing service traces, metrics and logs, propagate trace context across services, and use auto-instrumentation libraries. GenAI semantic conventions give standard attribute names so dashboards and tools interpret model calls consistently.

How do traces help with evaluation?

Model answer

They supply the full context for error analysis, provide the data for agent tool-call and trajectory evals, let you sample production requests for online judging, attach user feedback to exact inputs and outputs, and turn real failures into golden dataset cases. They also provide latency and cost per step for performance evals.

Quiz

Pick an answer to see whether it’s right and why.

  1. 01What is a span?
  2. 02Why is tracing especially important for LLM apps?
  3. 03What is OpenTelemetry?
  4. 04Why do GenAI semantic conventions matter?
  5. 05What should you do before sending prompts and outputs to a tracing backend?

0 of 5 answered

Key terms

Tracing
Recording every step of a request (model calls, tool calls, retrieval) with timing, inputs and outputs, so you can debug and measure it.
Span
One timed step inside a trace (such as a retrieval or a model call), with a parent, a status and attributes.
Trace
The full record of one request or task, made of nested timed spans for every step it took.
OpenTelemetry (OTel)
An open, vendor-neutral standard and toolkit for producing and exporting traces, metrics and logs.
GenAI semantic conventions
OpenTelemetry's standard attribute names for model calls, such as gen_ai.request.model and gen_ai.usage.input_tokens.

Before and after

Progress is saved in this browser only.