Before instrumenting anything, produce the design document: your failure taxonomy from real incidents, your five questions with what each requires, your chosen unit of work, your sampling strategy with arithmetic, and an honest inventory of what your current tooling already gives you. Then stand up the local stack and get one turn traced with the correct topology, proven by a test.
Do the taxonomy from support tickets rather than from imagination; the ordering by real frequency is the whole value, and it usually surprises people. For the local stack, set OTEL_TRACES_SAMPLER=always_on immediately — a sampled-out trace during development wastes more time than any other single thing here. The scripted stub model is worth building properly now: every later module's tests depend on it.
$ docker compose -f docker-compose.telemetry.yml up -d
$ OTEL_SERVICE_NAME=research-agent OTEL_TRACES_SAMPLER=always_on \
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:14317 python agent.py \
--q "what changed in the Q3 filing?"
$ pytest tests/test_agent_spans.py -q
..... 5 passed
trace 4bf92f3577b34da6a3ce929d0e0e4736 (jaeger, 1 root, 6 spans)
invoke_agent research-assistant 1,840ms SERVER
gen_ai.conversation.id = c_8813
gen_ai.conversation.turn_index = 0
├─ chat gpt-4o 410ms CLIENT
├─ execute_tool search_docs 180ms INTERNAL
├─ chat gpt-4o 520ms CLIENT
└─ chat gpt-4o 730ms CLIENT
sampling-strategy.md
measured: steps p50 3 p95 7 p99 14 cost p50 $0.012 p99 $0.31
uniform 1% of 10,000 turns keeps ~0 turns with >=14 steps
stratified: user_feedback 100% · debug 100% · version_rollout 100%
enterprise 100% · first_turn 100% · baseline 2% (w=50)
estimated kept/day: 610 of 10,000 (6.1%), containing every
flagged turn and every enterprise turn
inventory.md
slow turn 8ac9de11: vendor UI accounts for 1,900ms of 4,210ms
unaccounted 2,310ms -> a SELECT and a vector search, invisible there