How does log crate's log_enabled! macro help with conditional logging overhead?

The log_enabled! macro allows you to check whether a specific log level is enabled before executing expensive logging operations. This prevents the cost of computing log arguments when the output would be discarded anyway, which is especially important for debug and trace logging that often involves complex string formatting or data introspection.

The Logging Cost Problem

use log::{debug, info};
 
fn process_data(data: &mut [u8]) {
    // Expensive operation happens BEFORE debug! checks level
    debug!("Processing {} bytes: {:?}", data.len(), expensive_format(data));
    
    // Even at INFO level, expensive_format() still runs!
    // Its result is just discarded
}
 
fn expensive_format(data: &[u8]) -> String {
    // Simulated expensive computation
    data.iter()
        .map(|b| format!("{:02x}", b))
        .collect::<Vec<_>>()
        .join(":")
}

Arguments to logging macros are always evaluated, even when the log level is disabled.

How Logging Levels Work

use log::Level;
 
fn level_checking() {
    // The logging facade checks the level at runtime
    // Debug logging at INFO level:
    // 1. Arguments are evaluated
    // 2. Level is checked (DEBUG < INFO)
    // 3. Output is discarded
    
    // The check happens INSIDE the macro
    // Arguments are evaluated BEFORE the check
    
    // This is why expensive arguments are costly
}

Arguments are evaluated eagerly before the level check occurs.

Basic log_enabled! Usage

use log::{debug, log_enabled, Level};
 
fn process_data(data: &mut [u8]) {
    // Check BEFORE computing expensive values
    if log_enabled!(Level::Debug) {
        debug!("Processing {} bytes: {:?}", data.len(), expensive_format(data));
    }
    
    // Now expensive_format only runs if DEBUG is enabled
}
 
fn expensive_format(data: &[u8]) -> String {
    data.iter()
        .map(|b| format!("{:02x}", b))
        .collect::<Vec<_>>()
        .join(":")
}

The log_enabled! macro guards expensive computations.

Macro Syntax

use log::{log_enabled, Level};
 
fn syntax_examples() {
    // Check if DEBUG level is enabled
    if log_enabled!(Level::Debug) {
        // Debug logging is active
    }
    
    // Check if TRACE level is enabled
    if log_enabled!(Level::Trace) {
        // Trace logging is active
    }
    
    // Check against the logger's max level
    if log_enabled!(Level::Info) {
        // Info logging is active
    }
    
    // Target-specific check (advanced)
    if log_enabled!(target: "my_module", Level::Debug) {
        // Debug logging for specific target
    }
}

The macro accepts a Level variant and optionally a target.

Performance Comparison

use log::{debug, log_enabled, Level};
use std::time::Instant;
 
fn performance_demo() {
    let data = vec![0u8; 1000];
    
    // WITHOUT log_enabled!
    let start = Instant::now();
    for _ in 0..10_000 {
        debug!("Data: {:?}", expensive_format(&data));
    }
    let without_check = start.elapsed();
    
    // WITH log_enabled!
    let start = Instant::now();
    for _ in 0..10_000 {
        if log_enabled!(Level::Debug) {
            debug!("Data: {:?}", expensive_format(&data));
        }
    }
    let with_check = start.elapsed();
    
    // When DEBUG is disabled:
    // without_check: expensive_format runs 10,000 times
    // with_check: expensive_format never runs
    
    println!("Without check: {:?}", without_check);
    println!("With check:    {:?}", with_check);
}
 
fn expensive_format(data: &[u8]) -> String {
    // Simulated expensive work
    data.iter().map(|b| b.to_string()).collect::<Vec<_>>().join(",")
}

When logging is disabled, the check avoids all computation.

When to Use log_enabled!

use log::{debug, trace, log_enabled, Level};
 
struct ComplexState {
    data: Vec<Vec<u8>>,
    metadata: std::collections::HashMap<String, String>,
}
 
impl ComplexState {
    fn process(&self) {
        // GOOD: Cheap operations don't need guarding
        debug!("Processing state");
        debug!("Data count: {}", self.data.len());
        
        // GOOD: Guard expensive operations
        if log_enabled!(Level::Debug) {
            debug!("Full state: {:?}", self);
        }
        
        // GOOD: Guard complex computations
        if log_enabled!(Level::Trace) {
            for (i, chunk) in self.data.iter().enumerate() {
                trace!("Chunk {}: {:?}", i, hex_dump(chunk));
            }
        }
        
        // GOOD: Guard serialization
        if log_enabled!(Level::Debug) {
            let json = serde_json::to_string_pretty(&self.metadata).unwrap();
            debug!("Metadata JSON:\n{}", json);
        }
    }
}
 
