Rust tracing vs log: Which Logging Crate Should You Use in 2026?

Max WellsMax WellsFounder of Rustify

A direct comparison of Rust's two main logging approaches, tracing and log, with clear guidance on which to choose for your project in 2026. Covers structured logging, async contexts, performance, and the ecosystem around each.

If you are building a service, API, or any async production backend in 2026, tracing is usually the right default. If you are writing a library, CLI, or simpler application, log is still a perfectly valid choice.

By Max Wells, updated August 2026

TL;DR: Use tracing for web services, APIs, and async applications. Use log for libraries, CLI tools, and simple applications where structured context is not needed. The community has largely settled on tracing for production services; log remains valid for simpler use cases and libraries.

  • tracing: structured spans, async-aware context, OpenTelemetry integration, production default
  • log: simple, minimal, widely compatible, correct for libraries
  • Migration: going from log to tracing is easy (tracing is backward compatible with log)
  • Performance: both are zero-cost when disabled; tracing has small overhead per span when enabled
  • 2026 standard: most new Rust services use tracing + tracing-subscriber

Who Should Read This?

This comparison is for Rust developers starting a new project or evaluating their current logging setup who want a clear, opinionated answer about which approach to use. The log vs tracing debate comes up regularly in Rust communities, and the right answer depends on your use case. If you are building an API, a microservice, or anything that runs in production and needs debugging context, this article gives you the framework for the decision and enough code examples to evaluate both against your specific requirements.

Bottom line: Best default for services and async apps: tracing. Best default for libraries and simple tooling: log.


Which One Should You Choose in 2026?

Choose tracing if your application has async workflows, request context, or production observability needs. Choose log if your application is simple, synchronous, or intended to be embedded as a library inside someone else's stack.

Use this quick filter:

  1. Choose tracing if you are building an API, worker, background service, or anything where request/task context matters.
  2. Choose log if you are writing a library, a small CLI, or an application where flat log lines are enough.
  3. Do not over-optimize for future observability if the app is tiny today. Do not under-invest in context if the app already has concurrent or production debugging complexity.

What Are These Crates and What Problem Do They Solve?

log: The Original

log is Rust's original logging facade, part of the core ecosystem since Rust 1.0. It provides a simple, macro-based API:

use log::{info, warn, error, debug};
 
fn process_request(id: u64) {
    info!("Processing request {}", id);
    // ... do work ...
    warn!("Request {} took longer than expected", id);
}

log requires a backend (subscriber) to actually output logs: env_logger, pretty_env_logger, fern, etc.

tracing: Structured and Async-Aware

tracing extends the logging concept to include spans: structured, hierarchical contexts that track what code is executing and what the context is:

use tracing::{info, warn, instrument, span, Level};
 
#[instrument(fields(user_id = %user_id))]
async fn process_request(user_id: u64) {
    info!("Processing request");
    // All events inside this function automatically carry user_id context
    warn!("Took longer than expected");
}

tracing requires tracing-subscriber (or another subscriber) to output events.


The Core Difference: Flat Events vs Structured Spans

log produces flat event records. tracing produces a tree of spans with events inside them.

With log, every log line is independent:

INFO  2026-04-24 14:23:01 - Processing request 42
WARN  2026-04-24 14:23:03 - Took longer than expected
INFO  2026-04-24 14:23:01 - Processing request 43

When requests are interleaved in async code, understanding which log line belongs to which request requires including the ID in every single log message manually.

With tracing, context is attached to spans:

INFO request{user_id=42}: Processing request
WARN request{user_id=42}: Took longer than expected
INFO request{user_id=43}: Processing request

The user_id context is attached to the span and automatically included in all events within that span, even nested function calls, without manually threading it through every function signature.


When Async Changes Everything

In synchronous code, log works fine. In async code, tracing is nearly essential.

Consider an async web server handling 1000 concurrent requests. With log:

// You must manually include request_id in EVERY log call
async fn handler(request_id: Uuid, ...) {
    log::info!("request_id={}: handling request", request_id);
    let result = some_async_operation(request_id).await;
    log::info!("request_id={}: completed", request_id);
}
 
async fn some_async_operation(request_id: Uuid) {
    log::debug!("request_id={}: starting operation", request_id);  // must pass id down
}

With tracing:

#[instrument(fields(request_id = %request_id))]
async fn handler(request_id: Uuid, ...) {
    info!("handling request");  // request_id automatically included
    some_async_operation().await;
}
 
#[instrument]  // parent span context propagated automatically
async fn some_async_operation() {
    debug!("starting operation");  // request_id context still present
}

In async Rust, multiple tasks are interleaved on the same thread. The tracing crate maintains span context correctly across .await points. log cannot do this; context must be manually threaded.


3 spots open this month → Check if you are eligible.

We help experienced developers transition into Rust roles at €80K–€150K+ in Europe or $130K–$200K+ in the US.

