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

Spans and traces: the anatomy of one thing that happened

An eval score is a claim about a population. A trace is a record of one event. This session builds the vocabulary the whole course runs on - span, trace, parent, attribute, status, event - on the smallest possible unit, a single model call, and then shows what happens to that vocabulary the moment the call becomes an agent with tools underneath it. By the end you can read a waterfall and say precisely which part of it you are not being shown.

🟢 Builder track AI engineers · platform engineers · technical PMs 45 min
0-3 · Welcome 3-18 · Span anatomy + the tree 18-40 · Build-along: trace one call, then the agent 40-45 · Q&A
Part 0

What this course is, and what it is not

This is not a course about eval metrics, and it is not a course about platform architecture. Three siblings own those: Learn Evals owns retrieval and generation metrics, Agent Launch owns the go/no-go decision, and AI Infrastructure owns the platform layers underneath. This course owns one thing they all assume and none of them teach: the trace.

Live - presented in session Self-study - read after class Official sources covered
★ What you walk out with today The six words this course runs on and what each one buys you, the difference between a flat log line and a parented span tree, the three questions a trace must answer before it is worth storing, and your first instrumented call - with the attribute keys spelled the way the standard spells them.
One sentence for the whole course Evals tell you whether it works. Observability tells you what actually happened. Both are necessary and they answer different questions: an eval score is a claim about a distribution you constructed, and a trace is the receipt for one real request that one real person made at 09:14 on a Tuesday.
Part 1 · the vocabulary

Six words, and what each one is for 7 min live

Tracing has a small vocabulary and it is worth being pedantic about, because almost every argument about observability tooling turns out to be two people using one of these words differently.

Span one unit of work One operation with a beginning and an end: a model call, a tool call, a database query, a whole agent run. If you can time it and it can fail on its own, it deserves to be a span. Trace one request, end to end Every span sharing one trace id, assembled into a tree by their parent ids. The trace is the unit you open when somebody says "this one answer was wrong". It is the receipt. Parent who called whom The one field that turns a pile of log lines into a shape. Without parenting you know six things happened; with it you know the retrieval happened INSIDE the agent's second reasoning step. Attributes key-value facts gen_ai.provider.name · gen_ai.usage.input_tokens · gen_ai.retrieval.documents.count Duration tells you WHERE the problem is. Attributes are the only thing that tells you WHAT it is. Status ok, error, unset The field every dashboard aggregates and almost nobody sets deliberately. A caught exception that does not set status leaves a failing system reporting itself green. Session b6 lives here. Events timestamped notes Things that happened at a moment rather than over an interval: a retry fired, a guardrail tripped, an exception was recorded. Also where message content belongs, if you keep it at all - see b10. Duration localises a problem. Attributes identify it. Status decides whether anyone is told.
🔍 Click to zoom - the six words, and the distinct job each one does
LiveWhy a log line is not enough4 min▶

Most teams arrive at AI observability already logging. The logs look adequate right up until the first real incident, and then they fail in a specific way worth naming.

Log linesA parented trace
ShapeA flat sequence, ordered by clockA tree, ordered by causation
ConcurrencyInterleaved and ambiguous - two parallel tool calls become four confusing linesTwo sibling spans with overlapping intervals. Unambiguous
"Where did the time go?"Subtract timestamps by hand, hope nothing is missingRead it off the waterfall
NestingInferred from indentation, if someone botheredExplicit parent id
Under an agent loopCollapses. Iteration 3 looks exactly like iteration 1Each iteration is its own subtree

The honest summary: logs answer "what did the code say", traces answer "what did the system do". For a single-shot model call the gap is narrow enough that logs get you by. For anything with a loop in it, the gap is the whole problem.

Self-studyThe three questions a trace must answer to be worth storing3 min read▶

Instrumentation is not free, so it is worth having a test for whether a span is earning its keep. A trace is worth storing when it can answer all three of these about a single request:

  • Where did the time go? Every second of wall clock should sit inside some span. If the child spans do not add up to the parent, the missing time is exactly where your next incident will hide.
  • What did each step actually get and return? Not the full text necessarily - the counts, the ids, the row counts, the document counts. "Retrieval ran" is nearly useless; "retrieval returned zero documents" is the whole diagnosis.
  • Did anything fail, including quietly? A caught-and-handled failure is still a failure and still belongs in the record. This is the one teams get wrong most often, and it is why a broken system can hold a green dashboard for a week.

If a span cannot contribute to any of those three answers, it is costing you storage and cardinality for nothing. Session b7 puts numbers on that trade.

Part 2 · the tree

What happens when one call becomes an agent 6 min live

A single model call is a trivially easy thing to observe: one span, two token counts, done. Everything hard about this course arrives when the call gains the ability to decide what to do next.

A single model call - one span, and observability is basically solved chat claude-sonnet-4-5 1.1s An agent run - the same call, now with the right to decide what happens next insight_bot.answer 6.8s AGENT chat (plan) 1.1s tool 0.2s retrieve_metrics 4.2s RETRIEVER db.query 4.1s returned_rows=0 cache.get HIT age=19.4h The two red spans are the incident this whole course chases. Neither of them is an LLM call, and neither raises an error. The LLM is rarely the broken part. It is just the only part your SDK instrumented for free.
🔍 Click to zoom - one span versus a tree, and where the failure actually lives
The asymmetry that defines this course Auto-instrumentation gives you the model calls, because that is what the vendor SDK wraps. The model calls are also the part least likely to be your bug. Retrieval, caching, tool arguments and swallowed exceptions are where production breaks, and every one of them is something you have to choose to instrument. Coverage is not a technical problem; it is a decision about where you look, made before you know what you are looking for.
Demo 1 of 2

