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.
Rust Intermediate Concepts
- Generic Types in Functions and Structs
- Implementing Traits for Custom Behavior in Rust
- Trait Bounds and Constraints in Rust
- Lifetimes in Rust
- Using Closures (Anonymous Functions) in Rust
- The Iterator Trait and .next() in Rust
- Higher Order Iterator Methods: map, filter, and fold in Rust
- Using impl Trait for Simplicity in Rust
- Advanced Collections: HashSet and BTreeMap in Rust
- Custom Error Types and the Error Trait in Rust
- Option and Result Combinators in Rust
- Writing Unit Tests in Rust
- Integration Testing in Rust
- Logging in Rust with the log Crate
- Cargo Tips and Tricks for Rust Projects
- CLI Argument Parsing with Clap in Rust
- File I/O and File System Operations in Rust
- Running External Commands in Rust
- Make HTTP Requests in Rust with Reqwest
- JSON Serialization and Deserialization in Rust with Serde
- Building a Weather CLI in Rust
- Date and Time Handling in Rust with Chrono
- Using Regular Expressions in Rust with the regex Crate
- Memory Management in Rust
- Understanding Borrow Checker Errors in Rust
- Interacting with Databases in Rust
- Building a Todo List CLI in Rust with SQLite
- Concurrency in Rust