Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

attribute
A named value attached to a span or trace, such as a model name or token count, that you can filter and group by.
cardinality
The number of distinct values a field can take. User ids make a field expensive to store and chart; a status code does not.
cost
The money a request spends, usually worked out from token counts and the price per token.
error taxonomy
A short, named list of failure classes, each with an owner and a rule for whether it can be retried.
feedback label
A defined judgement attached to a request after the fact, such as helpful, wrong or unsafe, with a rubric two people can apply alike.
latency
The time a request spends waiting and working, reported for each stage and for the whole.
p95
The cutoff that 95 out of 100 requests finish under, so a few very slow calls stay visible.
redaction
Removing or masking sensitive values before a record is stored, while keeping enough context to debug.
retention
How long stored records are kept before deletion, set by policy rather than by disk space.
sampling
Keeping only a chosen share of records, ideally with every failure kept and the totals corrected.
self time
The part of a span's duration spent in that step alone, after subtracting the time its children took.
span
One timed step inside a request, with a start, an end and the values it carried.
trace
One request's whole journey, with every step nested under the step that caused it.
trace id
A value shared by every record of one request, so pieces written by different services can be joined.

Observability for AI systems

What to record

A request that leaves no record cannot be debugged, priced or improved. The unit is one trace carrying the inputs, decisions, outputs and cost of a single request.

Part 1 of 10

Intermediate15 min readBack to Courses

A request is a story, not a line

Most logging records that something happened. An AI request needs the story: what the user asked, which prompt version ran, what was retrieved, which tools were called, what came back, and how long each step took. Without that, a bad answer looks the same whether the cause was retrieval, a stale prompt or the model itself. The difference matters because each cause has a different fix and a different owner. The record is the difference between an anecdote and an incident report.

Write the record at the boundary where you can see the whole request. The model provider sees only its own call; your application is the only place that knows the question, the context and what the user did with the answer. That is why you instrument your code rather than relying on the dashboard the provider gives you. A provider dashboard is useful for its own latency and errors, and blind to everything before and after. Instrument the request handler, not the model client, so the rest of the tree has somewhere to hang.

Six fields that earn their keep

At minimum, record a request id, the prompt version, the model and its parameters, token counts for input and output, latency split by stage, and an outcome. The outcome is not only success or failure: it is whether the answer was accepted, corrected, retried or abandoned. Those six fields answer the first questions in any incident without a second query, and anything else is a bonus that should earn its storage. If a field cannot change a decision, it is telemetry for its own sake.

Add the ids of retrieved documents, not their full text. Ids let you reproduce the context later from a store that has its own retention rules, and they keep the trace small enough to search. Store the rank and score too, because they explain why a chunk was there. A trace carrying a megabyte of pasted context is a trace nobody can query, and a second copy of your corpus to protect. A document that has been re-indexed since the request is a different document, so the id should carry a version.

The fields, as span attributes

from opentelemetry import trace

tracer = trace.get_tracer("support-bot")

with tracer.start_as_current_span("chat") as span:
    span.set_attribute("app.prompt_version", "support-v7")
    span.set_attribute("gen_ai.request.model", "gpt-4.1-mini")
    span.set_attribute("gen_ai.usage.input_tokens", 812)
    span.set_attribute("gen_ai.usage.output_tokens", 96)
    span.set_attribute("app.outcome", "accepted")

One request, one record

Run the logger and read the record it writes. Then add a feedback field and a second request, and confirm both records share the same shape.

Output
Click "Run" to execute your code

Record decisions, not just text

The output is the least interesting part of the record. The decisions around it explain it: which prompt version, which model, which retrieved chunks, which tool result, which guardrail fired. Two runs can produce the same sentence for different reasons, and the fix depends on which reason it was. A trace that stores only the sentence cannot tell them apart. It is a transcript, not an explanation. The same output under a different prompt version is a different experiment.

This is what makes a trace reviewable by someone who was not on call. A reviewer can see that the model followed the context correctly and the retriever returned an old policy page, which is a different ticket from a prompt that ignored the context. The record turns an argument about quality into a look at a cause. That is also how a new engineer learns what the system does, rather than what the diagram says.

Log the prompt version on every request, even while you are still editing prompts by hand. A trace without a version tells you the answer changed and not what changed.

Keep the record cheap and safe

Observability has a cost and a privacy surface. Cap field sizes, hash identifiers, and redact before the record leaves the process. Prompts and completions can contain personal data, so a trace store is a second copy of your users’ text and needs the same access rules as the primary store. A field you would not put in a support ticket does not belong in a trace. Redaction at the edge is cheaper than a cleanup after the export.

Retention should be deliberate: full prompts for days, aggregates for months. Most debugging happens in the first week, while cost and quality trends need the long window. Keeping everything forever is how a trace store becomes a liability, and how a routine query becomes an incident. Set the clock before the first busy day, not after. Deleting on a schedule is cheaper than defending a growing archive. A retention policy is a product decision, not a storage setting, and it should be reviewed like one.

A schema that rejects thin records

Run the validator over three records. Then add a record with an empty answer and confirm it is reported rather than stored.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is the main reason to record the whole request rather than only the model call?