Agent tracing
An agent is a workflow where a language model decides what to do next. Production agents combine LLM calls, tool calls, sub-agent invocations, and retries, often in parallel and on non-deterministic branches. Tracing captures this entire flow as a hierarchical span tree so you can debug, cost-attribute, and optimize at every step. The marketing-side hub lives at /agent-tracing.
The four-layer span tree
Spanlens renders every trace as four nested layers. Each layer maps to a concrete entity in the data model.
| Layer | Span kind | Entity | What it captures |
|---|---|---|---|
| 1. Trace root | trace | traces table | End-to-end latency, total cost, trace ID, user/session tags |
| 2. Agent step | span(kind="agent_step") | spans table | Step name, input state, output state, latency |
| 3. LLM call | span(kind="llm") | spans + requests | Model, tokens, cost, full request/response body |
| 4. Tool call | span(kind="tool") | spans table | Tool name, arguments from the LLM, return value, latency |
Parent/child links
Every span has a parent_span_id field that points to the immediate parent. Spanlens does not enforce a foreign-key constraint on this column, by design — parallel agent fan-out and out-of-order ingestion mean child spans sometimes arrive before their parent. The trace builder reconstructs the tree at query time by joining on (trace_id, parent_span_id). Orphaned spans (parent never arrives) attach to the trace root with a visible "orphan" marker rather than disappearing.
Critical path
Critical path is the longest dependency chain through the trace — the path that determines total wall-clock time. For an agent that runs four steps in parallel and one sequentially after, the critical path is max(parallel_4) + sequential_1. Optimizing a non-critical-path span has zero effect on total latency. Spanlens computes critical path automatically and colors those spans differently in the trace view.
The algorithm walks the span tree from the trace root, follows the slowest child at each branch (when children run in parallel), and adds sequential children directly. Span overlap is detected via (start_ms, end_ms) ranges.
Emitting spans manually
For frameworks Spanlens does not ship a native integration for, emit spans with the SDK.
import { SpanlensClient } from '@spanlens/sdk'
const client = new SpanlensClient()
const trace = await client.startTrace({ name: 'support_ticket_flow' })
const classifyStep = await trace.startSpan({ name: 'classify', kind: 'agent_step' })
const classifyLlm = await classifyStep.startSpan({ name: 'classify.llm', kind: 'llm' })
// ... call your LLM ...
await classifyLlm.end({ tokens: { in: 1240, out: 12 }, cost_usd: 0.0023 })
await classifyStep.end()
await trace.end()tsOpenTelemetry mapping
Spanlens accepts OTLP/HTTP at /v1/traces. OTel span attributes map to Spanlens fields by convention:
| OTel attribute | Spanlens field |
|---|---|
gen_ai.system | provider |
gen_ai.request.model | model |
gen_ai.usage.input_tokens | tokens.in |
gen_ai.usage.output_tokens | tokens.out |
gen_ai.response.finish_reasons | finish_reason |
Where to go next
- LangGraph integration, native callback handler.
- LangChain integration, same callback contract.
- CrewAI integration, multi-agent framework support.
- Tutorial, end-to-end RAG agent example.