TL;DR:
tracingis a structured logging framework built for async Rust. Unlikelog(which only emits flat log lines),tracinghas spans, units of work with a start and end, that nest across async.awaitpoints. This lets you track what a request is doing across tasks and threads. It's the standard observability library for Tokio-based applications.
What Is the Difference Between tracing and log?
log emits flat, unstructured text lines. tracing emits structured events inside nested spans, it understands async context, task IDs, and structured key-value fields.
// log; simple, no structure
log::info!("processing request for user {}", user_id);
// tracing; structured fields, spans, async-aware
tracing::info!(user_id, request_id, "processing request");tracing is a superset: it implements the log crate's macros, so libraries using log automatically integrate with a tracing subscriber.
What Are Spans and Events?
A span represents a period of time (e.g., one HTTP request, one DB query). Events are instant log points within a span. Spans nest, a request span may contain a DB span.
use tracing::{info, instrument, span, Level};
// #[instrument] automatically creates a span for the function
#[instrument(fields(user_id))]
async fn handle_request(user_id: i64) -> Result<Response, Error> {
info!("handling request"); // event inside the span
let user = fetch_user(user_id).await?; // inner span created by fetch_user
Ok(build_response(user))
}
#[instrument]
async fn fetch_user(id: i64) -> Result<User, Error> {
info!("fetching from DB");
// ... db query
}Output (with tracing-subscriber):
INFO handle_request{user_id=42}: handling request
INFO handle_request{user_id=42}:fetch_user{id=42}: fetching from DBHow Do You Set Up tracing?
Add tracing and a subscriber (typically tracing-subscriber) to your Cargo.toml, then initialize in main().
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
#[tokio::main]
async fn main() {
// Initialize; reads RUST_LOG env var for level filtering
tracing_subscriber::registry()
.with(EnvFilter::from_default_env())
.with(tracing_subscriber::fmt::layer())
.init();
tracing::info!("server starting");
run_server().await;
}Set the log level via environment variable:
RUST_LOG=debug cargo run # all debug+ events
RUST_LOG=my_app=info,sqlx=warn cargo run # per-crate levelsHow Does tracing Work With Axum?
tower-http's TraceLayer adds automatic request/response spans to every Axum route, no manual instrumentation required for HTTP logging.
use axum::Router;
use tower_http::trace::TraceLayer;
let app = Router::new()
.route("/users/:id", get(get_user))
.layer(TraceLayer::new_for_http());This automatically logs request method, path, status code, and duration for every request.
What Are Subscribers and Layers?
A subscriber collects the spans and events tracing emits. Layers are composable, you can stack JSON output, filtering, and OpenTelemetry export together.
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
tracing_subscriber::registry()
.with(EnvFilter::from_default_env()) // filter by level/target
.with(tracing_subscriber::fmt::layer().json()) // JSON output
// .with(tracing_opentelemetry::layer()) // OpenTelemetry export
.init();Popular subscriber backends:
tracing-subscriber, human-readable or JSON output to stdouttracing-opentelemetry, export to Jaeger, Tempo, etc.tracing-appender, non-blocking file logging
Frequently Asked Questions
Use tracing, it's backward compatible with log (libraries using log work seamlessly with a tracing subscriber). For libraries, use tracing events without setting up a subscriber, let the application decide how to collect them.
Yes, this is tracing's main advantage over log. Spans are async-aware: #[instrument] correctly tracks context across .await points even when tasks are suspended and resumed on different threads.
Minimal when events are filtered out. tracing uses a fast filtering mechanism, if the level is disabled, instrumented functions have near-zero overhead. Active spans have small per-event allocations.
Yes, via tracing-opentelemetry. Once spans are in OpenTelemetry format, they can be exported to any compatible backend (Jaeger, Tempo, Datadog, Honeycomb).
Sources
- tracing crate docs: API reference
- tracing-subscriber docs: Subscriber setup
- Tokio blog, tracing introduction
Related Glossary Terms
- Tokio: The async runtime tracing is designed to work with
- Axum:
TraceLayerintegrates tracing into HTTP middleware - Async/Await: Spans track context across async suspension points
Keep Reading
- Rust on AWS Lambda: structured tracing in production serverless functions
- Rust at Cloudflare: observability at the edge
- Rust at FAANG: production observability at AWS and Microsoft scale
