---
title: How to Use tracing-subscriber for Log Output
summary: Initialize the logger by calling `rustc_log::init_logger` with a configuration derived from an environment variable like `LOG`. This sets up `tracing-subscriber` to handle log output based on the filter rules you define.
lang: en
slug: how-to-use-tracing-subscriber-for-log-output
published: 2026-04-17T14:12:14+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-use-tracing-subscriber-for-log-output/
---

# How to Use tracing-subscriber for Log Output



## When your application needs to speak

You write a Rust service. It works perfectly on your machine. You deploy it to production, and it silently drops a connection. You add `println!` statements everywhere, rebuild, redeploy. The logs become a wall of unstructured text. You strip them out, lose visibility, and repeat the cycle. This happens because `println!` is a blunt instrument. It prints directly to stdout, ignores log levels, blocks the calling thread, and gives you zero control over formatting. Rust gives you a better path: the `tracing` ecosystem. The `tracing` crate emits events. `tracing-subscriber` catches them, filters them, and formats them for output.

## What tracing-subscriber actually does

Rust's `tracing` crate uses a publish-subscribe architecture. Your code publishes events. A subscriber listens to them. Without a subscriber, your events vanish into the void. The subscriber attaches to the current thread or process, intercepts every event, checks it against your rules, and either drops it or writes it to a console, file, or external service.

Think of `tracing` as a network of sensors and `tracing-subscriber` as the monitoring dashboard. The sensors fire events constantly. The dashboard decides which ones to display, how to format them, and where to send them. The subscriber is not a single monolithic block. It is built from layers. Each layer handles one responsibility: filtering, formatting, routing to a file, or shipping to a metrics backend. You stack layers to build a pipeline. The pipeline runs on a global dispatcher. The dispatcher routes events to every registered layer in order.

This architecture keeps your code fast. Filtering happens before formatting. If an event fails the filter, the subscriber drops it immediately. No string allocation occurs. No I/O happens. The CPU moves on.

## The minimal setup

You do not need a complex configuration to get started. A few lines attach a default subscriber to your process. The standard approach reads an environment variable to control log levels at runtime.

```rust
use tracing::info;
use tracing_subscriber::{EnvFilter, fmt};

/// Initializes the global tracing subscriber with environment-based filtering.
fn setup_logging() {
    // Read RUST_LOG from the environment, or fall back to "info" if unset.
    let filter = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("info"));

    // Attach the fmt layer with the filter to the global dispatcher.
    tracing_subscriber::fmt()
        .with_env_filter(filter)
        .init();
}

fn main() {
    setup_logging();
    info!("service started");
}
```

Run this with `RUST_LOG=debug cargo run` to see debug-level output. Run it with `RUST_LOG=error cargo run` to suppress everything except errors. The `EnvFilter` parses the variable using a simple syntax. `info` sets the global default. `my_crate=debug` overrides it for a specific module. `my_crate::module=trace` drills down further. The parser handles wildcards, level overrides, and target matching without requiring a restart.

Convention aside: the standard environment variable is `RUST_LOG`. You will sometimes see `LOG` in older tooling or compiler-specific crates like `rustc_log`. Stick to `RUST_LOG` for application code. The ecosystem expects it, and third-party crates will automatically respect it.

## How the pipeline works

When `init()` runs, it installs a `fmt::Subscriber` as the global dispatcher. The dispatcher lives in thread-local storage for performance. Every call to `info!`, `debug!`, or `error!` pushes an event to that dispatcher. The dispatcher forwards the event to the `fmt` layer. The `fmt` layer runs the `EnvFilter` first. The filter checks the event's target module and level against your rules. If the event passes, the layer allocates a buffer, writes the timestamp, level, target, and message, and flushes it to stdout. If the event fails, the layer returns immediately.

This design means logging carries almost zero cost when disabled. The macro expands to a function call. The function checks the filter. If the filter rejects it, the function returns before any formatting logic runs. You get production-grade performance without sacrificing developer ergonomics.

The `fmt` layer also handles color. It detects whether stdout is connected to a terminal. If it is, it applies ANSI escape codes for levels and module names. If you pipe the output to a file or a log aggregator, it strips the codes automatically. You do not need to write conditional formatting logic.

## Real-world configuration

