learn-ai-observability-with-phoebe / Builder session 5 of 10
Learn AI Observability with Phoebe · Builder track · Session 5 of 10

Reading a broken trace: the five questions, and the span nobody recorded

This is the centre of the course. One run, one bad answer, eight rungs of instrumentation, and a simulator that recomputes the arithmetic in front of you every time you change what the system reports about itself. You will watch a diagnosis go from impossible to provable without anybody touching the application code, and then watch the most tempting lever of all add ten times the storage and not one new answer.

🟠 Builder track AI engineers · platform engineers · technical PMs 45 min
0-3 · Welcome 3-14 · The five questions 14-40 · Three simulators 40-45 · Q&A
Part 0

A diagnosis is not a feeling, it is five answers

Tuesday 09:14. Beacon's Insight Bot is sending executives stale revenue numbers, intermittently. No error is raised, no alert fires, the p95 latency dashboard is green, and the answers read as confidently as ever. An engineer opens the trace at 09:20 and closes it at 09:21, because there is genuinely nothing in it to see. Today you find the bug, and the thing you change to find it is not the application - it is what the application reports about itself.

Live - presented in session Self-study - read after class Official sources covered
★ What you walk out with today The five questions that constitute a diagnosis and the exact coverage each one requires, the experience of watching visible wall clock jump from 32.4% to 100% while the diagnosis moves from zero answers to one, the mechanism by which a swallowed exception holds a dashboard green through an entire incident, and a first-hand demonstration that full content capture multiplies your retention by 10.5x and answers nothing.
The honesty rail, stated plainly and repeated on screen The incident and its spans are constructed for this exercise. They use real OpenTelemetry GenAI attribute keys and a realistic failure, but nothing here was captured from a live system, and the bot is a teaching model rather than a product. What is real is the arithmetic: the unaccounted-time percentage, the cost per run and the retention volumes are computed live in your browser from the span table and your lever settings, with nothing hard-coded per step. Two of the keys on that trace, gen_ai.retrieval.cache_fallback and cache.entry_age_hours, are application-defined rather than conventions, because the registry has no name for either - which is itself worth noticing.
Part 1 · the five questions

What you have to be able to answer 6 min live

"The bot gave a wrong number" is a report, not a diagnosis. A diagnosis is five specific answers, and the useful thing about writing them down is that each one has a different coverage prerequisite. You cannot buy them in bulk and you cannot get them from the same lever.

A diagnosis is five answers, and no two of them come from the same lever The question What must be instrumented Answerable at Which step is actually slow? retrieval spans exist at all rung 3 · 1 of 5 Did retrieval return anything? + gen_ai.retrieval.documents.count rung 4 · 2 of 5 Where did the stale number come from? + db.query and cache.get children rung 5 · 3 of 5 Why did nothing alert? + span status and error.type rung 6 · 4 of 5 Is it intermittent or constant? + gen_ai.conversation.id rung 7 · 5 of 5 Rungs 1 and 2 answer none of them, and rung 8 answers no more than rung 7. Five of the eight levers are load-bearing. The other three buy something else, or nothing. Write the five questions down first. Then every coverage gap arrives as a ticket, not a mood.
🔍 Click to zoom - the five questions and their coverage prerequisites, in the order the simulator unlocks them

The order matters more than it looks. The first question - which step is slow - is the one every team instruments for, because latency is what dashboards are made of. It is also the least useful of the five here: the retrieval step in this incident is 4.2 seconds of a 6.8 second run, and knowing that tells you where to look and nothing about what is wrong. Slow was never the bug. Slow was the symptom that made the bug findable.

★ The 90-second read order, for any trace, any system

1. Does the wall clock add up? root duration vs the union of the children 2. What is the widest span? and is it a model call or something else 3. What did that step return? counts, row counts, document counts, ids 4. Did anything fail quietly? status, error.type, recorded exceptions 5. Is this run unusual? thread by conversation id and compare If step 1 fails, stop. Everything after it is a guess about a system you are not currently observing.

Part 2 · the chain

Five links, and the last one is silent 5 min live

Here is the whole incident, resolved, so you can watch it appear rather than guess at it. Read the right-hand column as a shopping list: each link in the chain has exactly one piece of telemetry that would have named it, and the team had none of them.

