Logging in Rust with the log Crate

Rust provides structured logging through the log crate, a lightweight and flexible facade for application-wide logging. By pairing it with implementations like env_logger, you can log messages at different levels (error, warn, info, debug, trace) without needing to write custom logging systems.

As my projects grew, using println! for debugging became unmanageable. I needed a way to log messages conditionally, filter them by importance, and control them without editing source code. That is when I integrated the log crate with env_logger, and it changed how I monitored my application.

In this article, you will learn how to set up structured logging in a Rust project using log and env_logger, how to choose log levels, and how to make your logs more useful during development and debugging.

What Is the log Crate?

The log crate defines a standard logging interface. It does not log anything by itself, it lets you write logging calls that any compatible logger (like env_logger) can process.

This separation means:

  • You can write log::info!, log::debug!, etc.
  • You can plug in any backend (e.g. file logger, terminal logger)
  • You do not tie your code to one logging engine

Setting Up log and env_logger

In Cargo.toml, add:

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

In your main.rs or application entry point, initialize the logger:

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

    log::info!("App started");
    log::warn!("This is a warning");
    log::error!("Something went wrong");
}

This setup prints log messages to the terminal.

Log Levels

The log crate supports five levels:

  • error! – serious issues that require attention
  • warn! – something unexpected, but not fatal
  • info! – general progress or status
  • debug! – detailed debugging information
  • trace! – very fine-grained, rarely needed

Each level builds on the next: if the level is set to info, it shows info, warn, and error, but hides debug and trace.

Controlling Log Output with Environment Variables

You can set the log level at runtime using the RUST_LOG environment variable:

RUST_LOG=info cargo run

This shows info, warn, and error messages.

To show everything:

RUST_LOG=trace cargo run

You can also filter per module:

RUST_LOG=my_crate=debug cargo run

This gives you powerful control without modifying code.

Example: Logging in a Function

pub fn divide(a: i32, b: i32) -> Option<i32> {
    if b == 0 {
        log::error!("Attempted to divide by zero");
        None
    } else {
        log::debug!("Dividing {} by {}", a, b);
        Some(a / b)
    }
}

During normal use, the user sees only info and error, but developers can enable debug when needed.

Best Practices for Logging

  • Use error! for critical failures
  • Use warn! for recoverable odd cases
  • Use info! for user-facing progress
  • Use debug! for internal flow tracking
  • Use trace! for deep-dive diagnostics

Avoid using println! in production, use structured logging instead.

Logging in Tests

You can enable logging during tests by initializing the logger manually:

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Once;

    static INIT: Once = Once::new();

    fn init_logger() {
        INIT.call_once(|| {
            env_logger::builder().is_test(true).init();
        });
    }

    #[test]
    fn test_logging_behavior() {
        init_logger();
        assert_eq!(divide(6, 2), Some(3));
    }
}

This ensures that logs appear in test output when needed.

Summary

The log crate provides a consistent interface for logging in Rust, and env_logger makes it easy to get started. You can log at multiple levels, filter output dynamically, and separate debug and production logs. By using log instead of println!, you make your code more professional, flexible, and easier to debug.