Structured tracing for applications. JSONL files, hierarchical spans, zero infrastructure.
from traqo import Tracer, trace
from pathlib import Path
@trace
def classify(text: str) -> str:
response = llm.chat(text)
return response
with Tracer(Path("traces/run.jsonl"), input={"query": "Is this a bug?"}):
result = classify("Is this a bug?")Your traces are just .jsonl files. Read them with grep, query them with DuckDB, or hand them to an AI assistant.
- Zero infrastructure -- no server, no database, no account.
pip install traqoand go. - AI-first -- JSONL is text. AI assistants read your traces directly, no browser needed.
- Hierarchical spans -- not flat logs. Reconstruct the full call tree across functions and files.
- Everything is a span -- LLM calls, DB queries, HTTP requests. All spans with metadata.
- Minimal dependencies -- one runtime dep (
zstandard). Integrations are optional extras. - Transparent -- traces are portable files. No vendor lock-in, no proprietary format.
pip install traqo # Core (requires zstandard)
pip install traqo[openai] # + OpenAI integration
pip install traqo[anthropic] # + Anthropic integration
pip install traqo[langchain] # + LangChain integration
pip install traqo[gemini] # + Google Gemini integration
pip install traqo[all] # Everythingfrom traqo import Tracer, trace
from pathlib import Path
@trace
def summarize(text: str) -> str:
# your logic here
return summary
@trace
def pipeline(docs: list[str]) -> list[str]:
return [summarize(doc) for doc in docs]
with Tracer(
Path("traces/my_run.jsonl"),
input={"docs": ["doc1", "doc2"]},
tags=["production"],
) as tracer:
results = pipeline(["doc1", "doc2"])
tracer.set_output({"count": len(results)})@trace works with sync/async functions and generators. It detects and handles all automatically.
from traqo.integrations.openai import traced_openai
from openai import OpenAI
client = traced_openai(OpenAI(), operation="summarize")
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Summarize this..."}],
)
# Token usage, model, input/output all captured automatically as span metadataWorks the same way for Anthropic, Gemini, and LangChain:
from traqo.integrations.anthropic import traced_anthropic
from traqo.integrations.gemini import traced_gemini
from traqo.integrations.langchain import traced_modelAll integrations auto-capture token usage, model parameters, streaming with TTFT, and tool calls.
from traqo import Tracer, LLM, TOOL
with Tracer(Path("traces/run.jsonl"), tags=["prod"]) as tracer:
with tracer.span(
"classify",
input={"text": "Is this a bug?"},
metadata={"model": "gpt-4o", "provider": "openai"},
tags=["llm"],
kind=LLM,
) as span:
result = call_llm(...)
span.set_metadata("token_usage", {"input_tokens": 100, "output_tokens": 50})
span.set_output(result)Kind constants: LLM, TOOL, RETRIEVER, CHAIN, AGENT, EMBEDDING, GUARDRAIL (or use any string).
from traqo import trace, update_current_span
@trace
def classify(text: str) -> str:
update_current_span(metadata={"confidence": 0.95, "model": "gpt-4o"})
return resultupdate_current_span() is a convenience helper — no-op when no span is active. For full control, use get_current_span() directly.
# Last line is always trace_end with summary stats
tail -1 traces/my_run.jsonl | jq .
# All LLM spans
grep '"kind":"llm"' traces/my_run.jsonl | jq .
# Filter by tag
grep '"tags"' traces/my_run.jsonl | jq .
# Errors
grep '"status":"error"' traces/**/*.jsonl
# Token usage from span metadata
grep '"token_usage"' traces/**/*.jsonl | jq '.metadata.token_usage'Trace Claude Agent SDK sessions with an async context manager. The Stop hook converts the session transcript into a traqo trace automatically.
from claude_agent_sdk import query, ClaudeAgentOptions
from traqo.integrations.claude_agent_sdk import traqo_agent
async with traqo_agent("code-review", output_dir="./traces", tags=["review"]) as hooks:
async for msg in query(
prompt="Review this PR for security issues",
options=ClaudeAgentOptions(hooks=hooks),
):
print(msg)Nest multiple agents inside a parent trace for pipeline orchestration:
with Tracer(Path("traces/pipeline.jsonl"), tags=["ci"]) as tracer:
async with traqo_agent("code-review", tags=["review"]) as hooks:
async for msg in query(prompt="Review", options=ClaudeAgentOptions(hooks=hooks)):
...
async with traqo_agent("test-gen", tags=["testing"]) as hooks:
async for msg in query(prompt="Generate tests", options=ClaudeAgentOptions(hooks=hooks)):
...The parent trace_end rolls up token usage and span counts from all child agents.
Give your AI coding assistant full knowledge of traqo traces — reading, querying, instrumenting, and launching the UI.
npx skills add Cecuro/traqo --yes --globalWorks with Claude Code, Cursor, Copilot, Codex, and other agents. Once installed, the agent can navigate traces, extract token usage, find errors, and add tracing to your code without further guidance.
Convert Claude Code session transcripts into traqo traces — one trace per session with turns, LLM calls, tool calls, and subagent hierarchy.
# Sync all sessions
traqo cc-sync --all --output-dir ./traces
# Sync a single session
traqo cc-sync path/to/session.jsonl
# View the results
traqo ui ./tracesAs a Claude Code Stop hook (~/.claude/settings.json):
{
"hooks": {
"Stop": [{ "type": "command", "command": "traqo cc-sync --hook" }]
}
}Browse and inspect traces in your browser. Uses Python's built-in HTTP server.
traqo ui ./traces # Serve traces on http://localhost:7600
traqo ui ./traces --port 8080 # Custom port
traqo ui s3://my-bucket/traces/ # Browse traces from S3
traqo ui gs://my-bucket/traces/ # Browse traces from GCS
python -m traqo ui ./traces # Alternative invocationCloud sources list files instantly via API, then download on click. Previously viewed traces show full summary data (duration, stats, tags) on the next page load.
Features: folder navigation, search/filter, span tree with waterfall timing, JSON viewer with syntax highlighting, token usage visualization, keyboard shortcuts (Escape to go back, ? for help).
Tracer(path, *, input=None, metadata=None, tags=None, thread_id=None, capture_content=True, backends=None)
Creates a trace session writing to a JSONL file. Use as a context manager.
with Tracer(
Path("traces/run.jsonl"),
input={"query": "What is the weather?"},
metadata={"run_id": "abc123"},
tags=["production", "chatbot"],
thread_id="conv-456",
capture_content=False, # Integrations omit LLM input/output
) as tracer:
result = my_pipeline()
tracer.set_output({"response": result})| Parameter | Type | Default | Description |
|---|---|---|---|
path |
Path |
required | JSONL file path. Parent dirs created automatically. |
input |
Any |
None |
Trace input, written to trace_start. |
metadata |
dict |
{} |
Arbitrary metadata written to trace_start. |
tags |
list[str] |
[] |
Tags for filtering/categorization, written to trace_start. |
thread_id |
str |
None |
Conversation/thread grouping ID, written to trace_start. |
capture_content |
bool |
True |
If False, integration wrappers omit LLM message inputs/outputs. The @trace decorator has separate capture_input/capture_output flags. |
backends |
list[Backend] |
None |
Storage backends notified on events and trace completion. The local JSONL file is always written regardless. |
Methods:
| Method | Description |
|---|---|
span(name, *, input=, metadata=, tags=, kind=) |
Span context manager. Yields a Span object. |
set_output(value) |
Set trace-level output (written to trace_end). |
log(name, data) |
Write a custom event. |
child(name, path) |
Create a child tracer writing to a separate file. |
Mutable handle yielded by tracer.span(). Set output and metadata during execution.
with tracer.span("my_step", input=data, tags=["important"], kind="tool") as span:
result = do_work()
span.set_output(result)
span.set_metadata("latency_ms", 42)
span.update_metadata({"extra": "info"})| Method | Description |
|---|---|
set_output(value) |
Set span output (written to span_end) |
set_metadata(key, value) |
Set a metadata key |
update_metadata(dict) |
Merge a dict into metadata |
Decorator that wraps a function in a span. Works with sync/async functions and generators.
@trace
def my_step(data: list) -> dict:
return process(data)
@trace("custom_name", capture_input=False, kind=TOOL)
def sensitive_step(secret: str) -> str:
return handle(secret)
@trace(ignore_arguments=["password"], kind=TOOL)
def login(user: str, password: str) -> bool:
return authenticate(user, password)Parameters: name, capture_input, capture_output, ignore_arguments, metadata, tags, kind.
When no tracer is active, @trace is a pure passthrough with zero overhead.
Returns the current active span, or None.
Convenience helper to update the active span. No-op when no span is active.
from traqo import trace, update_current_span
@trace
def my_function(text: str) -> str:
update_current_span(metadata={"custom_key": "custom_value"})
return process(text)Returns the active tracer for the current context, or None.
from traqo import get_tracer
tracer = get_tracer()
if tracer:
tracer.log("checkpoint", {"count": len(results)})import traqo
traqo.disable() # All tracing becomes no-op
traqo.enable() # Re-enableOr via environment variable: TRAQO_DISABLED=1
For concurrent agents or workers that produce many events. Each child writes to its own file, linked to the parent.
with Tracer(Path("traces/pipeline.jsonl")) as tracer:
child = tracer.child("reentrancy_agent", Path("traces/agents/reentrancy.jsonl"))
with child:
run_agent(...)The parent trace records child_started / child_ended events and includes child summaries in trace_end.
Every line is a self-contained JSON object. Five event types:
| Type | When | Key Fields |
|---|---|---|
trace_start |
Tracer enters | tracer_version, input, metadata, tags, thread_id |
span_start |
Span begins | id, parent_id, name, input, metadata, tags, kind |
span_end |
Span ends | id, duration_s, status, output, metadata, tags, kind |
event |
Custom checkpoint | name, data |
trace_end |
Tracer exits | duration_s, output, stats, children |
The kind field categorizes spans (e.g. "llm", "tool", "retriever"). The tags field is a list of strings for filtering. Both are omitted when not set.
The metadata dict is the universal extension point. LLM-specific data like model, provider, and token_usage are stored there.
-- All LLM spans with token usage
SELECT metadata->>'model' as model,
count(*) as calls,
sum((metadata->'token_usage'->>'input_tokens')::int) as total_in,
sum((metadata->'token_usage'->>'output_tokens')::int) as total_out,
avg(duration_s) as avg_duration
FROM read_json('traces/**/*.jsonl')
WHERE kind = 'llm'
GROUP BY model;
-- All traces for a conversation thread
SELECT * FROM read_json('traces/**/*.jsonl')
WHERE thread_id = 'conv-123'
AND type = 'trace_start';MIT