---
title: How to use env_logger crate in Rust environment logger
summary: Initialize env_logger in Rust by adding the dependency and calling env_logger::init() in main to enable RUST_LOG environment variable control.
lang: en
slug: how-to-use-envlogger-crate-in-rust-environment-logger
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-use-envlogger-crate-in-rust-environment-logger/
---

# How to use env_logger crate in Rust environment logger



## The wall of text problem

You built a CLI tool that works perfectly on your machine. You added `println!` statements everywhere to track state while you were coding. Now you ship the binary to a friend. They run it and get a wall of debug text that buries the actual error message. They can't tell what went wrong.

You comment out the debug lines, rebuild, and ship again. A week later, a new bug appears. You have no visibility into what the program is doing. You uncomment the lines, rebuild, and repeat. Printing to stdout is a sledgehammer. It's either on or off, and changing it requires recompiling. You need a way to control verbosity without touching the binary, and you need it to work across different crates without passing a logger object around.

That's what `env_logger` solves. It gives you a remote control for your logs. You configure the behavior using the `RUST_LOG` environment variable, and the logger respects those settings at runtime. You can turn up the volume on a specific module, mute background noise, or silence everything with a single flag. No rebuilds. No refactoring.

## The soundboard analogy

Rust separates the act of logging from the destination. The `log` crate provides the macros like `info!`, `warn!`, and `error!`. It is a facade. It defines the interface but does not print anything by itself. `env_logger` is the implementation that listens to the environment and decides what gets printed.

Think of `log` as a network of microphones scattered through your code. Every crate can plug into the network. `env_logger` is the soundboard. You connect the microphones to the board, and then you use the `RUST_LOG` variable to adjust the faders. You can crank up the gain for the "network" channel while muting the "database" channel. The microphones stay in place; only the output changes.

This separation is the key to Rust's logging ecosystem. Libraries can use the `log` macros without forcing a specific logger on your application. You choose the backend that fits your needs, and all the libraries just work.

## Setting up the logger

You need two crates: `log` for the macros and `env_logger` for the backend. Add both to your `Cargo.toml`.

```toml
[dependencies]
env_logger = "0.11"
log = "0.4"
```

The `log` crate provides the trait definitions and macros. `env_logger` depends on `log`, but it does not re-export the macros for you. You must list `log` explicitly to use `info!` and friends. If you forget this, the compiler rejects the import with `E0432` (use of undeclared crate or module).

Initialize the logger at the top of `main`. This reads the `RUST_LOG` variable and sets up the filter.

```rust
use log::{info, error, warn};

fn main() {
    // Initialize the logger once.
    // This reads RUST_LOG and configures the backend.
    env_logger::init();

    info!("Server listening on port 8080");
    warn!("Config file missing, using defaults");
    error!("Failed to connect to database");
}
```

Run the program with `RUST_LOG=info` to see output. Without the variable, the logger stays silent. The `init` call is cheap, but it should happen exactly once. Calling it multiple times is a code smell. In newer versions of `env_logger`, repeated calls are no-ops, but the convention is to initialize at the entry point and leave it alone.

**Convention aside:** The community prefers `env_logger::try_init()` over `init()`. The `try_init` function returns a `Result`, allowing you to handle initialization failures gracefully. If the `RUST_LOG` syntax is malformed, `try_init` lets you log the error or exit cleanly. Use `try_init` in production code; `init` is acceptable for quick scripts.

## How RUST_LOG controls the flow

The `RUST_LOG` variable uses a simple syntax to control which messages appear. The logger checks this variable once during initialization and builds a filter map. Every log call checks the map at runtime. If the message passes the filter, it gets formatted and sent to stderr. If not, the call is a no-op. The overhead is tiny because the check happens before any string formatting.

The levels form a hierarchy. Setting a level enables that level and everything above it.

- `error`: Only errors.
- `warn`: Warnings and errors.
- `info`: Info, warnings, and errors.
- `debug`: Debug, info, warnings, and errors.
- `trace`: All messages.

Run with `RUST_LOG=debug` to see everything. Run with `RUST_LOG=error` to see only critical failures. The default is off. If you want a sensible default for development, set `RUST_LOG=info` in your shell profile or use the `Builder` pattern to parse a default value.

You can target specific modules or crates. The syntax uses the module path.

```bash
# Enable debug for the entire app
RUST_LOG=debug ./my_app

# Enable debug only for the network module
RUST_LOG=my_app::network=debug ./my_app

# Enable debug for a third-party crate
RUST_LOG=tokio=debug ./my_app

# Combine multiple filters with commas
RUST_LOG=info,my_app::db=debug ./my_app
```

The filter applies to the module path where the macro is called. If you call `debug!` inside `my_app::network::connect`, the path is `my_app::network::connect`. Setting `RUST_LOG=my_app::network=debug` matches this path and enables the log. Setting `RUST_LOG=my_app=debug` also matches, because the filter applies to the prefix.