Five links in one chain, and the span that would have named each WHAT HAPPENED THE TELEMETRY THAT SAYS SO The warehouse query times out after 4.1 seconds and comes back with nothing. db.query warehouse.metrics_daily db.response.returned_rows = 0 A try/except catches the timeout, and nobody sets the span status on the failure path. error.type = QueryTimeout span status ERROR + record_exception The fallback path quietly serves a cache entry that is 19.4 hours old. cache.get metrics_snapshot cache.entry_age_hours = 19.4 Retrieval therefore returns no documents at all, and reports success. gen_ai.retrieval.documents.count = 0 gen_ai.retrieval.cache_fallback = true The model summarises the stale data faithfully. Status OK. Dashboard green. CFO informed. nothing No span fails here. Nothing failed here. The model did its job perfectly. That is why capturing what it said explains nothing.
🔍 Click to zoom - the causal chain, and the one link no telemetry can flag
The last row is the whole reason the anti-lever exists Four of the five links have a telemetry signal. The fifth does not, because at that point in the chain the system is behaving correctly: given stale inputs, the model produced an accurate summary of stale inputs. Everything you could capture about the model - the prompt, the completion, the finish reason, the token counts - looks completely healthy. Hold onto that when you reach rung 8 of the ladder and are tempted to store everything.
Demo 1 of 3

The climb: eight rungs from blind to provable ★ 12 min · watch, then argue

Step through these in order and read the caption before you look at the meters. The application code never changes across these eight rungs. Only what it reports about itself changes, which is the entire claim of this course made falsifiable in a browser.

Rung 1 is not a strawman - it is what most teams have in production right now Three spans: the root plus the two model calls, because the SDK gave you those for free. Everything is fast, everything is status OK. 32.4% of the wall clock is visible, which means 4.60 seconds of a 6.80 second run is inside no span you emit, and the diagnosis stands at 0 of 5. Nothing on that screen looks wrong. That is the point of it. An engineer looking at this trace during the incident was not careless, and would not have found the bug by looking harder.

Rung 2 adds tool spans and takes visible wall clock from 32.4% to 38.2%. The diagnosis stays at 0 of 5. Sit with that for a moment, because it is the most expensive lesson on the page: tools are the easy thing to instrument, they are what a framework integration hands you first, and in this incident they were never where the time went. A team can spend a sprint on tool instrumentation, feel productive, ship a nicer waterfall, and be exactly as blind as they started.

Rung 3 adds the retriever span and the number moves to 100%. The 4.2 second step appears out of nowhere and the unaccounted-time warning disappears entirely. The diagnosis goes to 1 of 5. This is worth naming precisely: you now have complete coverage of time and 20% of a diagnosis. Accounting for every millisecond tells you where to look. It does not tell you what you are looking at.

Rung 4 adds the retrieval attributes and the sentence changes shape. gen_ai.retrieval.documents.count is zero and gen_ai.retrieval.cache_fallback is true, so the step is not slow, it is empty - and something quietly substituted cached data for the answer. 2 of 5. Rung 5 goes one level deeper into the database and cache children and locates the root cause physically: the query returned zero rows after 4.1 seconds, and a cache entry 19.4 hours old was served in its place. That is the stale number the CFO read at 09:14, now pinned to an exact span. 3 of 5.

Rung 6 is the one that explains the silence. The query did not return empty, it timed out - and someone caught the exception, fell back to cache and returned success. That swallowed exception is why nothing alerted and why the dashboard stayed green through the entire incident. 4 of 5. Rung 7 threads conversations and adds the token attributes: the failure is intermittent rather than constant, which is a different fix, and the run's cost becomes knowable at $0.0180 using the per-span token work from session b4. 5 of 5. You can write the postmortem from the trace.

★ Rung 6 in four lines - the whole difference between green and red

try: rows = warehouse.query(sql, timeout=4.0) except QueryTimeout as e: span.set_status(Status(StatusCode.ERROR)) # <- the missing line span.record_exception(e) # <- the other missing line span.set_attribute("gen_ai.retrieval.cache_fallback", True) rows = cache.get("metrics_snapshot") # served 19.4 hours stale

