daboross / fern

Simple, efficient logging for Rust
MIT License
848 stars 51 forks source link

A way to change log level in runtime? #76

Open dmilith opened 3 years ago

dmilith commented 3 years ago

I wish to change the log level in runtime without restarting my long-running service. Is there a way of doing so?

daboross commented 3 years ago

Hi!

There isn't a built-in way, but it's fairly possible to build this on top of fern & something to synchronize the level get/set. Something like the following should work:

lazy_static! {
    static LOG_LEVEL: RwLock<log::LevelFilter> = RwLock::new(log::LevelFilter::Off);
}

fn set_log_level(level: log::LevelFilter) {
    *LOG_LEVEL.write() = level;
}

fn setup_logging() -> Result<(), fern::InitError> {
    fern::Dispatch::new()
        .filter(|metadata| {
            metadata.level() < *LOG_LEVEL.read()
        })
        .format(|out, message, record| {
            out.finish(format_args!(
                "{}[{}][{}] {}",
                chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
                record.target(),
                record.level(),
                message
            ))
        })
        .chain(fern::log_file("program.log")?)
        .apply()?;
    Ok(())
}

I don't plan on adding this to fern natively for performance reasons, but if you get this working, it could be a great example to add.

dmilith commented 3 years ago

Ha! It works. I had to add unwrap() for RwLock since it's Result, but the key here is not to set .level() in initialization chain. Then it works like a charm! Thank you very much!

daboross commented 3 years ago

Nice! Glad you got it working.

I'll leave this open for the sake of documenting this somewhere, or making an example of it, if that's alright.

dmilith commented 3 years ago

I did it this way:

use std::sync::RwLock;

lazy_static! {
    static ref LOG_LEVEL: RwLock<LevelFilter> = RwLock::new(LevelFilter::Info);
}

/// Set log level dynamically at runtime
fn set_log_level() {
    let level = Config::load().get_log_level();
    match LOG_LEVEL.read() {
        Ok(loglevel) => {
            if level != *loglevel {
                drop(loglevel);
                match LOG_LEVEL.write() {
                    Ok(mut log) => {
                        println!("Changing log level to: {}", level);
                        *log = level
                    }
                    Err(err) => {
                        println!("Failed to change log level to: {}, cause: {}", level, err);
                    }
                }
            }
        }
        Err(_) => {}
    }
}

fn setup_logger() -> Result<(), SetLoggerError> {
    let log_file = Config::load()
        .log_file
        .unwrap_or_else(|| String::from("krecik.log"));
    let colors_line = ColoredLevelConfig::new()
        .error(Color::Red)
        .warn(Color::Yellow)
        .info(Color::White)
        .debug(Color::Magenta)
        .trace(Color::Cyan);
    Dispatch::new()
        .filter(|metadata| {
            match LOG_LEVEL.read() {
                Ok(log) => metadata.level() <= *log,
                Err(_err) => true,
            }
        })
        .format(move |out, message, record| {
            out.finish(format_args!(
                "{color_line}[{date}][{target}][{level}{color_line}] {message}\x1B[0m",
                color_line = format_args!(
                    "\x1B[{}m",
                    colors_line.get_color(&record.level()).to_fg_str()
                ),
                date = Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
                target = record.target(),
                level = record.level(),
                message = message
            ))
        })
        // .level(level) -- it's very important to not do this, otherwise level never changes in runtime!!
        .chain(std::io::stdout())
        .chain(fern::DateBased::new(format!("{}.", log_file), "%Y-%m-%d"))
        .apply()
}

and then I just run

set_log_level();

in my server main loop :)