Courses / Taking AI to Production

Session 6 of 9

Observability for AI Systems

Overview

Chapter 6 — Observability for AI Systems

Course: Taking AI to Production

Prerequisites: You can read and run a short Python script that uses lists, dictionaries and tuples and that follows an id from one record to another. You know that a model call reads input tokens and produces output tokens, that a token is a small piece of text, and that providers charge by token. You know that an agent can call tools and that checks can run on a model’s input and output. Every other term is defined in the notes before it is used.

What the reader can do after this chapter:

  • Describe a request as a trace made of spans, and say which fields each span of a model call should record: operation, model, input and output tokens, finish reason and error.
  • Compute the token totals and cost of a trace from per-span counts and a price per million tokens, and name the span that cost the most.
  • Walk a trace from a bad answer back through its spans to find a truncated output, an error or a suspect step, and decide which fields to record, drop or mask before a record is stored.

Core question this chapter answers: When a user reports one specific bad answer, what must you have recorded, request by request, to rebuild why it happened and what it cost?

Connections:

  • Builds on: guardrails and the release gate, whose blocks and decision logs only help if each request leaves a record that can be read afterwards.
  • Leads into: the security chapter in Building AI Agents, which treats the same records as an attack surface: what an attacker can reach through a tool, and what must never be written into a trace.

Your answers are scored

This chapter is assessed. The quiz is marked against a key held separately from your materials. Questions 1–8 are multiple choice and are marked by comparing your letter with the key. Questions 9–10 ask for a short answer in an exact format, and each one is marked by a rule printed with the question.

  • Nothing is judged by reading it for style. An answer either matches the rule or it does not.
  • All three project tasks are scored from an evidence file of key: value lines. Every accepted value, or the rule that decides it, follows from what the task prints.

Notes

Taking AI to Production — Chapter 6: Observability for AI Systems

Before you start

Nothing to install for the reading. For the project you need Python 3 and nothing beyond its standard library. You do not need an account with any model provider or an API key: every span, price and record the project uses is printed in the project file. The prices in this chapter are invented round numbers for practice; real prices come from your provider’s current price list, which you read yourself.

Keep one question in mind while you read: if a user sent me one screenshot of a wrong answer, could I find the exact request, every step it took and what it cost?

Summary

Observability means recording enough about each request that you can explain afterwards what the system did. For a model-backed system the unit of record is a trace: one request from start to finish, made of spans, each a timed operation such as a retrieval, a model call, a tool call or a guardrail check. A span for a model call should carry the operation, the model, the input and output token counts, the reason generation stopped and any error. Token counts multiplied by prices give cost per span and per request, which shows where money goes. To reconstruct a bad answer you rebuild the trace’s span tree from the parent links and read its steps in order. Message content is the sensitive part: standards and frameworks treat it as something to record deliberately, or not at all, and the usual choice is to log identifiers, counts and timings freely and content only where permitted and redacted.

Key concepts

Term Definition
Observability Recording enough about each request that you can explain afterwards what the system did and why.
Trace The record of one request, end to end, made up of spans.
Span One timed operation inside a trace, with a start, an end, an id and a link to its parent span.
Parent span The span that contains another. The first span of a trace has none and is the root.
Operation name What kind of work a span did, such as chat for a model call.
Input tokens / output tokens The counts of tokens a model call read and produced. Cost is computed from them.
Finish reason The recorded reason a model stopped generating, such as finishing normally or hitting a length limit.
Error type A short label for the kind of error a span ended with, such as timeout.
Price per million tokens The charge for one million input tokens or one million output tokens, set by the provider.
Cost of a span (input tokens × input price + output tokens × output price) ÷ 1,000,000.
Opt-in content Message text that is recorded only when deliberately switched on, because it may be sensitive.
Redaction Removing or masking sensitive fields before a record is stored.

Explanations

The failure, before the explanation

A support assistant answers a customer: “Your refund was approved for the full amount of” and stops. The customer sends a screenshot. The team opens the logs and finds one line per request: a timestamp, a user id and “200 OK”. The reply was cut off, but nothing says which model call produced it, whether a tool timed out earlier, how many tokens were used, or what the request cost. The bill is also higher than expected, and nothing in the logs says which step grew.

