Skip to content

tracing

ObservabilityLoggingRust

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-filter

Getting started

The smallest useful thing you can do with it, and what each part means.

Spans and structured events
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();
#[instrument] wraps the function in a span automatically, so every event inside carries book.id — even across await points where the task was suspended and resumed elsewhere.

Advanced usage

Where the library earns its place over a simpler alternative.

Filtering and OpenTelemetry export
// 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.
The layered registry is tracing's real strength: one instrumentation produces console logs, JSON for a log platform and spans for a tracing backend simultaneously.

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.