What is tracing? (2019 rustc 1.65+)
Rust crate for structured, contextual logging and distributed
tracing.
Unlike plain text log lines, it records events (log messages with
typed fields) inside spans (named scopes
that follow a request or task through async code).
It is widely used with async Rust (tokio,
axum, tonic) because
spans survive across
.await points.
Crates: tracing (macros + span API),
tracing-subscriber (formatters, filters, layers),
tracing-appender (file rotation). Often paired with
tracing-opentelemetry to export traces to Jaeger, Tempo,
etc.
Simple example
use tracing::{info, instrument};
use tracing_subscriber;
#[instrument] // creates a span named "log" with args user_id
async fn log(user_id: u64) {
info!(user_id, "processing request");
// ... work ...
info!("request finished");
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter("info")
.init();
log(42).await;
}
Sample output (JSON mode is also supported):
2026-07-18T10:00:00.123Z INFO log{user_id=42}: processing request
2026-07-18T10:00:00.125Z INFO log{user_id=42}: request finished
The span name and user_id field appear on every line inside
log, so you can grep or query logs by request context
without repeating fields in every message.
Spans vs events
| Concept | Analogy | Macro examples |
|---|---|---|
| Event | A single log line | info!, warn!, error! |
| Span | A scope / operation with start and end | #[instrument], span!(...) |
Spans can be nested (HTTP handler → DB query → cache lookup). Subscribers can export span trees for latency analysis, not just flat text logs.
How tracing suitable for Async runtimes(eg: tokio)
What async does?
A single OS thread may run
task A -> task B (.await) -> task A — every time a
task hits .await and is parked. .await loses
track of which request or task produced a message.
How tracing helps here? A span is attached to the
logical task, not just the thread.
When Tokio polls a future again after an .await, the span
context is re-entered, so every log line inside that handler
still carries the same request id, trace id, and parent span — even if
the work ran on three different threads.
Why log and slog poor fit across async
runtimes
We can call
log::info! or slog::info! from an
async fn. The problem is
context does not follow .await automatically.
Which is best? Which is newest?
Newest (among these): tracing (2019). Active
development continues in the Tokio project.
Best depends on your project:
| Scenario | Recommendation |
|---|---|
| Small CLI, quick debug output | log + env_logger |
Need Log4j-style config files on log |
log4rs |
| Structured JSON logs, sync or legacy codebase | slog (still solid; check team familiarity) |
| Async web/service, request context, OpenTelemetry | tracing + tracing-subscriber |
There is no single “best” crate — log remains the common
denominator (many libraries emit log events). Production
async services in 2026 usually standardize on tracing and
bridge older log output with tracing-log when
needed.