Code Comparison: Full Setup

log + env_logger Setup

// Cargo.toml
// log = "0.4"
// env_logger = "0.11"
 
fn main() {
    env_logger::init();  // reads RUST_LOG env var
 
    log::info!("Application starting");
    process_something();
}
 
fn process_something() {
    log::debug!("Processing...");
    log::error!("Something went wrong: {}", "error details");
}

Run with: RUST_LOG=debug cargo run

tracing + tracing-subscriber Setup

// Cargo.toml
// tracing = "0.1"
// tracing-subscriber = { version = "0.3", features = ["env-filter"] }
 
fn main() {
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();
 
    tracing::info!("Application starting");
    process_something(42);
}
 
#[tracing::instrument]
fn process_something(id: u64) {
    tracing::debug!("Processing...");
    tracing::error!("Something went wrong");
    // Both events automatically carry id=42 from the span
}

Run with: RUST_LOG=debug cargo run

tracing for Async Web API (Production Pattern)

// In main.rs or setup
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
 
fn init_tracing() {
    tracing_subscriber::registry()
        .with(tracing_subscriber::EnvFilter::new(
            std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()),
        ))
        .with(tracing_subscriber::fmt::layer().json())  // JSON output for log aggregation
        .init();
}
 
// In Axum:
let app = Router::new()
    .route("/users/:id", get(get_user))
    .layer(tower_http::trace::TraceLayer::new_for_http());  // Automatic request tracing

Ecosystem Comparison

logtracing
Backend optionsenv_logger, fern, pretty_env_logger, slogtracing-subscriber, tracing-appender
JSON outputvia fern or customtracing-subscriber fmt().json()
OpenTelemetryNot directlytracing-opentelemetry
Jaeger / ZipkinNot directlytracing-opentelemetry + OTLP
Axum integrationManualtower-http::TraceLayer
Tokio integrationN/AFirst-class (tracing was created by Tokio team)
Log filteringRUST_LOG env varEnvFilter (same syntax)
Async context
Library compatibility✅ Universal✅ (tracing has a log compatibility layer)

Which Should You Use?

Use tracing When:

  • Building a web service or API (Axum, Actix-web, any async framework)
  • Running in an async context (anything with Tokio)
  • You want OpenTelemetry integration for distributed tracing
  • Production logs will be aggregated (JSON output to Datadog, Loki, CloudWatch)
  • You want automatic request context propagation

Use log When:

  • Writing a library crate (the convention is for libraries to use log, not tracing: let consumers choose the backend)
  • Building a simple CLI tool where structured spans add no value
  • You need maximum ecosystem compatibility (virtually every crate supports log)
  • The application is synchronous and simple

The Good News: tracing Supports log

If you use tracing, you can enable the log compatibility layer:

tracing = { version = "0.1", features = ["log"] }

This means any crate that uses log::info!() will have its events automatically captured by tracing-subscriber. You get unified logging even when your dependencies use log. This is why new projects can use tracing exclusively without losing output from log-based dependencies.


Performance Considerations

Both are zero-cost when disabled. tracing has small overhead per active span.

When a log level is disabled (e.g., debug logs in production):

  • Both log and tracing macros compile down to a level check; if the level is not enabled, essentially zero overhead.

When enabled:

  • log: allocates a string per event (format! under the hood)
  • tracing: creates a span record (small allocation) plus event records. The overhead is measurable in micro-benchmarks but negligible in I/O-bound services.

For CPU-bound hot loops where logging has measurable overhead: both should be disabled in the hot path, or use tracing's enabled!() macro to conditionally skip expensive event construction.


Frequently Asked Questions

Yes, and it is easy. tracing provides the same macros (info!, debug!, warn!, error!) so you can replace use log::info with use tracing::info with no other changes. Then add #[instrument] to functions where you want automatic span context. The migration is incremental.

Convention: libraries should use log. The reason: if a library uses tracing, it forces all users of the library to pull in the tracing crate. If it uses log, users can choose any backend. Many libraries now use tracing and accept this dependency, but log is more conservative. For a library targeting wide compatibility: log. For a library used in async services (like reqwest, which uses tracing): tracing is fine.

slog was an earlier attempt at structured logging in Rust. It is still used in some codebases but has fallen behind tracing in ecosystem support and community adoption. New projects should use tracing rather than slog.

OpenTelemetry is a standard for distributed tracing that tracks requests as they flow across multiple services. If you have multiple microservices and want to trace a request from the API gateway through service A to service B, you need distributed tracing. tracing-opentelemetry integrates Rust's tracing spans with the OpenTelemetry standard, enabling export to Jaeger, Zipkin, Datadog, or any OTLP-compatible backend. For a single-service application, you do not need OpenTelemetry; tracing-subscriber with JSON output is sufficient.


Sources

Ready to Land a $120k+ Rust Job in the US or Europe?