**Convention aside:** `env_logger` writes to stderr by default. This is intentional. Stdout is reserved for program output that other tools might parse. Stderr is for diagnostics. This separation lets you pipe the data stream to a file while keeping logs on the terminal. `./my_app > output.txt` captures the data; the logs still appear on screen.

## The log facade architecture

The split between `log` and `env_logger` is not an accident. It is a deliberate design pattern called a facade. The `log` crate defines a trait-based interface. Crates implement this interface to provide logging backends. Applications select a backend at runtime.

This architecture solves a dependency problem. Imagine a library that wants to log debug information. If the library depends on `env_logger`, every application using that library must also depend on `env_logger`. If another library uses `tracing`, you end up with conflicting loggers. The application has no control.

With the facade, the library depends only on `log`. It calls `debug!` and moves on. The application depends on `log` and `env_logger`. The application initializes `env_logger`, which registers itself as the global logger. When the library calls `debug!`, the call routes through the facade to `env_logger`. The library knows nothing about the backend. The application controls the configuration.

This pattern appears elsewhere in Rust. The `serde` crate uses a similar approach for serialization. The `log` facade is the standard for logging in the ecosystem. Most crates use it. If you see a crate using `println!` for logging, it is likely a sign of immaturity.

## Realistic usage with targets

In larger applications, you often need to distinguish between different subsystems. The `log` macros support a `target` parameter. This lets you assign a custom tag to a log message, independent of the module path.

```rust
mod database {
    use log::{debug, error};

    pub fn query(sql: &str) {
        debug!(target: "db", "Executing query: {}", sql);
        // Simulate work
        if sql.contains("DROP") {
            error!(target: "db", "Blocked dangerous query");
        }
    }
}

mod auth {
    use log::warn;

    pub fn check_token(token: &str) {
        if token.is_empty() {
            warn!(target: "auth", "Empty token received");
        }
    }
}

fn main() {
    env_logger::init();

    log::info!("Application starting");
    database::query("SELECT * FROM users");
    auth::check_token("");
}
```

The `target` parameter overrides the default module path. You can filter by target using the same `RUST_LOG` syntax.

```bash
# Enable debug for the database target
RUST_LOG=db=debug ./my_app

# Enable debug for auth and database
RUST_LOG=auth=debug,db=debug ./my_app
```

This is useful when you want to group logs by logical subsystem rather than file structure. You can move code between modules without breaking your log filters. The target stays consistent.

**Convention aside:** Use lowercase targets with underscores or hyphens. `db`, `auth`, `network_io`. Avoid module paths in targets unless you are mirroring the module structure. Targets should be stable identifiers that make sense in a filter string.

## Pitfalls and gotchas

Forgetting to initialize the logger is the most common mistake. If you call `info!` without calling `env_logger::init()`, nothing happens. The logs are silently dropped. The compiler does not warn you. The facade requires a backend to be registered. If no backend is registered, the macros compile to no-ops. Always initialize the logger before any logging calls.

Another trap is assuming `RUST_LOG` is case-insensitive. The level names are case-insensitive (`DEBUG`, `debug`, `Debug` all work). Module paths and targets are case-sensitive. `RUST_LOG=MyApp=debug` does not match `my_app`. Use lowercase for consistency.

The `init` function parses the environment variable immediately. If the syntax is invalid, `init` panics in some versions or ignores the variable in others. Use `try_init` to handle errors. If you need to validate the configuration, check the result.

```rust
if let Err(e) = env_logger::try_init() {
    eprintln!("Failed to initialize logger: {}", e);
}
```

This pattern prevents panics from malformed environment variables. It is especially important in libraries that might initialize logging on behalf of the application.

**Convention aside:** Keep `env_logger` blocks small and isolated. Do not wrap large sections of logic in logger configuration. Initialize once, log everywhere. If you need dynamic configuration, use `env_logger::Builder` to construct the logger with custom rules, then initialize. Avoid calling `init` inside loops or request handlers.

## Decision: when to use this vs alternatives

Use `env_logger` when you need a simple, zero-config logger for a CLI tool or a small service. It works out of the box with environment variables and has almost no setup cost.

Use `tracing` when you are building a complex async application or need structured data with spans and fields. `tracing` gives you hierarchical context that `env_logger` cannot provide. It tracks the flow of execution across async boundaries and lets you attach metadata to specific operations.

Use `fern` when you need fine-grained control over destinations, like writing errors to a file and info to stdout, or custom formatting rules. `fern` is a configuration engine, not just an environment parser. It excels at routing logs to multiple outputs with different formats.

Reach for `println!` only during rapid prototyping. Switch to `log` macros before the code leaves your local machine. `println!` cannot be filtered, cannot be redirected, and clutters the output stream. Treat logging as a first-class concern from day one.

Trust the facade. It keeps your dependencies clean and your options open.

## Where to go next

- [How to Use env_logger in Rust](/en/how-to-use-envlogger-in-rust/)
- [How to Use fern for Custom Log Configuration in Rust](/en/how-to-use-fern-for-custom-log-configuration-in-rust/)