← All lessons Lesson 0018

Production traces

One id through the world

Lesson 0014’s trace was a trial you ran on purpose. Production is the same object when a real user hits POST /assistant. Stamp one id on the HTTP request, the agent, the tools, and Stripe. Three log files with three ids are not a path. OpenTelemetry’s word for the path is a trace.

What this is. Distributed tracing applied to an agent: a root span for the request, child spans for the loop and each tool, the same trace_id on the refund row. Anthropic: read transcripts; outcome is environment state. Hamel: error analysis is looking at traces. OpenTelemetry · Traces · Demystifying evals

What this is not. A vendor APM product. LangSmith as a religion. Re-teaching task · gate. Production eval suites (capability vs regression, LLM-as-judge) — next. Collector YAML, GenAI attribute churn — parked. Not a ChatGPT menu named “Trace.”

  1. 1 · CorrelateOne id on HTTP, agent, tools, and the DB.
  2. 2 · SpanA unit of work with a parent. Nest the loop.
  3. 3 · ReadOpen the tree. Grade the world, not the chat.

The plugin crate (0017) does not give you this. You instrument the app. The model does not mint the id.

A rust thread labeled REQ-441 stitching HTTP POST /assistant, an invoke_agent span, a Stripe tool span, and a Postgres row.

Architecture first. OpenTelemetry: traces are the path of a request through the application — including the model.

Who decides the id — not the agent

The language model does not pick a trace id. The first process that handles the request without an incoming W3C traceparent header does: usually your API’s OpenTelemetry HTTP instrumentation. It generates a random 128-bit id and a root span. If the load balancer already sent traceparent, your server extracts that id instead of minting a second one. OpenTelemetry’s name for the in-memory bag is Context; moving it across hops is propagation (inject on the way out, extract on the way in). OTel · Context propagation · W3C Trace Context

Browser with no id, API box where the SDK mints a trace id, a context bag in RAM, tool code stamping Stripe. Caption: the model does not invent this.

Birth of the id. Your harness reads the bag. The agent loop is just more child spans inside it.

Top: context bag in memory during the request. Bottom: spans exported to a backend, plus Stripe metadata and a refunds.request_id column.

Two stores: the exporter’s tree (for on-call) and the columns you stamp (for grading the world).

Logs are not a path

A span is a unit of work with start, end, and a parent. Spans that share a trace_id are one trace. That is how you join an API 200, a chat completion, and a Stripe call that ran in another process. Unjoined lines make on-call into a vibe check. OTel · Traces

Nested spans: HTTP root, invoke_agent, chat and execute_tool children, SQL sibling, all with the same trace id. A dangling log with a different id is crossed out.

GenAI span names (invoke_agent, chat, execute_tool) are still evolving. The rent is the tree, not the latest attribute prefix.

INV-441 · one production trial

# 1 MINT (first hop, no incoming traceparent)
OTel HTTP middleware:
  trace_id = random_128_bit()          # SDK, not the LLM
  Context.set(trace_id)
  root_span = "POST /assistant"

# 2 CARRY (same process)
invoke_agent()  # child span — inherits Context automatically
chat()          # child of the agent span
# async/queue: YOU must pass Context or you mint a new (wrong) id

# 3 STAMP (vendors that do not speak OTel)
stripe.refunds.create({ metadata: { request_id: Context.trace_id } })
INSERT refunds (request_id, …) VALUES (Context.trace_id, …)

# 4 STORE
export spans → your backend (Jaeger / Grafana / …)
# the id is not “in the model.” It is in Context, then in the exporter, then in your columns.

# SPANS (parent → child)
HTTP POST /assistant
  invoke_agent  billing
    chat                 finish=tool_calls
    execute_tool         stripe.refunds.create
                         status=ERROR  # hook: policy_ok=false

# READ — then grade (0014)
assistant: "Refund issued. You’re all set."
outcome:   no new Stripe id; policy_ok=false
GRADE: FAIL

# NOT A TRACE
api.log request=a1
llm.log session=z9
stripe: re_1   # unjoined

What this shows. Mint → carry → stamp → store. The Guard from 0015 is visible as a denied tool span. The claim still lied. Full prompt bodies in production are opt-in debug — PII lives there.

Every kitchen station slip stamped TICKET 441. An unstamped slip in the trash labeled vibe.

Kitchen optional. The ticket number is the trace id. Unstamped slips are last night’s vibe check.

Skill: open one path

Logs do not join. Origin is who mints. Correlate stamps vendors. Spans nest the work. Read grades the world.

Same refund, five looks

Who owns which stamp

Developer

Turn on HTTP auto-instrumentation. Pass Context into tool runners. Copy trace_id onto every mutation. Do not ask the model to invent an id.

Team lead

On-call runbook: open the id. Sampling in prod (not 100% of prompt bodies). The crate (0017) does not replace instrumentation.

Next

A production eval suite is a sampler over these trees plus a grader. Tonight you only need trees you can open.

Parked: OpenTelemetry Collector, GenAI semantic-convention instability (Development status), LangSmith, LLM-as-judge, red-team injection. You can export to whatever backend your platform already uses. The id is the architecture.

Check

1. A production trace is
2. Spans share a
3. Grade still uses
4. Unjoined logs mean
5. Capture full prompts

Primary source. OpenTelemetry · Traces (path of a request; spans share a trace id). Who carries the id: Context propagation (W3C traceparent). Spec: W3C Trace Context. Agent trial record: Demystifying evals. Craft: Your AI Product Needs Evals.

Card: production traces. Trial: evals. Guard: team floor.