A local-first CLI for critical paths, a high-confidence Recovery Ledger, retry loops, model/tool usage, tokens, latency, and opt-in cost estimates.
Explore the live report → · Quick start · Input adapters · Privacy
SpanGarden answers the questions raw traces leave open: where did the run stall, which failed attempts were verifiably recovered, what did that recovery consume, and what can you safely share?
Agent traces are rich and awkward: nested spans, vendor-shaped attributes, retries that look like normal calls, and token numbers without context. SpanGarden turns those exports into one reproducible report on your machine—no collector, database, Docker image, account, or cloud upload.
SPANGARDEN SpanGarden agent trace report
──────────────────────────────────────────────────────────────
1 traces 6 spans 2 errors 2 recovered retries
2 retry candidates 1 loop signals
2.85s wall time p50 280ms p95 2.85s
3,420 in / 1,196 out tokens
CRITICAL PATHS
garden-demo-01 3.93s agent.plan_trip → chat final answer
RECOVERY LEDGER
tool:weather:execute_tool trace garden-demo-01 parent root
failed weather-1 190ms tokens unknown cost unknown
failed weather-2 250ms tokens unknown cost unknown
recovered by weather-3 (ok, 280ms) tokens unknown cost unknown
final delay 80ms recovery latency 680ms
USAGE
model orchid-2 1 calls 1.08s 0 err
model orchid-2-mini 1 calls 680ms 0 err
tool weather 3 calls 720ms 2 err
LOOP SIGNALS
repeated siblings tool:weather [weather-1 → weather-2 → weather-3]
| Question | Signal |
|---|---|
| Where did the run spend time? | Per-trace trees, wall time, p50/p95, and longest root-to-leaf critical path |
| Which failures actually recovered? | A conservative Recovery Ledger that requires matching trace, parent, operation identity, and non-overlapping chronology |
| Is the agent stuck? | Recursive-path and repeated-sibling loop signals plus retry candidates |
| Which tools and models dominate? | Deterministic call, error, latency, and token rollups |
| What might this run cost? | Estimates from your local pricing JSON only—never stale bundled prices |
| Can I share the report? | Sensitive-key and credential-pattern redaction is on by default; HTML embeds encoded data |
| Will this become infrastructure? | No daemon, no network calls, and zero runtime dependencies |
Get the result above from a built-in synthetic OpenTelemetry trace—no trace file, backend, or API key required:
npx --yes github:mockingbird777/spangarden --demoTurn the same demo into a portable interactive report, or analyze your own export:
npx --yes github:mockingbird777/spangarden --demo --format html --output spangarden-report.html
npx --yes github:mockingbird777/spangarden ./trace.json --format html --output report.htmlBoth commands run with Node.js 20+. Open the generated HTML file directly in a browser; it contains the report, styles, and interaction code in one file and loads no remote assets.
Or clone for repeat use:
git clone https://github.com/mockingbird777/spangarden.git
cd spangarden
npm ci
npm run build
node dist/src/cli.js examples/agent-run.json --pricing examples/pricing.jsonGenerate every report shape:
spangarden run.jsonl --format terminal
spangarden run.json.gz --format json --output report.json
spangarden run.json --format markdown --output investigation.md
spangarden run.json --format html --output investigation.html
spangarden run.json --output report.html # format inferred from the extension
cat run.jsonl | spangarden - --format jsonThe HTML report is one portable file with search, kind filters, critical-path highlighting, a Recovery Ledger, usage tables, and loop evidence. It loads no remote assets.
| Approach | Best at | What SpanGarden adds |
|---|---|---|
| Hosted observability backend | Always-on ingestion, retention, alerting, and team dashboards | A local investigation path with no service, account, or upload |
| Raw OTel viewer | Inspecting individual spans and attributes | Agent-aware critical paths, auditable recovered retries, model/tool rollups, token totals, and opt-in cost arithmetic |
| One-off scripts | Answering one question for one trace shape | Tolerant adapters, deterministic reports, default redaction, and four stable output formats |
| SpanGarden | Fast local diagnosis and shareable artifacts | Zero runtime dependencies and one self-contained interactive HTML report |
SpanGarden is an investigation tool, not a collector, long-term trace store, billing system, or production alerting platform.
- Parent/child span forests, including missing-parent and cyclic-input recovery
- Longest duration-weighted root-to-leaf path per trace
- Model and tool calls, errors, durations, input tokens, and output tokens
- High-confidence recovered retry sequences with failed attempts, the non-failed span that recovered them, failed duration, final retry delay, and recovery latency
- Repeated siblings as retry candidates; three or more as a loop signal
- Repeated operations along an ancestor path as a recursive-loop signal
- Trace wall time and p50/p95 span latency
- Optional per-model cost estimates with priced/unpriced token accounting
The Recovery Ledger is deliberately stricter than the general retry heuristic. It records a recovery only when all of this evidence is present:
- Every attempt has the same
traceIdand an explicit, identicalparentSpanId. - Every attempt has a stable normalized signature: an explicit tool identity, or a model identity plus operation identity. A matching span name alone is never enough.
- All spans in that operation group are serial. If any sibling intervals overlap or share the same start time, the entire group is omitted instead of guessing which call retried which.
- One or more
errorspans are followed by a span not marked as an error. Its originalokorunsetstatus remains visible; SpanGarden does not invent a success status. - Usable timing exists for the whole group. Timing-free groups are omitted with a report note.
The normalized operationSignature is tool:<tool>[:<gen_ai.operation.name>] for explicit tool spans and model:<model>:<gen_ai.operation.name-or-span-name> for explicit model spans. Its parts are trimmed, whitespace-normalized, and case-folded. Generic name-only spans do not get a Recovery Ledger signature.
Each entry keeps the failed span IDs and the recoveredBy span ID so the conclusion can be checked against the raw trace. failedDurationMs is the sum of failed-attempt durations. retryDelayMs is the gap from the last failure ending to the recovery attempt starting. recoveryLatencyMs runs from the first failure ending until the recovery attempt ends.
Token counts appear only when non-zero token evidence exists. Per-attempt and failed-work cost estimates appear only when a matching rate from the user-supplied local pricing file exists; terminal, Markdown, and HTML reports say unknown, while JSON omits unavailable optional fields. SpanGarden never backfills missing telemetry.
A shortened priced JSON entry looks like this:
{
"schemaVersion": "1.1",
"summary": { "recoveredRetries": 1 },
"recoveryLedger": [
{
"traceId": "trace-a",
"parentSpanId": "agent-run",
"operationSignature": "model:alpha:chat",
"failedAttempts": [
{
"spanId": "attempt-1",
"status": "error",
"durationMs": 240,
"inputTokens": 800,
"outputTokens": 20,
"estimatedCostUsd": 0.00176
}
],
"recoveredBy": {
"spanId": "attempt-2",
"status": "ok",
"durationMs": 310,
"inputTokens": 800,
"outputTokens": 90,
"estimatedCostUsd": 0.00232
},
"failedDurationMs": 240,
"retryDelayMs": 75,
"recoveryLatencyMs": 385,
"failedInputTokens": 800,
"failedOutputTokens": 20,
"estimatedFailedCostUsd": 0.00176
}
]
}Loop evidence is bounded to 1,000 findings and 100 span IDs per finding so adversarial repetition cannot multiply report size without limit; a report note appears when evidence is clipped.
Loop and general retry results are deliberately labeled signals and candidates. Recovery Ledger entries have stronger structural and chronological evidence, but still describe telemetry—not application intent.
SpanGarden accepts regular JSON, newline-delimited JSON, and gzip files ending in .gz. The adapter searches these common containers:
- OpenTelemetry
resourceSpans → scopeSpans → spans - Legacy
instrumentationLibrarySpans - Generic
spans,traces,runs,events,children, andsteps - Top-level span arrays and JSONL span records
Common snake_case and camelCase IDs, parents (including parent_run_id), timestamps, durations, statuses, models, tools, and token fields are normalized. Explicit durations are read from durationMs/duration_ms/latency_ms (milliseconds) or, as a fallback, durationNs/duration_ns (nanoseconds, converted to ms); when none are present, duration is derived from start and end timestamps. OpenTelemetry attribute arrays, arrayValue, and kvlistValue wrappers are decoded. Nested generic children inherit their enclosing trace and parent even when the enclosing span needs a generated ID. Unknown attributes stay in the report instead of being thrown away.
Minimal generic input:
{
"spans": [
{ "id": "run", "trace_id": "t1", "name": "agent.run", "start_time": 0, "duration_ms": 840 },
{ "id": "llm", "parent_id": "run", "trace_id": "t1", "name": "chat", "model": "my-model", "duration_ms": 620, "input_tokens": 900, "output_tokens": 220 }
]
}Timestamp support includes ISO strings, millisecond numbers, and OpenTelemetry Unix nanosecond fields with sub-millisecond precision where JavaScript numbers permit it. When timestamps are absent, analysis continues and adds a warning.
The packed ESM entry point exposes the same adapter, analysis, formatting, input, pricing, and redaction primitives used by the CLI:
npm install github:mockingbird777/spangardenimport { adaptSpans, analyzeSpans, formatReport } from "spangarden";
const report = analyzeSpans(adaptSpans(rawTrace));
const markdown = formatReport(report, "markdown");SpanGarden does not ship or fetch model prices. Supply rates you have reviewed:
{
"currency": "USD",
"models": {
"my-model": { "inputPerMillion": 2, "outputPerMillion": 8 },
"*": { "inputPerMillion": 0.5, "outputPerMillion": 1.5 }
}
}spangarden trace.json --pricing pricing.json --format html -o report.htmlExact model names are preferred, matching is case-insensitive, and * is an optional fallback. Unmatched tokens are reported as unpriced in aggregate usage and remain unknown in Recovery Ledger cost fields. Estimates are arithmetic aids, not billing records.
SpanGarden performs no network requests. Before output, it redacts sensitive-looking keys such as prompts/message content, authorization headers, token/key/email fields, passwords, cookies, private keys, and session IDs. It also masks common bearer credentials, OpenAI/GitHub/Slack/Google/AWS credentials, JWTs, email addresses, private-key blocks, and secret-bearing URL parameters inside strings. The same protection is applied to report metadata, aggregate labels, and credential-shaped identifiers; affected identifiers become stable one-way aliases so parent and critical-path links remain intact.
Token count fields are preserved. Redaction can miss domain-specific data, so inspect artifacts before sharing them. Markdown text and terminal controls are neutralized, and the self-contained HTML uses encoded data, DOM text nodes, and a restrictive Content Security Policy.
# Only for controlled local debugging; a warning is written to stderr.
spangarden trace.json --no-redactInput is bounded to 128 MiB after gzip decompression by default. Change the limit explicitly:
spangarden large.jsonl.gz --max-bytes 268435456spangarden <trace.json|trace.jsonl|trace.json.gz|-> [options]
-f, --format <type> terminal, json, markdown, or html (inferred from
the --output extension when omitted)
-o, --output <path> atomically write a file (mode 0600)
--pricing <path> local USD pricing JSON (maximum 1 MiB)
--title <text> report title
--max-bytes <n> decompressed input limit
--no-redact disable default redaction
--fail-on-errors write the report, then exit with status 2 on error spans
--demo analyze a built-in synthetic OTel trace
Machine-readable reports use schema version 1.1; the new summary.recoveredRetries and recoveryLedger fields extend the 1.0 report shape. Results are sorted and the report timestamp is anchored to trace data, making repeated analysis byte-for-byte reproducible for the same inputs and options.
npm ci
npm run check
npm test
npm run docs
npm auditThe implementation uses strict TypeScript and Node's standard library. See CONTRIBUTING.md, SECURITY.md, and the changelog.
Useful first contributions include a minimized synthetic fixture for an unsupported trace shape, an adapter regression test, a redaction pattern with safe look-alikes, or an accessibility improvement to the HTML report. Please do not attach production prompts, credentials, customer traces, or identifying telemetry.
Start with a feature request or read the contributor guide. Every adapter change should include a focused fixture and deterministic test.
- Streaming aggregation mode for traces larger than the in-memory analysis boundary
- Additional semantic-convention adapters and adapter diagnostics
- Critical-path self-time alongside duration-weighted chain analysis
- Diff mode for comparing two agent runs
If SpanGarden makes a difficult agent run legible, consider starring it—and share a synthetic fixture for the next adapter. 🌿