What is Add Tracing?
Add Tracing is an AI refactoring for LLM systems, in the Operations group. Record every call, tool, prompt version, and cost in a browsable trace.
Adicionar Trace
Add Tracing is an AI refactoring for LLM systems, in the Operations group. Record every call, tool, prompt version, and cost in a browsable trace.
At 3:12 a.m., the incident agent restarted the catalog service, which was healthy. The broken service was payments. The next morning, the log had two lines: “alert received” and “done”. Nobody could say what the agent read, what it concluded, or which prompt version was live.
When something goes wrong, nobody can see which calls, tools, and context led to the answer. These are the No Tracing and Unversioned Prompt smells.
Wrap each run in a trace, and each model call, tool, and code decision in a child span, with the attributes that explain the decision: prompt and version, input, output, tokens, cost, latency, and tool results.
def handle_alert(alert):
- log.info(f"alert received: {alert.name}")
- result = agent.run(alert)
- log.info("done")
- return result
+ with tracer.start_as_current_span("incident.handle") as root:
+ root.set_attributes({"alert.name": alert.name, "agent.version": AGENT_VERSION,
+ "prompt.version": PROMPT.version})
+ result = agent.run(alert) # the agent creates child model and tool spans
+ root.set_attributes({"result.action": result.action,
+ "cost.usd": result.cost_usd, "stop.reason": result.stop_reason})
+ return result
+
+# inside the agent loop
+with tracer.start_as_current_span(f"chat {MODEL}") as span:
+ reply = client.messages.create(...)
+ span.set_attributes({"gen_ai.operation.name": "chat",
+ "gen_ai.request.model": MODEL,
+ "gen_ai.usage.input_tokens": reply.usage.input_tokens,
+ "gen_ai.usage.output_tokens": reply.usage.output_tokens,
+ "gen_ai.response.finish_reasons": [reply.stop_reason]})
Before: the log says it started and it ended.
flowchart LR
A(["1. Alert"]):::io --> M(["2. Agent"]):::llm
M --> L[("3. Two log lines")]:::store
After: a tree of spans that tells the whole run.
flowchart TD
R(["1. Trace: incident.handle"]):::io --> S1(["2. chat"]):::llm
R --> T1["3. execute_tool read_logs"]:::tool
R --> S2(["2. chat"]):::llm
R --> T2["3. execute_tool restart_service"]:::tool
S1 --> A1["4. prompt, version, tokens, cost"]:::tool
T1 --> A2["4. arguments, result, error"]:::tool
R --> D[("5. Trace storage")]:::store
A model’s decision is a function of what went into its context. A trace stores exactly that, in order, linked to the tool that produced each piece. With it, “why did the agent restart the catalog?” becomes a reading exercise.
This is the 3:12 a.m. trace after the refactoring (abridged):
incident.handle 4.8s alert=payments-5xx agent=v14 prompt=triage@7 cost=$0.052
├─ chat claude-... 1.9s in=11,840 out=212 finish=tool_use tools=[read_logs]
├─ execute_tool read_logs 0.3s args={"service": "catalog"} result="1,204 lines, 0 errors"
├─ chat claude-... 2.1s in=13,102 out=148 finish=tool_use tools=[restart_service]
└─ execute_tool restart_service 0.5s args={"service": "catalog"} result="ok"
Reading it takes a minute. The alert was about payments, but the first chat asked for the catalog’s logs. The log came back clean, and instead of growing suspicious, the model concluded the catalog needed a restart. The cause sits in the first span: prompt triage@7 carries an example where the service is always catalog.
Without the trace, the team would have argued about “the model hallucinated”. With it, the fix is concrete: replace the example in the prompt and add an eval case with this alert.
The tree structure is what sets a trace apart from a log. Every span knows its parent, so you can sum cost per task, see the critical latency path, and compare good and bad runs of the same task type. Two tasks running at the same time do not get mixed up, because every record carries the run’s identifier.
The prompt version on each span closes the loop: a bad answer points to the exact text that produced it, and the case goes into the eval with everything needed to reproduce it.
gen_ai.* semantic conventions works with most observability tools (Jaeger, Grafana Tempo, Langfuse, Arize Phoenix, Datadog, among others). These conventions are still marked as in development and have already renamed attributes between versions, so pin the version you follow.traceparent header from the W3C Trace Context standard) so everything lands in the same trace.Suppose a model that charges US$ 3 per million input tokens and US$ 15 per million output tokens (illustrative prices; use your provider’s table). A span with 11,840 input tokens and 212 output tokens costs:
11,840 × 3 / 1,000,000 = US$ 0.0355
212 × 15 / 1,000,000 = US$ 0.0032
span total = US$ 0.0387
Recorded on every span, this number adds up on the root span by itself. Then “which alert type costs the most?” becomes a query, not a spreadsheet.
Keep attributes and content apart. Attributes (model, tokens, cost, version, stop reason, duration, ids) are small and should exist on 100% of runs. Content (full prompt, answer, tool output) is heavy and carries personal data, so it can be sampled.
Sampling has to look at the end of the run, not the start. Sampling a random 5% up front misses exactly the bad case. Decide after the run finishes, and always keep runs with errors, high cost, a limit stop, or an action with side effects.
SimpleSpanProcessor sends each span right away; in production use BatchSpanProcessor, which sends batches in the background.This span holds information, but it does not help explain the decision. What is missing?
with tracer.start_as_current_span("llm") as span:
reply = client.messages.create(model=MODEL, system=SYSTEM, messages=msgs, tools=TOOLS)
span.set_attribute("duration_ms", elapsed_ms)
span.set_attribute("ok", True)
It is missing the prompt version, the tokens (for cost), the stop reason, the tools requested, and, when policy allows, the input and output. Duration is already measured by the span itself, and ok=True says nothing about the quality of the decision.
with tracer.start_as_current_span(f"chat {MODEL}") as span:
reply = client.messages.create(model=MODEL, system=SYSTEM, messages=msgs, tools=TOOLS)
span.set_attributes({
"gen_ai.operation.name": "chat",
"gen_ai.request.model": MODEL,
"prompt.version": SYSTEM_VERSION,
"gen_ai.usage.input_tokens": reply.usage.input_tokens,
"gen_ai.usage.output_tokens": reply.usage.output_tokens,
"gen_ai.response.finish_reasons": [reply.stop_reason],
"tools.requested": [b.name for b in reply.content if b.type == "tool_use"],
})Add Tracing is an AI refactoring for LLM systems, in the Operations group. Record every call, tool, prompt version, and cost in a browsable trace.
It treats Unversioned Prompt and No Tracing.
The Audit Log design pattern.