fn hex_dump(data: &[u8]) -> String {
    // Expensive hex formatting
    data.iter()
        .map(|b| format!("{:02x}", b))
        .collect::<Vec<_>>()
        .join(" ")
}

Guard expensive operations like serialization, complex formatting, and iteration.

When NOT to Use log_enabled!

use log::{debug, info, warn, log_enabled, Level};
 
fn unnecessary_guards() {
    // UNNECESSARY: Simple formatting is cheap
    if log_enabled!(Level::Debug) {
        debug!("Count: {}", 42);  // This is already cheap
    }
    
    // JUST WRITE:
    debug!("Count: {}", 42);
    
    // UNNECESSARY: Info and above are usually enabled in production
    if log_enabled!(Level::Info) {
        info!("Important event");
    }
    
    // JUST WRITE:
    info!("Important event");
    
    // UNNECESSARY: Error logging should always be fast
    if log_enabled!(Level::Error) {
        warn!("Something went wrong");
    }
    
    // JUST WRITE:
    warn!("Something went wrong");
}

Simple formatting and high-priority logging don't need guards.

Targeted Logging Checks

use log::{debug, log_enabled, Level};
 
// Module-specific logging configuration
fn module_logging() {
    // Check for specific logging target
    if log_enabled!(target: "database", Level::Debug) {
        debug!(target: "database", "Query executed");
    }
    
    if log_enabled!(target: "network", Level::Trace) {
        trace!(target: "network", "Packet received: {:?}", format_packet());
    }
}
 
fn format_packet() -> String {
    // Expensive packet formatting
    String::new()
}

Target-specific checks enable fine-grained logging control.

Common Patterns

use log::{debug, trace, log_enabled, Level};
 
// Pattern 1: Detailed iteration logging
fn process_items(items: &[Item]) {
    debug!("Processing {} items", items.len());
    
    if log_enabled!(Level::Trace) {
        for (i, item) in items.iter().enumerate() {
            trace!("Item {}: {:?}", i, item);
        }
    }
    
    // Process...
}
 
// Pattern 2: Conditional state dump
fn validate_state(state: &State) -> Result<(), Error> {
    if log_enabled!(Level::Debug) {
        debug!("Validating state: {:#?}", state);
    }
    
    // Validate...
    Ok(())
}
 
// Pattern 3: Expensive diagnostic computation
fn diagnose_issue(data: &[u8]) -> String {
    // Expensive diagnostic computation
    format!("Length: {}, Hash: {:x}", 
            data.len(), 
            blake3::hash(data))  // Hypothetical
}
 
fn handle_data(data: &[u8]) {
    if log_enabled!(Level::Debug) {
        debug!("Diagnostics: {}", diagnose_issue(data));
    }
}

These patterns avoid expensive work when logging is disabled.

The Level Filter Hierarchy

use log::Level;
 
fn level_hierarchy() {
    // Levels from lowest to highest:
    // Trace < Debug < Info < Warn < Error
    
    // If INFO is enabled:
    // - Trace is disabled
    // - Debug is disabled
    // - Info is enabled
    // - Warn is enabled
    // - Error is enabled
    
    // log_enabled! respects this hierarchy
    if log_enabled!(Level::Debug) {
        // Only runs if current level is Debug or lower
    }
}

The level hierarchy determines which logs are emitted.

Combining with Other Macros

use log::{debug, error, info, log_enabled, trace, warn, Level};
 
fn combined_usage() {
    // Multiple checks for different levels
    if log_enabled!(Level::Trace) {
        trace!("Very detailed: {:?}", full_dump());
    } else if log_enabled!(Level::Debug) {
        debug!("Summary: {:?}", summary());
    }
    
    // Nested expensive operations
    if log_enabled!(Level::Debug) {
        let state = compute_expensive_state();
        debug!("State: {:?}", state);
        
        if log_enabled!(Level::Trace) {
            trace!("State details: {:?}", detailed_state(&state));
        }
    }
}
 
fn full_dump() -> String { String::new() }
fn summary() -> String { String::new() }
fn compute_expensive_state() -> String { String::new() }
fn detailed_state(s: &str) -> String { String::new() }

Checks can be combined for layered logging.

Structured Logging Context

use log::{debug, log_enabled, Level};
 
struct RequestContext {
    request_id: String,
    user_id: u64,
    timestamp: i64,
}
 
