Tracing Structure
Span kinds, the span tree, metadata fields, and cost tracking.
Span kinds
Every span has a kind that describes what type of work it represents. The kind is a discriminated union on the type field.
llm_call
The most common kind. Represents a call to a language model.
{
"type": "llm_call",
"model": "gpt-4o",
"provider": "openai",
"input_tokens": 150,
"output_tokens": 42,
"cost": 0.00123
}| Field | Type | Description |
|---|---|---|
model | string | The model identifier (gpt-4o, claude-3-opus, llama-3.1-70b, etc.) |
provider | string | Optional. The provider name (openai, anthropic, ollama, etc.) |
input_tokens | number | Optional. Number of input/prompt tokens |
output_tokens | number | Optional. Number of output/completion tokens |
cost | number | Optional. Estimated cost in USD |
Token counts and cost can be set at span creation time (if you know them up front) or at completion time (more common, since you get token counts from the API response). If you pass kind in both the create and complete requests, the complete request's values take precedence.
custom
For any non-LLM step: database queries, vector searches, API calls, parsing, formatting, tool invocations.
{
"type": "custom",
"kind": "vector_search",
"attributes": {
"collection": "documents",
"top_k": 10,
"similarity": "cosine"
}
}| Field | Type | Description |
|---|---|---|
kind | string | A label for the kind of work (vector_search, http_request, parse, etc.) |
attributes | object | Arbitrary key-value pairs for additional context |
fs_read / fs_write
File system operations, primarily used by the proxy when it intercepts file operations.
{
"type": "fs_read",
"path": "/data/config.json",
"bytes_read": 1024
}| Kind | Fields |
|---|---|
fs_read | path (string), bytes_read (number) |
fs_write | path (string), bytes_written (number) |
The span tree
Spans within a trace form a tree through parent_id. When you create a span with a parent_id, it becomes a child of that span. The dashboard renders this tree visually, letting you expand and collapse branches.
summarize-document (trace)
├── chunk-document (custom: parser)
│ ├── chunk-1 (custom: parser)
│ └── chunk-2 (custom: parser)
├── summarize-chunk-1 (llm_call: gpt-4o-mini)
├── summarize-chunk-2 (llm_call: gpt-4o-mini)
└── combine-summaries (llm_call: gpt-4o)Depth is unlimited. In practice, most traces are 2-3 levels deep.
Automatic tree construction with the SDK
When you use tw.trace() and ctx.span(), the SDK manages parent-child relationships for you:
await tw.trace('pipeline', async (ctx) => {
// This span is a root span (no parent)
await ctx.span('step-1', async (span) => {
// Nested spans could be created with the low-level API
// using span.id as parent_id
});
// This is also a root span (sibling of step-1)
await ctx.span('step-2', async (span) => {
// ...
});
});If you need deeper nesting with the low-level API:
const trace = await tw.createTrace('deep-pipeline');
const parent = await tw.startSpan({
traceId: trace.id,
name: 'outer',
kind: { type: 'custom', kind: 'orchestrator', attributes: {} },
});
const child = await tw.startSpan({
traceId: trace.id,
parentId: parent.id, // nested under "outer"
name: 'inner',
kind: { type: 'llm_call', model: 'gpt-4o', provider: 'openai' },
input: messages,
});
await tw.completeSpan(child.id, { text: 'response' });
await tw.completeSpan(parent.id, { result: 'done' });Metadata and computed fields
Traceway computes additional fields after a span is completed:
| Field | Type | Description |
|---|---|---|
duration_ms | number | ended_at - started_at in milliseconds |
total_tokens | number | input_tokens + output_tokens (for llm_call spans) |
estimated_cost | number | Computed from the model pricing table if not provided |
Cost estimation
Traceway maintains a pricing table with 50+ models across OpenAI, Anthropic, and other providers. When a span is completed with a model and token counts, Traceway looks up the per-token price and calculates the cost.
Supported providers and example models:
| Provider | Models |
|---|---|
| OpenAI | gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-3.5-turbo, o1, o1-mini, o1-pro |
| Anthropic | claude-3-opus, claude-3-sonnet, claude-3-haiku, claude-3.5-sonnet, claude-3.5-haiku |
| Meta (via various providers) | llama-3.1-8b, llama-3.1-70b, llama-3.1-405b |
| gemini-1.5-pro, gemini-1.5-flash, gemini-2.0-flash | |
| Mistral | mistral-large, mistral-small, mixtral-8x22b |
If a model isn't in the pricing table, the cost field is left empty. You can always provide your own cost value in the span's kind field.
The pricing table is updated with each Traceway release. Costs are per-token estimates and may differ from your actual invoice due to caching, batching, or pricing changes.
Trace-level aggregates
The dashboard rolls up span-level metrics to the trace level:
- Total duration — from the earliest span start to the latest span end
- Total cost — sum of all span costs
- Total tokens — sum of all span token counts
- Span count — number of spans in the trace
- Status — if any span failed, the trace shows as failed
These aggregates are computed on-the-fly in the dashboard and API responses. They are not stored separately.
Input and output formats
The input and output fields on a span accept any JSON value. There is no enforced schema. Common patterns:
LLM calls — input is typically an array of messages, output is the response text or structured output:
{
"input": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "What is the capital of France?" }
],
"output": {
"text": "The capital of France is Paris.",
"finish_reason": "stop"
}
}Tool calls — input is the arguments, output is the return value:
{
"input": { "city": "San Francisco" },
"output": { "temperature": 62, "condition": "foggy", "unit": "fahrenheit" }
}Custom steps — input and output are whatever makes sense for your step:
{
"input": { "query": "machine learning", "top_k": 5 },
"output": [
{ "id": "doc_1", "score": 0.95, "title": "Introduction to ML" },
{ "id": "doc_2", "score": 0.87, "title": "Neural Networks" }
]
}The dashboard renders these as formatted JSON. Large values are collapsed by default and expandable on click.