Nothing was broken in a way that raised an alarm. The system simply recorded the outcome of the request and none of what happened inside it.

Traces and spans

An example: one request to a support agent goes through five steps. It retrieves documents, calls a model, calls a tool to look up an order, calls the model again with the tool result, and runs a check on the final reply. The whole request is one trace. Each step is a span, with a start, an end and an id.

One agent framework’s documentation defines them this way: “Traces represent single end-to-end workflow operations composed of spans”, and spans “denote operations with temporal boundaries”, with fields including started_at, ended_at, trace_id and parent_id (OpenAI, Tracing, Agents SDK documentation, retrieved 2026-10-04). The same page lists the kinds of operation its SDK records as spans: agent runs, model generations, function tool calls, guardrail checks and handoffs.

A second way to picture it: a courier parcel. The parcel’s tracking number is the trace; each scan at a depot is a span, with a time and a place, and the scans link in order. When a parcel goes missing, you read the scans, you do not guess.

  • Applied use: give every request a trace id on arrival, and record every step as a span carrying that id and its parent’s id.
  • Practical consequence: the parent links turn a flat pile of log lines into a tree you can walk.
  • Selection: one line per request is a log; a tree of timed spans per request is a trace. Only the second can answer “which step?”.

What a model-call span should record

Take the model-call span from the support agent. A useful record of it holds the operation (chat), the model, the input and output token counts, the reason it stopped, and an error type if it failed.

The OpenTelemetry project, an open standard for telemetry, publishes naming and attribute conventions for generative AI spans. Its page carries the status Development, so check the current version before depending on an attribute name (OpenTelemetry, Semantic conventions for generative client AI spans, retrieved 2026-10-04). On that page:

  • the span name “SHOULD be {gen_ai.operation.name} {gen_ai.request.model}”, for example chat model-a;
  • gen_ai.operation.name is Required, with example values such as chat;
  • gen_ai.usage.input_tokens and gen_ai.usage.output_tokens are Recommended, “The number of tokens used in the GenAI input (prompt)” and “in the GenAI response (completion)”, and the input count “SHOULD include all types of input tokens, including cached tokens”;
  • gen_ai.response.finish_reasons is Recommended: an “Array of reasons the model stopped generating tokens”;
  • error.type is Conditionally Required if the operation ended in an error.

A tool call has its own span name: “SHOULD be execute_tool {gen_ai.tool.name}” (same page).

A second example, from a different field: a record saying only “model call, 1.8 seconds” cannot tell you whether the slowness was a long prompt or a long answer. With the token counts recorded, it can.

  • Applied use: record the same small set of fields on every model-call span, so spans can be compared.
  • Exam or real-world signal: a question about why a reply was cut off points to the finish reason; one about why a request was expensive points to the token counts; one about a failed step points to the error type and the span’s parent.
  • Practical consequence: consistent names let one query answer a question across every request, and following a published convention keeps your records readable by common tools.
  • Selection: use the published names where they exist. Do not invent a private name for a field a convention already covers; and treat a Development-status convention as likely to be revised.

Cost: tokens times price, per span

Suppose prices of 3.00 per million input tokens and 15.00 per million output tokens (invented for this unit). A model call that read 1,200 tokens and wrote 150 costs (1,200 × 3.00 + 150 × 15.00) ÷ 1,000,000 = (3,600 + 2,250) ÷ 1,000,000 = 0.00585. A second call that read 1,500 and wrote 400 costs (4,500 + 6,000) ÷ 1,000,000 = 0.0105. The request’s cost is the sum of its model-call spans.

A second way to see it: an electricity bill is units used times a rate; the meter reading is the token count and the tariff is the price. A bill that rises says nothing about which appliance is responsible until you have a reading for each one.

  • Applied use: compute the cost of every span and the total per trace, and keep the span-level numbers, so a rise can be traced to the step whose tokens grew.
  • Practical consequence: with the prices above, an output token costs five times an input token, so two calls that read the same input can differ in cost when their outputs differ in length.
  • Selection: do not compare cost per request across a change without comparing cost per span; a rise in one span can hide a fall in another.