Rung 8 is the trap, and it is the lever most teams pull first Full content capture: every prompt, every completion, stored. The diagnosis score does not move. It was already 5 of 5, and the message content was never the problem, because the model summarised the stale data perfectly well - the prompt was reasonable, the answer was well written, the finish reason was clean. What does move is retention, from 4.6 GB to 48.1 GB, a factor of 10.5, and the PII flag, which flips to true because you have just put raw customer text into a log store. This is the only rung on the ladder that costs you something and returns nothing, and it is the one that feels most like progress.
Demo 2 of 3

Free play: get to 5 of 5 yourself ★ 10 min · everyone builds

Same incident, all eight levers unlocked, nothing scripted. Work it yourself, and pay more attention to the levers you did not need than to the ones you did.

Start from nothing and get the diagnosis to 5 of 5. Then write down your minimum lever set - the smallest collection of switches that answers all five questions. Compare it with the person next to you; most people arrive with at least one lever they did not need.

The minimum set is five levers: retriever spans, retrieval attributes, database and cache children, error propagation, and session IDs. Tool spans and token attributes are not required for any of the five questions - tools buy you sequencing, tokens buy you the cost figure - and content capture buys nothing at all here. Knowing which of your levers are load-bearing is what turns an instrumentation backlog into a priority order.

Now switch on tool spans alone and watch the span count grow while the diagnosis stays at 0 of 5. Then switch on retriever spans alone, with tools off, and watch the diagnosis reach 1 of 5 immediately. One lever is instrumentation theatre on this incident and the other is the whole game, and nothing about their names tells you which is which in advance.

Turn on the database and cache children and watch the visible wall clock stay exactly where it was. Those spans are children of the retrieval span, so they add no new accounted time - the time was already counted by their parent. Accounting for time is a property of the top level of the tree; depth beyond that buys meaning, not minutes. This is the single most common misreading of a coverage metric.

Finally, turn the token attributes on and off and watch the cost meter flip between $0.0180 and unknown. Nothing about the run changed. The money was always being spent; without the attributes it was simply not attributable to this user, this feature or this failure.

The question to take back to your own system For each of the five questions, name the attribute in your telemetry that answers it, today, on last Tuesday's traffic. Every question where you cannot name one is a coverage gap with an owner and a size, which is a very different object from a general feeling that observability could be better.
Demo 3 of 3

What the anti-lever actually costs, at every scale ★ 6 min · read the ratio

Rung 8 added no answers. Here is what it added instead, computed live across five traffic volumes from a modelled 4 KB base trace, 38 KB of message content and $0.28 per GB-month. Substitute your own storage price and your own traffic; the interesting column is not the dollars.

Read the Multiple column top to bottom. It is 10.5x at every single row, because it is a ratio of bytes and ratios do not care about scale. Content capture does not cost a little more when you are small and a lot more when you are large - it costs the same order of magnitude more at every scale you will ever operate at.

Now read the dollars at 1,000 runs per day, where the extra rounds to $0. That row is how content capture gets switched on during a quiet week in a pre-production environment and never switched off. The decision was made when it was free and it is still in force at a million runs a day.

Then read the dollars at the top row and notice they are still modest. Money is not the argument - the argument is that you have converted a telemetry store into a system holding raw customer text, usually in third-party SaaS, usually with far broader internal read access than the production database the same text came from. Session b10 does the legal and retention arithmetic properly.

Decide your own policy in one sentence before you leave: metadata at 100%, content sampled, redacted, or scoped to the traces that need it. Write down which of the three you chose. The specification's own default is the first pattern - do not record instructions, inputs or outputs - and the second is described as best suited to pre-production.

The honest cost of the honest default Metadata-rich and content-poor is the right default and it is not free. With content off you can localise a failure to a step, name the step, and prove what it returned - which is exactly what the eight rungs above just did - but you often cannot explain why the model chose what it chose. This incident is the friendly case, where the model was innocent. Session b6 catalogues the cases where it is not, and b10 designs the sampled, consented, time-boxed exception that covers them without holding your whole traffic in plain text.
Homework

