Loading pageā¦
Rust walkthroughs
Loading pageā¦
log::LevelFilter reduce logging overhead at runtime for disabled log levels?log::LevelFilter is an enum that configures the maximum log level a logger will process at runtimeāmessages at levels below the filter are discarded immediately without string formatting, argument evaluation, or I/O operations. The log crate's macros (error!, warn!, info!, debug!, trace!) check the current LevelFilter before doing any work: if LevelFilter::Info is set, debug! and trace! calls become no-ops that skip argument evaluation entirely, avoiding the cost of computing expensive values or formatting strings that would never be output. This design enables verbose logging in debug builds while incurring zero overhead in release buildsāthe filter is checked once at the callsite, and disabled levels compile to nothing when using static filtering features.
use log::LevelFilter;
fn main() {
// LevelFilter represents maximum verbosity
let filters = [
LevelFilter::Off, // No logging
LevelFilter::Error, // Only errors
LevelFilter::Warn, // Errors and warnings
LevelFilter::Info, // Errors, warnings, info
LevelFilter::Debug, // Everything except trace
LevelFilter::Trace, // All messages
];
for filter in filters {
println!("Level: {:?}", filter);
}
}LevelFilter variants range from Off to Trace, controlling which messages reach the logger.
use log::{LevelFilter, info, debug};
fn main() {
// Set global maximum level
log::set_max_level(LevelFilter::Info);
// This gets through - Info <= Info
log::info!("This will be logged");
// This is skipped - Debug > Info
log::debug!("This will be skipped");
// Query current level
println!("Current max level: {:?}", log::max_level());
}The level filter is set globally; all loggers respect the maximum level.
use log::{Level, LevelFilter, Log, Metadata, Record};
struct SimpleLogger {
level: LevelFilter,
}
impl Log for SimpleLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
// This check happens before the log message is formatted
metadata.level() <= self.level
}
fn log(&self, record: &Record) {
// Only called if enabled() returned true
if self.enabled(record.metadata()) {
println!("[{}] {}", record.level(), record.args());
}
}
fn flush(&self) {}
}
fn main() {
let logger = SimpleLogger { level: LevelFilter::Info };
log::set_boxed_logger(Box::new(logger)).unwrap();
log::set_max_level(LevelFilter::Info);
// This gets through - Info <= Info
log::info!("This will be logged");
// This is skipped - Debug > Info
log::debug!("This will be skipped");
}The enabled() method checks before formatting; disabled levels skip all work.
use log::{LevelFilter, info, debug};
fn expensive_computation() -> String {
println!("Computing..."); // This won't print if level is Info
"expensive result".to_string()
}
fn main() {
log::set_max_level(LevelFilter::Info);
// The expensive_computation() is NEVER CALLED
// because the level check happens first
debug!("Result: {}", expensive_computation());
// Compare to this approach where evaluation happens:
// let result = expensive_computation();
// debug!("Result: {}", result);
// ^ This would compute regardless of level
println!("Done");
}When disabled, debug! skips argument evaluation entirelyāexpensive_computation() is never called.
use log::{Level, LevelFilter, log_enabled, debug};
fn main() {
log::set_max_level(LevelFilter::Info);
// The debug! macro expands roughly to:
if log_enabled!(target: "myapp", Level::Debug) {
// Arguments are only evaluated if the level is enabled
let result = expensive_computation();
log::log!(target: "myapp", Level::Debug, "Result: {}", result);
}
// When LevelFilter::Info is set, the if condition is false
// and the block is skipped entirely
// Compare with always-evaluated approach:
// let msg = format!("Result: {}", expensive_computation());
// debug!("{}", msg); // Too late - already computed
}
fn expensive_computation() -> String {
"expensive".to_string()
}log_enabled! checks the level; arguments are only evaluated inside the block.
use log::{LevelFilter, info, debug, trace};
fn main() {
// In release builds, these checks can be optimized away
// when the level is statically known to be disabled
log::set_max_level(LevelFilter::Info);
// In release mode with LevelFilter::Info:
// The compiler can see that debug!/trace! never execute
// and may eliminate the code entirely
for i in 0..1_000_000 {
// These debug calls compile to nothing in release
debug!("Processing item {}", i);
trace!("Item {} details: {:?}", i, compute_details(i));
}
// Only info level and above produce code
info!("Completed processing");
}
fn compute_details(i: u32) -> String {
format!("details for {}", i)
}The compiler can eliminate disabled log calls entirely in release builds.
use log::{LevelFilter, info, debug};
// Static filtering: compile-time level configuration
// Set in Cargo.toml:
// [dependencies]
// log = { version = "0.4", features = ["release_max_level_info"] }
// This makes debug!/trace! compile to nothing in release builds
// regardless of runtime LevelFilter
fn main() {
// Even if we try to set a higher level at runtime
log::set_max_level(LevelFilter::Debug);
// In release builds with release_max_level_info feature,
// debug! calls are already compiled out
debug!("This may not exist at all in release");
info!("This always works");
}release_max_level_* features remove disabled levels at compile time for zero overhead.
use log::{LevelFilter, info, debug};
fn main() {
// env_logger reads RUST_LOG environment variable
env_logger::Builder::new()
.filter_level(LevelFilter::Info) // Default level
.parse_env("RUST_LOG") // Override from env
.init();
// RUST_LOG=debug cargo run -> shows debug
// RUST_LOG=info cargo run -> shows info
// RUST_LOG=myapp=debug,info cargo run -> debug for myapp module
// RUST_LOG=off cargo run -> no logging
info!("This message appears with default config");
debug!("This appears only if RUST_LOG=debug or higher");
}
// Module-level filtering via environment
// RUST_LOG=myapp::module=debug,myapp=infoRUST_LOG environment variable enables dynamic filtering without recompilation.
use log::{LevelFilter, info, debug};
fn main() {
env_logger::Builder::new()
// Default level for all modules
.filter_level(LevelFilter::Info)
// Override for specific modules
.filter_module("myapp::network", LevelFilter::Debug)
.filter_module("myapp::database", LevelFilter::Trace)
.filter_module("external_crate", LevelFilter::Warn)
.init();
// Different modules can have different levels
log::info!(target: "myapp::network", "Network info message");
log::debug!(target: "myapp::network", "Network debug message");
log::debug!(target: "myapp::database", "Database debug message");
log::debug!(target: "myapp::other", "Other debug - filtered");
}Module-specific filters enable verbose logging where needed without flooding all modules.
use log::{info, debug, log_enabled, Level};
fn main() {
log::set_max_level(LevelFilter::Info);
// Good: check level before expensive computation
if log_enabled!(Level::Debug) {
let data = expensive_serialization();
debug!("Serialized data: {}", data);
}
// Bad: expensive computation happens regardless
// let data = expensive_serialization();
// debug!("Serialized data: {}", data); // data computed even if not logged
}
fn expensive_serialization() -> String {
// Imagine this is CPU-intensive
"large serialized output".to_string()
}Use log_enabled! to guard expensive computations that would only be logged at debug levels.
use log::{LevelFilter, Log, Metadata, Record, info, debug};
struct ThreadAwareLogger {
level: LevelFilter,
output: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
}
impl Log for ThreadAwareLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
// Check level filter
if metadata.level() > self.level {
return false;
}
// Could add additional filtering logic here
// e.g., per-thread levels, rate limiting, etc.
true
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return; // Skip if disabled
}
let message = format!("[{}] {}", record.level(), record.args());
self.output.lock().unwrap().push(message);
}
fn flush(&self) {}
}
fn main() {
let logger = ThreadAwareLogger {
level: LevelFilter::Info,
output: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
};
log::set_boxed_logger(Box::new(logger)).unwrap();
log::set_max_level(LevelFilter::Info);
info!("This is logged");
debug!("This is not logged");
}Custom loggers implement enabled() to filter efficiently before message construction.
use log::{LevelFilter, info, debug, warn};
fn main() {
log::set_max_level(LevelFilter::Warn);
debug!("Not shown - level is Warn");
info!("Not shown - level is Warn");
warn!("Shown - level is Warn");
// Change level at runtime
log::set_max_level(LevelFilter::Info);
debug!("Still not shown - level is Info");
info!("Now shown - level changed to Info");
warn!("Still shown");
// Useful for debugging mode toggle
println!("Current level: {:?}", log::max_level());
}set_max_level() changes the filter at runtime for dynamic debugging modes.
use log::{Level, LevelFilter};
fn main() {
// Level values are ordered: Error < Warn < Info < Debug < Trace
assert!(Level::Error < Level::Warn);
assert!(Level::Warn < Level::Info);
assert!(Level::Info < Level::Debug);
assert!(Level::Debug < Level::Trace);
// LevelFilter comparison
let filter = LevelFilter::Info;
// These levels pass through the filter (level <= filter)
assert!(Level::Error <= Level::Info);
assert!(Level::Warn <= Level::Info);
assert!(Level::Info <= Level::Info);
// These levels are blocked (level > filter)
assert!(Level::Debug > Level::Info);
assert!(Level::Trace > Level::Info);
println!("Level ordering: Error < Warn < Info < Debug < Trace");
}Lower levels (Error, Warn) are more severe; higher levels (Debug, Trace) are more verbose.
use log::{LevelFilter, info, debug};
fn expensive() -> String {
// Simulate expensive operation
let mut s = String::new();
for i in 0..10 {
s.push_str(&format!("{}", i));
}
s
}
fn main() {
// Approach 1: Macro-based (skips evaluation)
log::set_max_level(LevelFilter::Info);
let start = std::time::Instant::now();
for _ in 0..100_000 {
// Arguments never evaluated when level is filtered
debug!("Result: {}", expensive());
}
println!("Macro-based: {:?}", start.elapsed());
// Approach 2: Manual check (also skips)
let start = std::time::Instant::now();
for _ in 0..100_000 {
if log::max_level() >= LevelFilter::Debug {
debug!("Result: {}", expensive());
}
}
println!("Manual check: {:?}", start.elapsed());
// Approach 3: Always evaluate (BAD)
let start = std::time::Instant::now();
for _ in 0..100_000 {
let msg = format!("Result: {}", expensive());
if log::max_level() >= LevelFilter::Debug {
debug!("{}", msg);
}
}
println!("Always evaluate: {:?}", start.elapsed());
}The macro approach and manual check skip evaluation; pre-formatting defeats the optimization.
use log::{LevelFilter, info, debug};
fn main() {
env_logger::Builder::new()
.filter_level(LevelFilter::Info)
.format(|buf, record| {
use std::io::Write;
writeln!(
buf,
"{{\"timestamp\":\"{}\",\"level\":\"{}\",\"message\":\"{}\"}}",
chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.3f"),
record.level(),
record.args()
)
})
.init();
// JSON structured logging
info!("Application started");
debug!("This is filtered out");
}Formatting only happens for enabled levels; the format closure is never called for disabled messages.
LevelFilter values in order:
| Level | Filter | Included Levels |
|-------|--------|-----------------|
| Off | Nothing | None |
| Error | Errors only | Error |
| Warn | Warnings and above | Error, Warn |
| Info | Info and above | Error, Warn, Info |
| Debug | Debug and above | Error, Warn, Info, Debug |
| Trace | All | Error, Warn, Info, Debug, Trace |
Overhead comparison:
| Approach | Argument Evaluation | Formatting | I/O | |----------|---------------------|------------|-----| | Macro-based filtering | Skipped | Skipped | Skipped | | Pre-format string | Always | Always | Skipped | | No filtering | Always | Always | Always |
Key methods:
| Function | Purpose |
|----------|---------|
| log::set_max_level(filter) | Set global level filter |
| log::max_level() | Get current level filter |
| log_enabled!(Level::Debug) | Check if level is enabled |
| Logger::enabled(&metadata) | Custom logger check |
Key insight: log::LevelFilter achieves zero-overhead logging for disabled levels through two mechanisms working togetherāruntime level checking before argument evaluation, and compile-time elimination via cargo features. When LevelFilter::Info is set, debug! and trace! macros first check if the level is enabled, and only if true do they evaluate arguments, format strings, and call the logger. The log_enabled! macro exposes this check for guarding expensive computations. For release builds, features like release_max_level_info let the compiler eliminate disabled log calls entirely, reducing them to no-ops at compile time. The enabled() method on custom loggers receives the same optimizationāthe facade checks the level before calling log(), avoiding the cost of constructing Record objects for filtered messages. This design means verbose debug logging in development incurs no runtime cost in production; the same code runs with different filters, and disabled levels simply disappear from the execution path. Per-module filtering via filter_module() and RUST_LOG enables fine-grained controlāa production service can log at Info globally but enable Debug for a specific problematic module without redeployment.