fn handle_request(ctx: &RequestContext, data: &[u8]) {
    // Cheap to log
    debug!("[{}] Handling request for user {}", ctx.request_id, ctx.user_id);
    
    // Expensive to compute, guard it
    if log_enabled!(Level::Debug) {
        let hex_data: String = data.iter()
            .map(|b| format!("{:02x}", b))
            .collect::<Vec<_>>()
            .chunks(16)
            .map(|c| c.join(" "))
            .collect::<Vec<_>>()
            .join("\n");
        
        debug!("[{}] Request payload:\n{}", ctx.request_id, hex_data);
    }
}

Guard expensive context-specific logging.

Debug vs Release Behavior

use log::{debug, log_enabled, trace, Level};
 
fn demonstrate_behavior() {
    // In debug builds, logging might be more verbose
    // In release builds, only info/warn/error might be enabled
    
    // This expensive trace logging:
    if log_enabled!(Level::Trace) {
        trace!("Full execution trace: {:?}", build_trace());
    }
    
    // Will:
    // - Run in debug builds (if trace enabled)
    // - Skip entirely in release (if trace disabled)
}
 
fn build_trace() -> String {
    // Expensive trace building
    String::new()
}

The macro adapts to the logging configuration at runtime.

Implementation Details

// What log_enabled! expands to (conceptually):
// log_enabled!(Level::Debug)
// becomes something like:
// log::logger().enabled(&log::Record::builder().level(Level::Debug).build())
 
// This is a runtime check against the current logger's max level
// It's very fast - just a comparison in most implementations
 
use log::{log_enabled, Level};
 
fn check_cost() {
    // The log_enabled! check itself is very cheap
    // It's just a level comparison
    
    let start = std::time::Instant::now();
    for _ in 0..1_000_000 {
        let _ = log_enabled!(Level::Debug);
    }
    let elapsed = start.elapsed();
    
    // This is typically sub-microsecond for 1 million checks
    println!("1M checks: {:?}", elapsed);
}

The check itself is extremely cheap; it just avoids expensive argument evaluation.

Real-World Example

use log::{debug, info, log_enabled, trace, Level};
use std::collections::HashMap;
 
struct Cache {
    data: HashMap<String, Vec<u8>>,
    hits: u64,
    misses: u64,
}
 
impl Cache {
    fn get(&mut self, key: &str) -> Option<&[u8]> {
        // Always log cache operations at debug
        debug!("Cache lookup for key: {}", key);
        
        let result = self.data.get(key);
        
        if result.is_some() {
            self.hits += 1;
            debug!("Cache hit for {}", key);
        } else {
            self.misses += 1;
            debug!("Cache miss for {}", key);
        }
        
        // Guard expensive dump
        if log_enabled!(Level::Trace) {
            trace!("Cache state: {} entries, {} hits, {} misses",
                   self.data.len(), self.hits, self.misses);
            
            for (k, v) in &self.data {
                trace!("  {} -> {} bytes", k, v.len());
            }
        }
        
        result.map(|v| v.as_slice())
    }
    
    fn stats(&self) -> String {
        // This is called explicitly, so no guard needed
        format!("Hits: {}, Misses: {}, Ratio: {:.2}%",
                self.hits, self.misses,
                if self.hits + self.misses > 0 {
                    100.0 * self.hits as f64 / (self.hits + self.misses) as f64
                } else {
                    0.0
                })
    }
}

Production code benefits from strategic use of log_enabled!.

Synthesis

log_enabled! prevents wasted computation in logging:

When to use it:

Scenario Use log_enabled!?
Simple formatting ("count: {}") No
Integer/string interpolation No
Serialization (serde_json::to_string) Yes
Iteration over collections Yes
Complex computations Yes
Debug/Trace level logging Often
Info/Warn/Error level Rarely

Performance impact:

// WITHOUT guard (DEBUG disabled):
// expensive_format() runs, result discarded
debug!("Data: {:?}", expensive_format(&data));
 
// WITH guard (DEBUG disabled):
// expensive_format() never runs
if log_enabled!(Level::Debug) {
    debug!("Data: {:?}", expensive_format(&data));
}

Best practices:

  1. Use for debug and trace logging with expensive operations
  2. Skip for info, warn, error (usually enabled)
  3. Skip for simple formatting
  4. Use for serialization and complex diagnostics
  5. The check itself is extremely cheap

Common pattern:

use log::{debug, log_enabled, Level};
 
fn process(data: &[u8]) {
    // Cheap: no guard needed
    debug!("Processing {} bytes", data.len());
    
    // Expensive: guard it
    if log_enabled!(Level::Debug) {
        debug!("Data: {:?}", expensive_format(data));
    }
}

log_enabled! is a zero-cost abstraction when logging is disabled—the check prevents all argument evaluation overhead.