Try it yourself - this week ◐ 50-60 min total

Source material

Official sources covered

The incident is constructed; the attribute keys and the arithmetic are not. Every key on the simulated trace comes from a primary source - the OpenTelemetry GenAI semantic conventions and the general database conventions - catalogued in this course's source map and verified 2026-08-13. This page covers:

✓Reading a span tree for unaccounted time: root duration against the union of top-level childrenPart 1 and Demo 2 · computed live, 32.4% to 100% across the climb
✓Span status, error.type and recorded exceptions as the only signal a caught failure ever leavesDemo 1 rung 6 · the mechanism behind the green dashboard
✓Retrieval telemetry: gen_ai.retrieval.documents.count, and db.response.returned_rows from the database conventionsPart 2 and Demo 1 rungs 4-5 · empty is not slow
✓gen_ai.conversation.id as the join key across turns, and the spec's prohibition on fabricating oneRung 7 · intermittent versus constant is a different bug
✓Content capture is Opt-In and off by default, with three usage patterns in the spec's own orderDemo 3 · named here, designed properly in b10
◐The incident itself, its timings and its two application-defined keys (cache_fallback, entry_age_hours)Constructed for teaching · no convention exists for either key, and the registry is worth checking before you invent your own
◐Retention volumes and cost per runModelled unit economics (4 KB base, 38 KB content, $0.28/GB-month, $3.00/$15.00 per million tokens, read 2026-08-13) computed live in the browser
◐Everything in the GenAI conventions is Development status with no tagged releaseStated once per page by policy · check the repository before shipping any key from this course
◐Which traces to keep, and what to alert onNot covered by design · sampling is b7, alerting and SLOs are b8
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · Adding tool spans took visible wall clock from 32.4% to 38.2% and left the diagnosis at 0 of 5. Why is that the most important rung on the ladder?

Framework integrations hand you tool spans early, so they are what teams ship first. On this incident the missing 4.60 seconds was entirely inside a retrieval step that nobody had wrapped, and no amount of tool detail reaches it. The lesson is not that tool spans are useless - it is that coverage has to be chosen against the questions you need to answer, before you know which step will break.

2 · At rung 8 you capture every prompt and completion. Retention goes from 4.6 GB to 48.1 GB, a factor of 10.5, and the PII flag flips to true. The diagnosis stays at 5 of 5. Why does it not improve?

Trace the causal chain and the last link has no telemetry signal at all, because at that point nothing is failing: given stale inputs, the model produced an accurate summary of stale inputs. Content capture is the lever that feels most like debugging and, on this class of failure, contributes nothing while multiplying your retention and converting a telemetry store into a system holding raw customer text.

3 · The p95 latency dashboard was green and no alert fired, yet the warehouse query had timed out. What made the system structurally incapable of telling anyone?

No threshold can fire on a signal that was never emitted. The two missing lines are set_status(ERROR) and record_exception(e) in the except block, and their absence is what separates a system that is degraded and knows it from a system that is degraded and reports itself healthy. This is also why session b8's alerting work has to sit on top of coverage work rather than beside it.

Builder session 5 cheat sheet · pin this

The five questionsWhich step is slow · did retrieval return anything · where did the stale number come from · why did nothing alert · is it intermittent.
Rung 1 reality32.4% of wall clock visible, 4.60s of 6.80s missing, 0 of 5 answerable. Nothing on screen looks wrong.
Tools are theatre hereRung 2 takes coverage to 38.2% and the diagnosis stays at 0 of 5. Easy to instrument is not the same as load-bearing.
100% is not a diagnosisRung 3 accounts for every millisecond and answers 1 of 5. Time coverage tells you where, attributes tell you what.
Empty, not slowZero documents plus cache_fallback true. The 4.2s step was not slow work, it was no work.
The green dashboardTimeout caught, 19.4-hour cache served, success returned, status never set. A swallowed exception is the whole mechanism.
Minimum lever setRetriever spans, retrieval attributes, db/cache children, error propagation, session ids. Five of eight get you to 5 of 5.
The anti-leverFull content capture: 4.6 GB to 48.1 GB (10.5x), PII true, still 5 of 5. The multiple is constant at every scale.