Production systems rarely use the default setup. You need structured output for log aggregators, separate sinks for different modules, and graceful fallbacks when environment variables are malformed. A realistic initialization function handles these requirements without panicking on startup.

```rust
use tracing::Level;
use tracing_subscriber::{EnvFilter, fmt, prelude::*};

/// Configures logging based on the current deployment environment.
fn setup_logging() {
    // Convention: check APP_ENV or similar to toggle dev/prod behavior.
    let is_dev = std::env::var("APP_ENV")
        .unwrap_or_else(|_| "dev".into()) == "dev";

    // Parse RUST_LOG, falling back to a safe default if the variable is invalid.
    let filter = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("info"));

    if is_dev {
        // Pretty printing adds colors and indentation for terminal readability.
        fmt::Subscriber::builder()
            .with_env_filter(filter)
            .pretty()
            .finish()
            .try_init()
            .expect("failed to set up development logging");
    } else {
        // JSON output is machine-readable and works with Datadog, ELK, or Splunk.
        fmt::Subscriber::builder()
            .with_env_filter(filter)
            .json()
            .finish()
            .try_init()
            .expect("failed to set up production logging");
    }
}

fn main() {
    setup_logging();
    // Application logic follows.
}
```

The `prelude::*` import brings extension traits into scope. These traits add methods like `.pretty()` and `.json()` to the builder. Without the prelude, you must call the full path `fmt::format().pretty()`, which clutters the code. The community convention is to import the prelude at the top of your logging module.

Notice `try_init()` instead of `init()`. The `init()` method panics if a subscriber is already registered. `try_init()` returns a `Result`. It fails gracefully if another part of your dependency tree already set up logging. This prevents startup crashes in larger projects.

## Common traps and compiler feedback

Logging in Rust behaves differently from `println!`. The compiler will not save you from silent drops. If you forget to call `init()` or `try_init()`, your events disappear. No warning appears. No error code. You simply get an empty terminal. This is by design. `tracing` assumes you control the subscriber lifecycle.

Double initialization is the next frequent issue. If you call `init()` twice, the second call panics. The panic message points to `tracing::subscriber::set_global_default`. Switch to `try_init()` and handle the `Err` variant. Most teams log a warning and continue, or they panic during development and swallow the error in production.

Environment variable parsing fails silently if you use `unwrap()`. A typo in `RUST_LOG=debg` causes `EnvFilter::try_from_default_env()` to return an error. If you unwrap it, your binary crashes before it starts. Use `unwrap_or_else` with a sensible fallback. The fallback keeps your service alive while you fix the deployment configuration.

Mixing `tracing` with the older `log` crate causes trait bound errors. The `log` crate uses a different dispatcher. If you call `log::info!` without a bridge, the message vanishes. The compiler may reject your code with `E0277` (trait bound not satisfied) when you try to combine subscriber layers that expect different event types. Add `tracing-log` to your dependencies and call `tracing_log::LogTracer::init()` before setting up your subscriber. The bridge routes `log` events into the `tracing` pipeline.

Performance traps appear when you log large structures. `info!("state = {:?}", huge_struct)` forces a full debug format at call time. The formatting happens before the filter checks the level. If you run with `RUST_LOG=error`, you still pay the CPU cost of formatting the struct, only to have the subscriber drop it. Use `info!(state = %huge_struct)` for cheap string conversion, or wrap expensive formatting in a closure that the subscriber evaluates lazily. The `tracing` macros support lazy evaluation through the `fields!` API.

Trust the filter. Put it at the top of the pipeline. Never format before you filter.

## Choosing your logging strategy

Use `tracing_subscriber::fmt::init()` when you need a quick default setup for a binary or integration test. Use `EnvFilter` with `try_from_default_env()` when you need runtime control over log levels without recompiling. Use the `json()` formatter when shipping to a log aggregator like Datadog or ELK. Use `tracing-log` when migrating a codebase that still depends on the older `log` crate. Reach for custom `Layer` implementations when you need to route specific events to separate files or metrics systems.

## Where to go next

- [How to Use Structured Logging in Rust with tracing](/en/how-to-use-structured-logging-in-rust-with-tracing/)
- [How to Use the tracing Crate in Rust](/en/how-to-use-the-tracing-crate-in-rust/)
- [How to use tracing crate in Rust logging and diagnostics](/en/how-to-use-tracing-crate-in-rust-logging-and-diagnostics/)