How do I work with Log for Simple Logging Facade in Rust?

Walkthrough

Log is a lightweight logging facade for Rust. It provides a single logging API that abstracts over the actual logging implementation. Applications can choose any logging backend (like env_logger, simple_logger, or tracing) while library authors can use log without forcing a specific implementation on users.

Key concepts:

  • Log levels — Five levels: Error, Warn, Info, Debug, Trace
  • Macroserror!, warn!, info!, debug!, trace!
  • Loggers — Backend implementations (env_logger, simple_logger, etc.)
  • Metadata — Information about log records (level, target, module path)
  • Filters — Control which messages get logged

When to use Log:

  • Libraries that need logging (let users choose backend)
  • Simple applications with basic logging needs
  • When you want logging abstraction
  • Prototyping and simple projects

When NOT to use Log:

  • Complex observability needs (use tracing instead)
  • When you need structured logging with spans
  • When you need async-aware logging
  • When you need correlation IDs and distributed tracing

Code Examples

Basic Logging

use log::{error, warn, info, debug, trace};
 
fn main() {
    // Initialize a logger (required!)
    // Without this, logs are silently discarded
    env_logger::init();
    
    // Log at different levels
    error!("This is an error");
    warn!("This is a warning");
    info!("This is info");
    debug!("This is debug");
    trace!("This is trace");
}

Formatted Logging

use log::{info, debug};
 
fn main() {
    env_logger::init();
    
    let user = "Alice";
    let count = 42;
    
    // Format string (like println!)
    info!("User {} has {} items", user, count);
    
    // Named arguments
    debug!(user = %user, count = count; "Processing user");
}

Log Levels

use log::{Level, LevelFilter};
 
fn main() {
    // Available levels (in order of severity)
    println!("Log levels:");
    println!("  Error: {}", Level::Error as i32);  // 1
    println!("  Warn:  {}", Level::Warn as i32);   // 2
    println!("  Info:  {}", Level::Info as i32);   // 3
    println!("  Debug: {}", Level::Debug as i32);  // 4
    println!("  Trace: {}", Level::Trace as i32);  // 5
    
    // LevelFilter for configuring logger
    println!("\nLevelFilter values:");
    println!("  Off:   {}", LevelFilter::Off as i32);
    println!("  Error: {}", LevelFilter::Error as i32);
    println!("  Warn:  {}", LevelFilter::Warn as i32);
    println!("  Info:  {}", LevelFilter::Info as i32);
    println!("  Debug: {}", LevelFilter::Debug as i32);
    println!("  Trace: {}", LevelFilter::Trace as i32);
}

Conditional Logging

use log::{info, debug, error};
 
fn process_data(data: &[u8]) -> Result<(), &'static str> {
    debug!("Processing {} bytes", data.len());
    
    if data.is_empty() {
        error!("Empty data provided");
        return Err("empty data");
    }
    
    info!("Data processed successfully");
    Ok(())
}
 
fn main() {
    env_logger::init();
    
    process_data(&[1, 2, 3]).unwrap();
    process_data(&[]).unwrap_err();
}

Using env_logger

use log::{info, debug, error};
 
fn main() {
    // Initialize with custom builder
    env_logger::Builder::new()
        .filter_level(log::LevelFilter::Debug)  // Default level
        .init();
    
    info!("Application started");
    debug!("Debug information");
    
    // Run with: RUST_LOG=debug cargo run
    // Or: RUST_LOG=my_app=debug cargo run
}

Module-Level Filtering

use log::{info, debug, trace};
 
mod database {
    pub fn connect() {
        super::debug!("Connecting to database");
    }
}
 
mod cache {
    pub fn get(key: &str) {
        super::trace!("Cache get: {}", key);
    }
}
 
fn main() {
    // Filter by module path
    env_logger::Builder::new()
        .filter_module("my_app::database", log::LevelFilter::Debug)
        .filter_module("my_app::cache", log::LevelFilter::Trace)
        .init();
    
    database::connect();
    cache::get("user:1");
}

Custom Target

use log::{info, error};
 
fn main() {
    env_logger::init();
    
    // Default target is module path
    info!("Default target");
    
    // Custom target
    info!(target: "custom_target", "Custom target message");
    
    // Useful for routing logs
    error!(target: "security", "Security alert!");
}

Structured Fields

use log::{info, warn, error};
 
