How to Use fern for Custom Log Configuration in Rust

Use `fern` to build a flexible logging pipeline by chaining configuration methods that define output destinations, formatting, and filtering levels, then initialize it once at the start of your application.

Use fern to build a flexible logging pipeline by chaining configuration methods that define output destinations, formatting, and filtering levels, then initialize it once at the start of your application. It works by creating a Dispatch object that can be cloned and passed to the log crate, allowing you to route logs to stdout, files, or both with custom formatters.

Here is a practical example of setting up a logger that writes to both the console and a file, with different formatting for each:

use fern::{colors::{Color, ColoredLevelConfig}, Dispatch};
use log::{info, LevelFilter};
use std::io;

fn main() -> Result<(), fern::InitError> {
    // Define color mapping for log levels
    let level_config = ColoredLevelConfig::new()
        .error(Color::Red)
        .warn(Color::Yellow)
        .info(Color::BrightBlack)
        .debug(Color::White);

    // Configure the logger
    let dispatch = Dispatch::new()
        // Console output with colors
        .chain(
            fern::Dispatch::new()
                .format(move |out, message, record| {
                    out.finish(format_args!(
                        "{} [{}] {}",
                        chrono::Local::now().format("[%Y-%m-%d %H:%M:%S]"),
                        record.level(),
                        message
                    ))
                })
                .level(LevelFilter::Info)
                .level_map(level_config)
                .chain(std::io::stdout())
        )
        // File output (plain text, no colors)
        .chain(
            fern::Dispatch::new()
                .format(|out, message, record| {
                    out.finish(format_args!(
                        "{} [{}] {}",
                        chrono::Local::now().format("[%Y-%m-%d %H:%M:%S]"),
                        record.level(),
                        message
                    ))
                })
                .level(LevelFilter::Debug)
                .chain(std::fs::File::create("app.log").expect("Failed to create log file"))
        )
        .level(LevelFilter::Debug);

    dispatch.apply()?;

    info!("Application started");
    log::debug!("Debugging details here");
    Ok(())
}

To use this, add fern, log, and chrono to your Cargo.toml. The key is that fern acts as a bridge; you must still call log::info!, log::error!, etc., in your code. The Dispatch object handles the routing. You can also chain multiple file outputs or use fern::log_file() for a quick setup if you don't need complex formatting logic. Remember to call dispatch.apply() before any logging macros are invoked, or the logs will be lost.