Cheat sheets

OpenTelemetry GenAI and Agent Observability

Emit gen_ai.* spans, sample traces, export metrics, pick a platform and debug a trace. Stdlib code from the lessons.

  • Concepts and terms
  • Tasks and commands
  • Annotated code
  • Diagrams
  • Comparisons
  • Defaults and numbers
  • Troubleshooting
  • Questions and tips

Source

Agent span hierarchy#

parentSpanIdparentSpanId_meta.traceparentparentSpanIdspansprocessagent.invoke_agentINTERNAL, root spanmodelllm.chatCLIENT, LLM providerprocesstool.executeINTERNALprocessmcp.callCLIENT, traceparentprocesssubagent.invokeINTERNALstoreExporterOTLP to any backendparentSpanIdparentSpanId_meta.traceparentparentSpanIdspansprocessagent.invoke_agentINTERNAL,root spanmodelllm.chatCLIENT, LLMproviderprocesstool.executeINTERNALprocessmcp.callCLIENT,traceparentprocesssubagent.invokeINTERNALstoreExporterOTLP to anybackend

Attributes to set#

gen_ai.operation.name
chat, embeddings, execute_tool, invoke_agent.
gen_ai.provider.name
openai, anthropic, aws.bedrock, google.vertex.
gen_ai.request.model
the model that you ask for.
gen_ai.response.model
the model that served the call. Routing can change it.
gen_ai.usage.input_tokens
tokens in. gen_ai.usage.output_tokens gives tokens out.
gen_ai.response.id
the provider response id for correlation.
gen_ai.tool.name
the tool. gen_ai.tool.call.id names one call.
gen_ai.data_source.id
the corpus or store that a RAG step read.

Instrument in code#

TaskCode
Start a child spanstart_span(name, kind, parent=root)
Build a traceparentf"00-{mcp_span.trace_id}-{mcp_span.span_id}-01"
Store content as a referencetracer.add_content(chat, key, prompt)
Wrap work in a spanwith builder.span(name, attributes=..., parent=...)
Write one span per lineJSONLExporter(path=trace_path)
Count tool callsmetrics.counter("tools_called_total")
Render Prometheus textprometheus_exposition(registry)
Sample in an exportermakeSamplingExporter(consoleExporter, 0.05)

Span builder with metrics#

metrics = MetricsRegistry()
builder = SpanBuilder(exporters=[jsonl, in_mem], metrics=metrics)
with builder.span("gen_ai.chat", attributes={GEN_AI_SYSTEM: "anthropic",
GEN_AI_OPERATION_NAME: "chat"}, kind="CLIENT") as chat_span:
chat_span.attributes[GEN_AI_USAGE_INPUT_TOKENS] = 412
with builder.span("gen_ai.tool.execution", parent=chat_span,
attributes={GEN_AI_TOOL_NAME: "read_file"}) as tool1:
tool1.attributes[GEN_AI_TOOL_RESULT_BYTES] = 1024
print(prometheus_exposition(metrics))tools_called_total, tool_latency_ms

Run the lessons#

CommandWhat it does
python code/main.pyLesson 13.20 prints the spans of one trace
OTEL_CAPTURE_CONTENT=1 python code/main.pyAdds prompt and completion events
python3 code/main.pyLesson 14.23 prints a span tree and the content store
python3 -m pytest code/tests/ -vLesson 19.28 checks round trip and buckets
npm startSeeds 1200 synthetic spans, prints the rollup
npm run serveServes ingest and dashboard on PORT

Trace debugger commands#

python3 scripts/project_test.py agent-trace-debugger --init learning-artifacts/agent-trace-debugger
python3 scripts/project_test.py agent-trace-debugger --stage 1 --path learning-artifacts/agent-trace-debugger --strict
python3 scripts/project_test.py agent-trace-debugger --all --solution --strict
node cli.ts --input samples/trace.jsonl --baseline samples/before.jsonl --output trace.html --json trace.json
node cli.ts --format otlp --input samples/otlp.json --output otlp-trace.html --json otlp-trace.json

Questions#

CLIENT or INTERNAL?
CLIENT crosses a process boundary. INTERNAL is the agent loop and tool execution.
Which invoke_agent kind?
CLIENT for remote agent services. INTERNAL for in-process frameworks.
What do I keep at scale?
100% errors, 100% high-cost, 5% success.
When is drift real?
PSI above 0.2 typically signals meaningful drift.
Is tracing enough?
No. Tracing without evaluation is just expensive logging.
Which metrics are histograms?
gen_ai.client.token.usage, gen_ai.client.operation.duration, gen_ai.tool.execution.duration.

Debug a trace: agent-trace-debugger#

1parseTraceone JSON object per line. A bad line stops withits line number.2validateTreerejects a duplicate id, a cycle, a missingparent, a child outside its parent.3unionDurationmerges overlapping child intervals. It does notsum them.4analyzeexclusive time is inclusive time minus the childunion.5analyzeslowest names the span with the greatestexclusive time.6renderwrites standalone HTML with escaped names anderror coloring.7cli.ts--baseline adds wall_time_delta_ms, token_delta,error_delta.

Defaults and numbers#

SettingDefaultNote
OTEL_CAPTURE_CONTENT"0"Set 1 to capture prompt and completion
TailSampler sample_rate0.10Errors and PII evals are always kept
ObservabilityStore capacity10_000Ring buffer of spans
PORT8011Set SERVE=1 to start the HTTP server
High-cost spantotalTokens > 8000Always kept by the sampling exporter
--formatjsonlAlso otlp. Baseline files stay JSONL
--outputtrace.html--json writes the report file

Troubleshooting#

SymptomCauseFix
PII and secrets in tracesFull prompts captured in spansStore content outside, put reference IDs on spans
Multi-provider dashboard breaksNo gen_ai.provider.nameSet it on every LLM span
Orphaned tool spansSpans without parent linksAlways propagate context
Attributes renamed on upgradeNo stability opt-inSet OTEL_SEMCONV_STABILITY_OPT_IN
MCP server not in the traceStdio carries no HTTP headersPut traceparent in _meta of each request
Cannot bisect a regressionPrompt versions not tied to tracesRecord the prompt version on the trace
child outside parentChild interval exceeds the parentNormalize clocks before import

Pick a platform#

FactorLangfusePhoenixOpik
LicenseMITElastic License 2.0Apache 2.0
Strongest forPrompt management, sessionsRAG relevancy, driftOptimization, guardrails
Prompt versioningYesNoA/B experiments
Free tier50K events/monthSelf-hostSelf-host or cloud
Use it whenAll-in-one with promptsDeep RAG eval and driftAutomated optimization