tracing-console
An interactive TUI for inspecting tracing spans coming off a live server, plus the cache and RPC layer that gets them to you.
The tracing tie-in, tracing-cache, is made to have the lowest possible overhead while disabled. It's expected that you will
have nothing connected to your tracing port almost all the time. In the rare case you need it, you can connect, enable briefly,
(or for longer with a non-100% sampling rate) and then disable. In this way you can capture detailed traces during events
without needing a bunch of infrastructure.
The downside is that if you wanted to see something that already happened, you can't. This is for looking at problems that are happening!
Currently this repo doesn't offer opentelemetry traces.
What this is for
This is suited for short-lived APIs — request-response work, RPC handlers, background jobs that complete in milliseconds to seconds. The cache only commits spans to its shared map when they close, and the protocol only streams closed spans. Anything still in flight is invisible to the console.
That makes it a poor fit for:
- Long-running background spans (event loops, daemons, things that stay open for hours) — they never reach the cache.
- Low-value logging that has no span structure — the console can stream events, but the UI is span-oriented.
- Distributed observability — This is a single-host debugging console, not a distributed tracing backend.
What it is good at: attaching to a running server, watching the live shape of recent requests, finding outliers, drilling into one trace's full event/span tree, and disabling again without leaving any undue overhead behind.
Getting started — integrating with your server
1. Add the dependencies
[]
= "1"
= "0"
= "0"
= "0"
2. Stand up the cache + serve
use Arc;
use ;
async
3. Instrument with regular tracing macros
You put your root spans carefully at the base of your APIs and the units of work you want to trace.
For async code, use #[tracing::instrument] or future.instrument(span).await instead of let _g = span.enter().
An enter() guard sits on the thread until it goes out of scope, including across every .await in between; when the
executor parks the task and runs something else on the same worker thread, that unrelated work ends up captured as a
child of your span. That's really confusing.
use ;
// Outer handler — root of one trace per request. `parent = None`
// explicitly anchors it so it can't inherit whatever happened to
// be on the caller's stack. `skip(req)` keeps the request body
// out of the span fields; we pull the bits we want via `fields`.
async
async
If you'd rather build the span by hand, use .instrument:
let span = info_span!;
do_work.instrument.await
let _g = span.enter() is fine in synchronous code where no
.await happens inside the guard's scope.
4. Connect from the TUI
You'll land in the stacks view with nothing visible (level defaults to Off). Press Shift+I (or Shift+D, Shift+T) to ask the server to start recording at Info / Debug / Trace. Spans will start showing up as they close.
Keyboard cheat sheet
The UI highlights available options contextually. Here are some of the main controls:
| Where | Key | Action |
|---|---|---|
| any | Shift+O/I/D/T |
request cache level Off/Info/Debug/Trace |
| any | Shift+C |
open chance % modal (sampling rate) |
| stack / graph / explore | s g e |
jump between top-level views |
| stack | ↑/↓, →, ←, Enter |
navigate, expand, collapse, expand-all |
| graph | a w l m u |
edit agg / window / lookback, toggle metric / time labels |
| explore | ↑/↓, ←/→, i |
row cursor, cycle sort column, invert direction |
| explore | / |
search across span/event names + field values |
| explore | Enter |
open the trace-detail view of the selected row |
| trace detail | ↑/↓, ←/→ |
cursor, collapse / expand subtree |
| any | Esc |
pop one level up |
| any | q |
quit |
Configuration
- Cache capacity (
SpanCache::with_predicate(capacity, …)) — the max number of closed spans in the ring buffer. Older spans evict FIFO. Too small and your client will get gaps. Too large and you'll just be wasting memory for no reason while you're tracing. CacheConfig::pending_batch(default8) — per-thread closed-span buffer before flushing. Lower = more responsive at low traffic, more sends at high traffic.CacheConfig::channel_capacity(default65_536) — buffer between producer threads and the driver.- Sampling rate (
ChancePredicate) — live-tunable viaShift+Cin the TUI, or programmatically viachance_handle. - Cache level (
LevelPredicate) — live-tunable viaShift+letterin the TUI, or programmatically vialevel_handle. The server starts at whatever you initialized; starting up withOffis recommended.
Examples
tracing-console-host/examples/fs_listing_api.rs— a self-contained host that traces a directory walk.tracing-console-host/examples/synth_load.rs— a synthetic-load generator used for bench testing.
# terminal 1 — start the example "server"
# terminal 2 — connect the TUI
Limitations
Spans only become visible when they close. In-flight long-running spans don't show up until they end. When you enable tracing, you are doing it to a live system in who-knows what state. You'll get some orphaned spans at the beginning, and some incomplete spans at the end.
This tool is intended for quick live analysis, not rigorous archival. There is no persistence. All caching is done in bounded ring buffers in-memory, and lost on restart.