fn main() {
    env_logger::init();
    
    let user_id = 42;
    let action = "login";
    let ip = "192.168.1.1";
    
    // Key-value pairs (requires kv feature)
    info!(
        user_id = user_id,
        action = action,
        ip = ip;
        "User action performed"
    );
    
    // Error with context
    error!(
        error_code = 500,
        endpoint = "/api/users";
        "Request failed"
    );
}

Logging Errors

use log::{error, warn, info};
use std::io;
 
fn read_config(path: &str) -> Result<String, io::Error> {
    std::fs::read_to_string(path)
}
 
fn main() {
    env_logger::init();
    
    match read_config("config.txt") {
        Ok(config) => info!("Config loaded: {} bytes", config.len()),
        Err(e) => error!("Failed to read config: {}", e),
    }
}

Simple Logger

use log::info;
 
fn main() {
    // Alternative to env_logger - simpler setup
    simple_logger::SimpleLogger::new()
        .env()
        .init()
        .unwrap();
    
    info!("Using simple_logger");
}

Fern Logger

use log::{info, LevelFilter};
 
fn main() {
    // Fern - more configuration options
    fern::Dispatch::new()
        .format(|out, message, record| {
            out.finish(format_args!(
                "[{} {} {}] {}",
                chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
                record.level(),
                record.target(),
                message
            ))
        })
        .level(LevelFilter::Debug)
        .chain(std::io::stdout())
        .apply()
        .unwrap();
    
    info!("Using fern logger");
}

File Logging

use log::{info, LevelFilter};
 
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let log_file = std::fs::File::create("app.log")?;
    
    fern::Dispatch::new()
        .format(|out, message, record| {
            out.finish(format_args!(
                "[{} {}] {}",
                record.level(),
                record.target(),
                message
            ))
        })
        .level(LevelFilter::Debug)
        .chain(log_file)
        .apply()?;
    
    info!("This goes to file");
    
    Ok(())
}

Multiple Outputs

use log::{info, LevelFilter, Level};
 
fn main() {
    fern::Dispatch::new()
        // Errors go to stderr
        .chain(
            fern::Dispatch::new()
                .level(LevelFilter::Error)
                .chain(std::io::stderr())
        )
        // Info and below go to stdout
        .chain(
            fern::Dispatch::new()
                .level(LevelFilter::Info)
                .chain(std::io::stdout())
        )
        .apply()
        .unwrap();
    
    info!("Info to stdout");
    log::error!("Error to stderr");
}

Performance: Log Level Checks

use log::{info, debug, trace};
 
fn expensive_computation() -> String {
    // Simulate expensive work
    std::thread::sleep(std::time::Duration::from_millis(100));
    "result".to_string()
}
 
fn main() {
    env_logger::init();
    
    // BAD: expensive_computation() always runs
    debug!("Result: {}", expensive_computation());
    
    // GOOD: Check level first
    if log::log_enabled!(log::Level::Debug) {
        debug!("Result: {}", expensive_computation());
    }
}

Custom Logger Implementation

use log::{Log, Metadata, Record, LevelFilter};
 
struct SimpleLogger;
 
impl Log for SimpleLogger {
    fn enabled(&self, metadata: &Metadata) -> bool {
        metadata.level() <= LevelFilter::Debug
    }
    
    fn log(&self, record: &Record) {
        if self.enabled(record.metadata()) {
            println!("[{}] {}", record.level(), record.args());
        }
    }
    
    fn flush(&self) {
        // Flush any buffers
    }
}
 
fn main() {
    let logger = SimpleLogger;
    log::set_boxed_logger(Box::new(logger))
        .map(|()| log::set_max_level(LevelFilter::Debug))
        .unwrap();
    
    log::info!("Custom logger works!");
    log::debug!("Debug message");
}

Using in Libraries

// In your library crate:
use log::{debug, info, warn};
 
pub struct Config {
    pub debug_mode: bool,
}
 
impl Config {
    pub fn new() -> Self {
        debug!("Creating new config");
        Self { debug_mode: false }
    }
    
    pub fn validate(&self) -> Result<(), String> {
        info!("Validating config");
        if self.debug_mode {
            warn!("Debug mode enabled");
        }
        Ok(())
    }
}
 
// Note: Libraries should NOT initialize the logger!
// Let the application choose the backend.