Reconstructing a bad answer

An example. Following the cut-off reply: in this small trace every step is a direct child of the root, so listing the root’s children in start order shows every step. The second model call has finish reason length: generation stopped because it reached a length limit, so the sentence was cut mid-way. That span alone explains the cut. The tool call before it has error type timeout, a separate problem: the model answered without that tool’s data. Reading the whole tree finds both.

A second example, in the other direction: a reply that is complete but wrong. The finish reasons are all normal, and no span has an error. The suspect step is now the retrieval span: you read what it returned. (Whether it may be read depends on whether content was recorded, which the next section covers.)

A procedure that works for both:

  1. Find the trace by the request’s id.
  2. Collect every span with that trace id and arrange them as a tree by their parent ids, each span’s children in start-time order, so nested steps and side branches are included.
  3. For each model-call span, read the finish reason and the token counts.
  4. For each span, read the error type.
  5. Look at the step before the first span that looks wrong.

In this chapter’s data, length means the output reached a limit on its length. Finish-reason values vary between providers: OpenAI’s structured-outputs guide shows a finish_reason of length, while Anthropic’s names a stop_reason of max_tokens (OpenAI, Structured model outputs; Anthropic, Structured outputs). Read your provider’s list before relying on a value.

  • Practical consequence: the same procedure gives a decision-log entry its explanation and a human reviewer something to look at.

What not to record: content and personal data

Picture one support request’s trace with content switched on. The input message reads “I was charged twice on the card ending 4421; send the refund to my new address”, followed by the address. Everyone who can open the tracing dashboard can now read it. A flight recorder is the better model: it keeps instrument readings for every flight, not a transcript of what every passenger said.

Recording everything is tempting, and risky. The OpenTelemetry page marks input messages, output messages, system instructions and tool definitions as Opt-In, with the warning “This attribute is likely to contain sensitive information including user/PII data” for the message attributes. The agent framework’s documentation says its generation spans “store LLM inputs/outputs” and function spans “store function inputs/outputs”, which may contain sensitive data, and that this can be disabled with a configuration setting that defaults to True (OpenAI, Tracing, retrieved 2026-10-04).

So the decision belongs to you, and the safe default is to record identifiers, token counts, timings, finish reasons and error types for every request, and message content only where it is permitted, needed and redacted.

A mechanical rule makes the choice enforceable: before a record is stored, drop a named set of content fields and mask identifiers. For example, keep the first two characters of a user id and replace the rest with ***. The chapter’s rule and field names are illustrative.

  • Applied use: apply the redaction in code at the point where records are written, so a developer cannot forget it.
  • Practical consequence: a two-character prefix is a coarse mask: different users can share it, so it narrows a record to a group of users and does not reliably single one out, which is why it suits display and not grouping by person. If you must group one person’s requests, that needs a separate, deliberate design (an id that is replaced by another stable id, with the mapping protected). A dropped content field means you cannot reread the text, which is the trade you made.
  • Selection: if you need content to debug, switch it on narrowly and for a stated reason, and treat the stored content as sensitive data.

Three situations and what they mean for your design

  1. The cost per request rose after a prompt change; the dashboard shows only a total.
  2. A complaint arrives with a request id; the logs have timestamps and statuses but no span ids.
  3. A developer proposes logging the full prompt and reply for every request “to be safe”.
What you see What it means What to do instead
A total that rose (1) The total cannot say which step grew Record tokens per span and compute cost per span; compare before and after
No span structure (2) You can see that something happened, not what Add a trace id, span ids and parent ids to every step
Log everything (3) Content may include personal or sensitive data Log identifiers and counts by default; opt in to content narrowly; redact in code

Flashcards

Say the answer aloud before revealing it. Speaking it is what exposes the gaps that reading past a written answer hides.

What is a trace, and what is a span?

