Seeing what your agent actually did, tracing in 10 steps with OpenTelemetry
An agent runs, makes twenty tool calls and delivers something at the end. Why? Tracing opens the black box and shows you every step on a timeline. Here is how to set it up.
You built an agent, it calls a few tools, thinks a bit in between and delivers a result at the end. If the result is good, you are happy. If it is wrong, you are standing in the dark. Which tool fired? What came back? Where did the chain tip over? With a single prompt you can still read that out of the log. With an agent that takes twenty steps across three MCP servers, you cannot.
Tracing solves exactly that. You make every step visible as an entry on a timeline, with duration, input and output, nested the way the calls really happened. This is not an exotic technique, it is the same standard that large web systems have been observed with for years. And since the MCP spec of 28 July 2026 anchored W3C Trace Context properly, a trace like that travels cleanly from your host app through the client, over the server and down into the last tool. I will walk through the ten steps you need to get from blind to seeing.
Step 1, the difference between logs and traces
Logs are notes you toss somewhere. "Called tool search." "Got a response." Every line stands on its own, none of them knows about the others. With an agent that makes many parallel and nested calls, that quickly turns into a pile where you can no longer tell what belongs to what.
A trace is a timeline with structure. One trace covers one complete run, for example a whole agent request. Inside it sit spans, and every span is a single section with a start, an end and a parent span. The agent run is the outer span, every tool call a child span inside it, every LLM call too. In the end you see a tree instead of a list, and in that tree you read off immediately where the time goes and where something went wrong.
Step 2, the vocabulary you actually need
Three terms, no more than that for a start. A trace is the whole process and has a trace ID. A span is one section inside it and has its own span ID plus a reference to its parent span. An attribute is a key-value pair you hang on a span, for example which model ran or how many tokens it cost.
That is enough to understand everything else. OpenTelemetry, OTel for short, is the open standard that defines those three things. It is vendor neutral, which means you instrument once and can send the traces to any compatible backend later without touching your code.
Step 3, why the trace holds across system boundaries
The trick with OTel is passing the IDs along. When your agent calls an MCP server, it hands the trace ID over with it. The server opens its own span, attaches it as a child and passes the ID on to the tool. That way a single tree comes out of it, even though three separate processes were involved.
The standard for this is called W3C Trace Context. It carries the information in a header named traceparent, plus tracestate and baggage for extra info. The format is fixed, an illustrative example looks like this:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
Four parts, separated by hyphens. Version, then the trace ID, then the ID of the calling span, then a flag for whether this trace is being recorded. You never have to build that by hand, the library does it. But it helps to know that this exact string is what keeps everything together.
Step 4, what the new MCP spec changes about this
Until recently, where these IDs belong inside an MCP request was a matter of interpretation. Every server did it a little differently, and a trace often broke off at the boundary between client and server. Spec version 2026-07-28 cleans that up. traceparent, tracestate and baggage now have fixed places in the _meta field of every request. That is SEP-414.
For you that means, if you use a current MCP server or client, trace context propagation either ships with it already or you can rely on the key names being right. No more guessing where the ID goes. If you do not know the details of the spec yet, you will find them in the playbook on the 2026-07-28 spec.
Step 5, pick a backend where the traces land
Instrumenting is one half, looking at it is the other. You need a destination that receives the traces and shows them as a timeline. For LLM and agent applications, Langfuse is a good first choice, because it is cut exactly for this case. Per span it shows the prompts, the responses, the token counts and the costs, not just bare time bars.
Langfuse can be self hosted and takes traces in OpenTelemetry format. That is the point that gives you freedom. You write your code against OTel, and if Langfuse does not suit you later, you send the same traces somewhere else. Alternatives that also speak OTel are Jaeger for the purely technical view, or a managed backend if you do not want to run anything yourself.
Step 6, set the first span by hand
Start small. Take a single function, the outermost point of your agent, and wrap it in a span. In Python with the OTel SDK, the core of it looks like this:
from opentelemetry import trace
tracer = trace.get_tracer("mein-agent")
with tracer.start_as_current_span("agent-run") as span:
span.set_attribute("agent.task", task_beschreibung)
ergebnis = agent.run(task_beschreibung)
span.set_attribute("agent.status", "ok")
That is the whole magic. start_as_current_span opens the span, everything that happens inside the with block hangs underneath it automatically, and at the end it closes itself. The attributes you set show up in the backend later and make the span readable. In TypeScript and other languages the pattern is identical, only the syntax differs.
Step 7, make the tool calls visible one by one
One span around the whole run shows you the total duration, nothing more. The value comes when every tool call becomes its own child span. So you put a span around every tool call as well, with the name of the tool and the important arguments as attributes.
with tracer.start_as_current_span("tool-call") as span:
span.set_attribute("tool.name", tool_name)
span.set_attribute("tool.args", str(argumente)[:500])
antwort = tool.call(argumente)
span.set_attribute("tool.result_len", len(str(antwort)))
One detail that will bite you otherwise. Cut long values off, here at 500 characters. If you store complete tool responses of ten thousand characters as an attribute, you bloat your traces and make the backend slow. The point is to see the structure, not to archive every byte.
Step 8, do not write secrets into the trace
This is the place where tracing gets dangerous if you are not careful. A trace captures inputs and outputs, and things end up in there fast that do not belong in an observability backend. An API key in an argument. A customer email address in a tool response. A whole prompt with personal data in it.
Build yourself a small function that replaces known patterns before an attribute gets set, keys, email addresses, tokens. And with every attribute, think about whether you really need that value to debug a problem. Most of the time the length or a hash is enough instead of the content. If you want to go deeper on data minimisation, you will find the basics in the playbook Data protection with AI tools.
Step 9, answer a question from the trace
Instrumentation is a means to an end. The end is answering a concrete question fast. Three of them come up again and again. First, where does the time go. You open the slowest run and see immediately which span fills the bar, often it is a single tool call that is blocked on an external API.
Second, where does the logic tip over. A run delivers nonsense, you walk through the child spans in order and find the point where a tool already returned the wrong answer, before the model built on top of it. Third, what does a run really cost. If you record token counts as attributes, you add them up across the trace and see the real cost per request instead of a monthly bill with no breakdown. For the pure cost side, the playbook What AI really costs is worth reading on top.
Step 10, make tracing a habit
An agent you instrumented once does not do much for you if you only look at it when something is already on fire. The benefit comes out of the routine. Look at a few traces after you deployed a change, not only when someone complains. Settle on two or three attributes that you record for every agent, task, model, status, token count, so your traces stay comparable.
And keep tracing and evaluation cleanly apart. A trace tells you what happened, an eval tells you whether the result was good. You need both, but they are two different tools. If you want to carry on with the eval part, the playbook Agent eval in 60 minutes is the right next step.
What next
If you do not have an agent of your own running yet that is worth tracing, build one first with the Claude Agent SDK and then come back here. If you already run MCP servers and want to know what else the new spec changes, read MCP goes stateless. And for the big arc on agent quality, Agent eval in 60 minutes belongs right next to this playbook as its twin.
Source
- W3C Trace Context in the MCP spec (SEP-414), official announcement: https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/
- OpenTelemetry concepts (traces, spans, context propagation): https://opentelemetry.io/docs/concepts/
- W3C Trace Context standard (traceparent format): https://www.w3.org/TR/trace-context/
- Langfuse, OpenTelemetry compatible tracing for LLM applications: https://langfuse.com/docs