Log Record Metadata

 
fn main() {
    env_logger::init();
    
    // Access record metadata in custom logger
    log::logger().log(
        &Record::builder()
            .args(format_args!("Custom log message"))
            .level(Level::Info)
            .target("my_target")
            .module_path_static(Some("my_app::main"))
            .file_static(Some(file!()))
            .line(Some(line!()))
            .build()
    );
}

Log Macros with Context

use log::{info, debug, error};
 
fn process_request(user_id: u64, action: &str) {
    debug!(user_id = user_id, action = action; "Processing request");
    
    // Do work...
    
    info!(user_id = user_id, action = action; "Request completed");
}
 
fn main() {
    env_logger::init();
    
    process_request(42, "update_profile");
}

Async-Safe Logging

use log::{info, error};
use std::sync::Arc;
use std::thread;
 
fn main() {
    env_logger::init();
    
    let data = Arc::new(vec![1, 2, 3]);
    
    let handles: Vec<_> = (0..3)
        .map(|i| {
            let data = Arc::clone(&data);
            thread::spawn(move || {
                info!("Thread {} processing", i);
                // Work...
                info!("Thread {} done", i);
            })
        })
        .collect();
    
    for handle in handles {
        handle.join().unwrap();
    }
}

Environment Variable Control

// In your Cargo.toml or shell:
// RUST_LOG=debug         - Show debug and above
// RUST_LOG=trace         - Show all logs
// RUST_LOG=my_app=debug  - Debug for specific module
// RUST_LOG=my_app::db=trace - Trace for db module
// RUST_LOG=warn,my_app=debug - Warn globally, debug for my_app
 
use log::{info, debug, trace};
 
fn main() {
    // Parse RUST_LOG environment variable
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
        .init();
    
    info!("Application started");
    debug!("Debug info (set RUST_LOG=debug to see)");
    trace!("Trace info (set RUST_LOG=trace to see)");
}

Testing with Logs

use log::{info, debug};
 
fn calculate(x: i32, y: i32) -> i32 {
    debug!("Calculating {} + {}", x, y);
    x + y
}
 
#[cfg(test)]
mod tests {
    use super::*;
    
    fn init_test_logger() {
        let _ = env_logger::builder()
            .is_test(true)
            .filter_level(log::LevelFilter::Debug)
            .try_init();
    }
    
    #[test]
    fn test_calculate() {
        init_test_logger();
        assert_eq!(calculate(2, 3), 5);
    }
}
 
fn main() {
    env_logger::init();
    println!("Result: {}", calculate(5, 7));
}

Rotation with chrono

use log::{info, LevelFilter};
 
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let today = chrono::Local::now().format("%Y-%m-%d");
    let log_path = format!("logs/app-{}.log", today);
    
    std::fs::create_dir_all("logs")?;
    let log_file = std::fs::File::create(&log_path)?;
    
    fern::Dispatch::new()
        .format(|out, message, record| {
            out.finish(format_args!(
                "[{} {}] {}",
                chrono::Local::now().format("%H:%M:%S"),
                record.level(),
                message
            ))
        })
        .level(LevelFilter::Info)
        .chain(log_file)
        .apply()?;
    
    info!("Logging to {}", log_path);
    
    Ok(())
}

Summary

Log Key Imports:

use log::{error, warn, info, debug, trace, Level, LevelFilter};

Log Levels (in order):

Level Macro Description
Error error! Errors, critical issues
Warn warn! Warnings, potential issues
Info info! General information
Debug debug! Debug information
Trace trace! Very detailed tracing

Common Backends:

Crate Description
env_logger Standard, uses RUST_LOG env var
simple_logger Simple setup
fern Highly configurable
flexi_logger Flexible, supports rotation

Environment Variables:

# Set log level
RUST_LOG=debug cargo run
 
# Per-module
RUST_LOG=my_app=debug,my_app::db=trace cargo run
 
# Multiple modules
RUST_LOG=warn,my_app=debug cargo run

Key Points:

  • Libraries should use log, not initialize logger
  • Applications choose the logging backend
  • Use log_enabled! for expensive computations
  • Default level filter discards logs below threshold
  • target: "name" for routing to specific outputs
  • Structured fields with key = value; syntax
  • env_logger::init() required to see logs
  • Test with env_logger::builder().is_test(true)

Best Practices:

  1. Use appropriate log levels
  2. Don't log sensitive data
  3. Use log_enabled! for expensive formatting
  4. Let library users choose backend
  5. Use structured fields for machine parsing
  6. Rotate log files for long-running apps