---
title: How to Filter Logs by Level and Module in Rust
summary: You can filter logs by level and module in Rust by configuring the `tracing` or `env_logger` crate with a specific filter string passed via the `RUST_LOG` environment variable or programmatically.
lang: en
slug: how-to-filter-logs-by-level-and-module-in-rust
published: 2026-04-17T16:39:40+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-filter-logs-by-level-and-module-in-rust/
---

# How to Filter Logs by Level and Module in Rust



## When the logs drown the signal

Your Rust web server is running in production. A user reports a login failure. You enable logging to investigate, but suddenly your terminal floods with thousands of lines of debug output from the database driver, the HTTP parser, and the background task scheduler. The actual auth error is buried under a waterfall of noise. You need a way to turn down the global volume while cranking up the detail for the specific module causing trouble.

Rust's logging ecosystem solves this with a filter string. You configure a baseline level for the entire application and then override that baseline for specific module paths. The filter runs at runtime, deciding which log events pass through to your output and which get dropped. This lets you isolate the signal without changing code or recompiling.

## How the filter works

Think of the filter as a mixing board with a master volume knob and individual faders for each channel. The master knob sets the global threshold. If you set the master to `info`, all events below `info` (like `debug` and `trace`) get silenced everywhere. The individual faders let you pull up specific channels. You can set the `auth` module to `debug` while keeping the rest of the app at `info`.

The filter matches log events against a set of rules. Each event has a target, which is usually the module path where the log macro was called. The filter checks the target against the rules and applies the most specific match. If no rule matches the target, the filter falls back to the global default.

This matching happens before the log message is formatted. Rust logging macros expand to code that checks the filter first. If the event is filtered out, the string formatting and argument evaluation are skipped entirely. This means `debug!("Expensive result: {}", compute_heavy())` won't call `compute_heavy()` if debug logging is disabled. The filter protects you from the cost of constructing the message, not just the cost of printing it.

## Minimal example

The standard way to configure filtering is through the `RUST_LOG` environment variable. The `tracing` ecosystem reads this variable automatically if you use `EnvFilter`.

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

fn main() {
    // Read the filter from RUST_LOG environment variable.
    // Fall back to "info" if the variable is not set.
    let filter = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("info"));

    // Initialize the subscriber with the filter.
    // This installs the global tracing dispatcher.
    tracing_subscriber::fmt()
        .with_env_filter(filter)
        .init();

    info!("Server starting up");
    debug!("Loading configuration"); // Hidden by default
}
```

Run this with a filter string to see the override in action:

```bash
RUST_LOG="info,my_app::auth=debug" cargo run
```

The output shows the info message but hides the debug message because the global level is `info`. If you move the debug call into a module named `my_app::auth`, it becomes visible because the specific rule overrides the global default.

Set the filter once at startup. The logger reads the environment variable during initialization and ignores changes afterward.

## Anatomy of the filter string

The filter string follows a concise syntax. You combine rules with commas. Each rule can be a global level or a module-specific override.

A bare level like `info` sets the global default. A rule like `my_app::auth=debug` sets the level for that specific path. The filter resolves targets using a prefix tree. When an event fires with target `my_app::auth::login`, the filter checks for a rule matching that exact path. If none exists, it checks `my_app::auth`. If that exists, it applies that rule. If not, it checks `my_app`. This continues until it hits a rule or falls back to the global default. The most specific match always wins.

You can silence a module entirely by setting it to `off`. This is useful for noisy third-party crates that spam debug logs. For example, `hyper=off` disables all logging from the `hyper` crate.

The levels follow a standard hierarchy: `trace`, `debug`, `info`, `warn`, `error`. `trace` is the most verbose. `error` is the least. Events are only emitted if their level is greater than or equal to the threshold set by the filter.

Convention aside: The community standard is to use lowercase levels in the filter string. `tracing` accepts `DEBUG`, `Debug`, and `debug`, but lowercase is the norm in documentation and examples. Stick to lowercase to avoid confusion.

## Realistic example with third-party noise

Real applications pull in dependencies that generate their own logs. You often need to suppress noise from external crates while keeping your own code verbose.

```rust
// main.rs
use tracing::{debug, info, warn};
use tracing_subscriber::{EnvFilter, fmt};

