What it is
tracing provides structured, contextual diagnostics for Rust, recording spans of time as well as events — which is what makes async code debuggable.
Events are point-in-time records; spans cover a period and nest. A subscriber decides how to filter and emit them.
Installation
cargo add tracing tracing-subscriber --features tracing-subscriber/env-filterGetting started
The smallest useful thing you can do with it, and what each part means.
rust
use tracing::{info, warn, instrument};
#[instrument(skip(db), fields(book.id = %id))]
async fn get_book(db: &Pool, id: i64) -> Result<Book> {
info!("looking up book"); // inherits the span's fields
let book = db.find(id).await?;
if book.is_none() {
warn!(found = false, "no such book");
}
Ok(book)
}
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env()) // RUST_LOG
.json()
.init();Advanced usage
Where the library earns its place over a simpler alternative.
rust
// Per-module levels: quiet dependencies, verbose your own code.
let filter = EnvFilter::new("info,my_app=debug,sqlx=warn,hyper=warn");
use tracing_subscriber::prelude::*;
tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer())
.with(tracing_opentelemetry::layer().with_tracer(tracer))
.init();
// Spans now become distributed traces as well as log lines.Errors and fixes
The failures you are most likely to hit, and what actually resolves them.
- No output at all
- No subscriber was initialised, or RUST_LOG filters everything out. Call tracing_subscriber::fmt().init() early in main.
- Secrets appear in the logs
- #[instrument] records every argument. Add skip or skip_all and name only the fields you want.
Best practices
- Use #[instrument] on async functions; without spans, interleaved logs are unreadable.
- Add skip() for large or sensitive arguments — #[instrument] records them all by default.
- Set per-module filters so a chatty dependency does not drown your own output.
- Prefer structured fields over formatted strings so a log platform can index them.
Background
Why it exists, and what it was reacting to.
From the Tokio project, tracing exists because plain logging breaks down in async code: tasks interleave, so a sequence of log lines no longer tells a coherent story. Spans restore that structure.