A trace is one request end to end. A span is one timed operation inside it, with an id and a link to its parent span.

What should a model-call span record?

The operation, the model, input and output token counts, the finish reason and, if it failed, the error type.

What is the span-name convention for a model call, and for a tool call?

A model call is named {operation} {model}, for example chat model-a; a tool call is execute_tool {tool name} (OpenTelemetry conventions, status Development).

How do you compute the cost of one span?

(input tokens × input price + output tokens × output price) ÷ 1,000,000, with prices per million tokens. A request’s cost is the sum of its spans.

Why keep cost per span and not only per request?

A rise in the total does not say which step grew. Per-span numbers point to the step.

A reply is cut off mid-sentence. Which field do you look at first?

The finish reason of the model-call span that produced it, to see whether generation stopped at a length limit; then the earlier spans for errors.

How do you find the cause of a complete but wrong answer?

Walk the trace for the steps that fed the answer, such as retrieval, and read what they returned, if content was recorded. Normal finish reasons and no errors move suspicion to those inputs.

Which fields does the OpenTelemetry page mark as opt-in, and why?

The message content attributes (input messages, output messages, system instructions, tool definitions). The page warns the message attributes are likely to contain sensitive information including personal data.

What is a safe default for what to log?

Identifiers, token counts, timings, finish reasons and error types for every request, and content only where permitted, needed and redacted.

Why do the parent links matter?

They turn a pile of log lines into a tree, so you can walk from the final answer back through every step that produced it.

Model answer

The question: “A customer says your assistant gave a wrong answer earlier. What do you need in place to find out why, and what it cost?” A strong answer hits five beats, in this order:

  1. Trace — every request has a trace id and its steps are spans with parent links.
  2. Fields — each model-call span records operation, model, token counts, finish reason and error.
  3. Cost — token counts times prices, kept per span and summed per trace.
  4. Walk — find the trace, rebuild its span tree, read finish reasons and errors.
  5. Privacy — identifiers and counts by default, content opt-in and redacted.

“I’d have a trace id on every request, with each step, retrieval, model call, tool call, guardrail, as a span linked to its parent. Each model-call span records the operation, model, input and output tokens, finish reason and any error, following the OpenTelemetry naming where it exists. Cost is tokens times price per span, summed per trace, so a rise points at a step. To investigate, I find the trace from the request id, rebuild its span tree from the parent links and read the finish reasons and errors: a length stop explains a cut-off answer and a timed-out tool explains missing data; normal stops point me to what retrieval returned. For privacy I log ids, counts and timings always and message content only when it’s permitted and redacted.”

Missing a beat is a failure state. Without a trace the steps are not connected. Without fields there is nothing to read in each step. Without cost the bill cannot be explained. Without the walk the records are never used. Without privacy the logs become a store of sensitive text.

Why this matters

  • Support and incident review. One request id leads to the whole story of that request.
  • Cost control. Per-span cost shows which prompt or step grew, instead of a single rising total.
  • Quality work. The finish reasons and errors you record feed an evaluation suite.
  • Compliance. Decision logs and review paths depend on traces; redaction depends on deciding up front what is stored.

After this chapter

The project for this chapter is Read a Trace (project.md). You will total the tokens and cost of one trace and find its most expensive span, find the truncated span, the failed span and the path from a span to the root, and apply a redaction rule to four log records.

The security chapter in Building AI Agents treats those records as part of an attack surface: what an attacker can reach through a tool, how secrets must be handled, and why a trace must never contain one.

Project

Hands-on Project — Read a Trace

Objective

Total the tokens and cost of one trace and find its most expensive span, find the truncated span, the failed span and the path from a nested span to the root, and apply a redaction rule to log records, so that you hold the small tools you would use to answer “what happened in this request, and what did it cost?”.

What this project needs: Python 3 with its standard library only. No task needs an account with a model provider, an API key, an internet connection or any payment. Every span, price and record the tasks use is printed below, and the prices are invented for practice. All three tasks are required, and each can be done on its own, in any order.