mod auth;

fn main() {
    // Read RUST_LOG from environment.
    // Default to "warn" for production-like silence.
    let filter = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("warn"));

    tracing_subscriber::fmt()
        .with_env_filter(filter)
        .init();

    info!("Application starting");
    debug!("Initializing subsystems"); // Hidden by default

    // Simulate a noisy third-party crate
    external_crate::do_work();

    auth::process_login("admin@example.com");
}

mod external_crate {
    use tracing::debug;

    pub fn do_work() {
        debug!("External crate internal state update");
        debug!("External crate buffer flush");
    }
}
```

```rust
// auth.rs
use tracing::{debug, info, warn};

pub fn process_login(email: &str) {
    debug!("Checking credentials for {}", email);
    info!("Auth module active");
    warn!("Slow response detected");
}
```

Run this with a targeted filter:

```bash
RUST_LOG="warn,auth=debug,external_crate=off" cargo run
```

The output shows warnings and errors globally. The `auth` module emits debug, info, and warn messages. The `external_crate` is completely silenced. This gives you precise control over the log stream without modifying the dependencies.

If you pass a malformed filter string, `EnvFilter::try_from_default_env()` returns an error. Handle this result explicitly. Using `unwrap()` or `expect()` is common in `main` because a bad filter string indicates a configuration mistake that should fail fast.

```rust
let filter = EnvFilter::try_from_default_env()
    .expect("Invalid RUST_LOG format");
```

Don't swallow filter errors silently. A typo in the filter string can leave your logging in an unexpected state. Fail loudly so you notice the mistake immediately.

## Pitfalls and gotchas

Module paths must match the target used by the log macros. By default, the target is the module path where the macro is called. If you have a module named `my_app::auth`, the filter rule `my_app::auth=debug` works. If you rename the module to `authentication`, the rule breaks. The filter is case-sensitive and exact on the path segments.

The `target` parameter in log macros overrides the default module path. `debug!(target: "custom", "message")` sets the target to `custom`. The filter matches against `custom`, not the module path. This is useful for grouping logs across modules, but it means your filter rules must match the custom target.

Convention aside: Avoid overriding the target unless you have a strong reason. The default module path provides automatic hierarchy and makes filtering predictable. Custom targets require manual maintenance of the filter string.

Dynamic filtering requires explicit setup. `EnvFilter` reads the environment variable once. Changing `RUST_LOG` after the subscriber is initialized has no effect. If you need to change filters at runtime, use `tracing_subscriber::reload` to wrap the filter in a reloadable layer. This lets you update the filter via an API endpoint or signal handler.

Performance degrades if you log too much. Filtering is fast, but emitting millions of log events still costs CPU cycles. Use `trace` and `debug` sparingly. Reserve `info` for events that matter to operators. `warn` and `error` should indicate problems that require attention.

Trust the borrow checker of logging: if you can't filter it, you probably shouldn't log it. Keep your log output proportional to the value it provides.

## Decision matrix

Use `RUST_LOG` environment variables when you want to control logging from the command line or deployment configuration without touching code. Use programmatic `EnvFilter` construction when your logging rules come from a config file or database and need to be parsed at startup. Use `tracing` with `tracing_subscriber` for new applications that need structured data, spans, and high performance. Use `env_logger` only for legacy codebases or tiny scripts where adding `tracing` feels like overkill. Use `off` in the filter string to silence a noisy third-party crate entirely. Use `RUST_LOG=debug` to flood the logs for local development, then switch to `RUST_LOG=info` for production. Use `target` overrides only when you need to group logs across unrelated modules under a single filter key.

Use `tracing` for new code. Reach for `env_logger` only when legacy constraints force your hand.

## Where to go next

- [How to Add Logging to a Rust Application](/en/how-to-add-logging-to-a-rust-application/)
- [What Is the Difference Between log and tracing in Rust?](/en/what-is-the-difference-between-log-and-tracing-in-rust/)
- [How to Use defmt for Efficient Logging on Embedded Devices](/en/how-to-use-defmt-for-efficient-logging-on-embedded-devices/)