Skip to main content
The three record different things, so each one answers a different question. Logs hold the values someone formatted into a string. Spans hold the boundaries someone marked out, with metadata attached. An execution graph holds the call tree that ran, with the inputs and outputs each call saw. With spans you ask what was slow. With an execution graph you ask why this list came back empty, and the answer sits on a step’s recorded output. Two rows are worth reading twice. A retry is several sibling spans with nothing in the data relating them, while an execution graph keeps the attempts under the one step they belong to. It labels the group declared where the step was marked retries=True, and observed_retry where the SDK inferred the grouping itself. And sampling here applies to values rather than to requests: an attempt that raised keeps its input and error values at any depth, while one roll per trace decides whether an ok trace’s values are kept at all. Sampling never touches the record of what ran.

Why not log every function’s inputs and outputs

Because you would have all of the values and none of the structure. Nothing in a log stream says which call caused which, so you rebuild the tree from request ids, timestamps, and thread ids, and that reconstruction is the work that costs incident time. The values themselves arrive flattened into strings by whatever formatter wrote them, so a list of five objects and a list of none are two strings you have to parse back. You would also pay the full price on every request, because a log line is written whether anyone reads it or not, and there is no way to ask for less of one. An execution graph is the same information arrived at already assembled, and separable: what ran is one thing to read and the captured bytes are another, each fetched on its own.

What reading less actually costs

Those costs are measured rather than asserted. The MCP server’s tool results were counted with @anthropic-ai/tokenizer 0.0.4, Anthropic’s published BPE, over the text an AI agent receives, on a server seeded with the execution graph spec’s 17-step conformance trace, cloned to 60 traces. vexlo_search_traces over its default page of 10 traces measures 1,473 tokens. The same search at the API’s own default of 50 measures 7,127, which is why the tool asks for 10. That tokenizer is the Claude 2 generation’s and no current model’s is published, so the error of these counts against a current model is unmeasured in both size and direction. The full table is on token costs; the per-rung costs of one trace are on the execution graph.

Where the vocabulary comes from

Where OpenTelemetry already has a word for something, Vexlo uses that word: the service name, trace_id, and status with ok and error, with tags carrying what span attributes carry. Where the concept has no equivalent there, it gets its own name on purpose. A step is a marked function rather than a span, and an attempt, one execution of a step, is Vexlo’s own addition. The borrowing is vocabulary only, so the data stays legible to anyone who knows OpenTelemetry’s terms without being mistaken for its data. The execution graph defines the primitive those words name, and reads one real trace down to its captured values.