Instrument one call, by hand, once ★ 12 min · everyone builds

Do this manually exactly once, so that every auto-instrumentation library you use afterwards is something you understand rather than something you trust. Any language with an OpenTelemetry SDK works; the shape below is Python.

Wrap your simplest model call in a span. Name it for the operation, not the function: chat <model> rather than call_llm_v2. Span names become dashboard rows, and a dashboard row named after a function is a dashboard row nobody reads.

Set the attributes the standard already has names for - model, token counts, finish reason. Session b2 goes through the exact current keys. Use the standard keys even if you are only ever going to look at them yourself: the day you switch tooling, conventional attributes port and bespoke ones do not.

Now deliberately break it. Raise an exception inside the span, and confirm the span comes back with error status. Then catch the exception, handle it, and watch the span go green while the failure is still real. Sit with that for a second - you have just reproduced the exact mechanism behind the incident this course is built around.

Add a second span around the whole request, as the parent. Confirm the child's duration sits inside the parent's, and subtract: the difference is your first piece of unaccounted time. Write the number down.

★ The shape - the only three lines that matter

with tracer.start_as_current_span(f"chat {model}") as span:  ·  span.set_attribute("gen_ai.request.model", model)  ·  span.set_attribute("gen_ai.usage.input_tokens", resp.usage.input_tokens)  ·  and on the failure path, span.set_status(Status(StatusCode.ERROR)) plus span.record_exception(e) - even when you handle the error and carry on.

The one habit worth forming today Every time you write a try / except around anything an AI request depends on, set the span status in the except block before you return your fallback. That single reflex is the difference between the incident in this course taking twenty minutes to find and taking a week.
Demo 2 of 2

Read the trace you will be living in ★ 8 min · explore

This is the incident the builder track hunts. Open it now with everything switched off, the way it actually shipped, and just look at the gap. Session b5 does the full hunt; today you only need to feel how innocent a badly instrumented trace looks.

Before touching a lever, read the red bar under the waterfall. Two thirds of the run is inside no span at all - and nothing on screen looks wrong. This is what your on-call engineer saw.

Switch on Tool spans alone. Notice how little it helps. Tools are the easy thing to instrument and they were never where the time went - a very common way to feel productive without getting closer to the bug.

Now switch on Retriever spans. The gap closes to zero and a 4.2 second step appears out of nowhere. You have not changed the system at all; you have changed only what it reports about itself.

Leave it there for today. The remaining levers, and the five questions a complete diagnosis has to answer, are session b5.

Homework

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

Source material

Official sources covered

Every convention and claim on this page traces to a primary source - the OpenTelemetry specification and its GenAI semantic conventions - catalogued in this course's source map. This page covers:

✓Span, trace, parent, attribute, status, event - the OpenTelemetry tracing data modelPart 1 · the vocabulary the rest of the course assumes
✓Manual instrumentation: starting a span, setting attributes, recording an exception, setting statusDemo 1 · done by hand once, on purpose
✓Agent-run span structure: parent agent span with model, tool and retrieval childrenPart 2 · framework auto-instrumentation in b3
◐The exact current GenAI attribute keys, with required and recommended statusUsed loosely here · spelled out properly in b2
◐Message-content capture and where it belongs in the data modelNamed here · the privacy and cost decision is b10
◐Metrics and logs as the other two signalsNot covered by design · this course is trace-first, and b8 adds the metrics you alert on
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · Your child spans add up to 2.2 seconds and the parent span says 6.8 seconds. What have you learned?

Unaccounted time is the most actionable number in a trace. The parent is measuring exactly right - it is measuring work you have chosen not to describe. Every serious AI incident in this course lives in that gap, because a step you do not instrument cannot appear slow, cannot appear empty and cannot appear failed.

2 · Which part of an agent run does vendor SDK auto-instrumentation reliably give you, and why is that awkward?

Auto-instrumentation wraps what the vendor ships, and the vendor ships the model client. That is genuinely useful and it is also the layer that fails least often. The failures that reach your users tend to sit in the glue - an empty retrieval, a stale cache, a tool called with the wrong argument - and none of that appears unless someone decided it should.

3 · You catch an exception, fall back to cached data, and return a successful response. What must happen to the span?

This is the mechanism behind the incident the whole course chases. The user got a response, so nothing looks wrong, and the fallback quietly served nineteen-hour-old data. If the caught exception never becomes a span status or a recorded event, your monitoring is structurally incapable of telling you the difference between working and degraded - and it will hold a green dashboard through an entire incident.

Builder session 1 cheat sheet · pin this

SpanOne timed unit of work that can fail on its own. If you can time it and it can break, it earns a span.
TraceAll spans sharing a trace id, assembled by parent id. The receipt for one real request.
The division of labourDuration localises the problem. Attributes identify it. Status decides whether anyone is told.
Unaccounted timeParent duration minus the sum of children. The single most useful number you can compute today.
Logs vs tracesLogs say what the code said. Traces say what the system did. The gap becomes the whole problem once there is a loop.
The auto-instrumentation asymmetryYou get the model calls free. The model calls are rarely the bug.
The try/except reflexSet span status in the except block before returning the fallback. Every time. This is the habit.
Three questionsWhere did the time go? What did each step get and return? Did anything fail, including quietly?