Your evidence file. Keep one plain-text file, chapter6-evidence.txt. Each task tells you which lines to add. Every line has the form key: value, one per line, with the key in lower case exactly as printed. Write what your code printed, not what you expected it to print.

The trace used in Tasks 1 and 2

One request produced these spans. Each row is (span id, parent id, name, kind, input tokens, output tokens, finish reason, error type). A None means the span has no such value.

SPANS = [
    ("sp1", None,  "handle_request",           "workflow",  None, None, None,         None),
    ("sp2", "sp1", "retrieve_docs",            "retrieval", None, None, None,         None),
    ("sp3", "sp1", "chat model-a",             "chat",      1200, 150,  "tool_calls", None),
    ("sp4", "sp1", "execute_tool lookup_order","tool",      None, None, None,         "timeout"),
    ("sp5", "sp1", "chat model-a",             "chat",      1500, 400,  "length",     None),
    ("sp6", "sp1", "summariser",               "agent",     None, None, None,         None),
    ("sp7", "sp6", "chat model-b",             "chat",      300,  60,   "stop",       None),
    ("sp8", "sp1", "guardrail output",         "guardrail", None, None, None,         None),
]

The prices are 3.00 per million input tokens and 15.00 per million output tokens. A span with None tokens counts as 0 tokens and costs 0.

Task 1 — Total a trace

  1. Add up the input tokens and the output tokens of all spans.
  2. Compute the total cost: (total input × 3.00 + total output × 15.00) ÷ 1,000,000. Print it to 6 decimal places.
  3. Compute each span’s cost the same way, and find the span with the highest cost.

Add to your evidence file:

t1_input_tokens: <total input tokens>
t1_output_tokens: <total output tokens>
t1_total_cost: <6 decimal places>
t1_costliest_span: <span id>

Output: a short script, and four evidence lines.

Task 2 — Walk the trace

Using the same SPANS:

  1. List the span ids whose finish reason is length.
  2. List the span ids that have an error type.
  3. Follow sp7 through its parent ids up to the root (the span whose parent is None). Write the span ids from sp7 to the root joined by >.
  4. Count the direct children of the root: the spans whose parent id is sp1.

Add to your evidence file:

t2_truncated_ids: <ids, span-id order, comma-separated, no spaces>
t2_error_ids: <ids, span-id order, comma-separated, no spaces>
t2_path_to_root: <ids joined by >, starting at sp7, no spaces>
t2_root_children: <number of direct children of the root>

Output: a short script, and four evidence lines.

Task 3 — Redact records

Four log records are about to be stored. Each is a dictionary:

RECORDS = [
    {"trace_id": "t-1", "user_id": "ab12345", "input_messages": "x", "output_messages": "y",
     "model": "model-a", "in_tokens": 10, "out_tokens": 5},
    {"trace_id": "t-2", "user_id": "zk99", "system_instructions": "s", "input_messages": "x",
     "output_messages": "y", "model": "model-a", "in_tokens": 20, "out_tokens": 8},
    {"trace_id": "t-3", "model": "model-b", "in_tokens": 5, "out_tokens": 2},
    {"trace_id": "t-4", "user_id": "q", "input_messages": "x", "model": "model-b",
     "in_tokens": 7, "out_tokens": 1},
]

Write safe(record) returning a new dictionary after applying two rules:

  1. Remove the keys input_messages, output_messages and system_instructions if they are present.
  2. If the record has a user_id, replace its value with the first two characters of the value followed by *** (a one-character value keeps that character).

Apply it to the four records.

Add to your evidence file:

t3_keys_kept: <total number of keys across the four safe records>
t3_masked_ids: <the masked user ids of the records that have one, in record order, comma-separated, no spaces>

Output: a safe function, and two evidence lines.

What carries forward

Keep chapter6-evidence.txt and your safe function. The security chapter in Building AI Agents asks what an attacker can reach through the same tools and traces: which secrets must never appear in a record, and how to limit what an agent can do when something goes wrong.

TechEazy Consulting training material — not affiliated with or endorsed by Amazon Web Services (AWS), OpenAI, Anthropic or any other company named in this course.