lib_flutter_rust_bridge_codegen/library/utils/
logs.rs

1//! Utilities related to logging
2
3use crate::utils::console::MULTI_PROGRESS;
4use fern::colors::{Color, ColoredLevelConfig};
5use log::LevelFilter;
6use std::io::IsTerminal;
7
8/// Configure an opinionated way of logging.
9///
10/// This is just one way of outputing logs, and users are free to use this function
11/// or choose their own way of outputing logs. That's why this function is "opinionated".
12///
13/// It will log to file and standard output.
14/// All logs with level `debug`(with parameter `verbose`=true or system variable `RUST_LOG`="debug") or above
15/// will be recorded in `./logs/<date>.log`.
16/// Logs with level `info` and above will be output to standard output, with colored tag.
17///
18/// # Example
19///
20/// ```
21/// use lib_flutter_rust_bridge_codegen::utils::logs::configure_opinionated_logging;
22/// configure_opinionated_logging("./logs/", false).expect("failed to initialize log");
23/// ```
24pub fn configure_opinionated_logging(path: &str, verbose: bool) -> Result<(), fern::InitError> {
25    let level_filter = log_level_from_env_var().unwrap_or_else(|| verbose_to_level_filter(verbose));
26
27    if level_filter == LevelFilter::Debug {
28        std::fs::create_dir_all(path).unwrap();
29    }
30
31    let mut fern_logger = fern::Dispatch::new();
32    fern_logger = log_format_simple(fern_logger);
33    fern_logger = match level_filter {
34        LevelFilter::Debug => fern_logger
35            .level(LevelFilter::Debug)
36            .chain(fern::DateBased::new(path, "%Y-%m-%d.log"))
37            .chain(std::io::stdout()),
38        LevelFilter::Info => fern_logger
39            .level(LevelFilter::Info)
40            .level_for("cbindgen", LevelFilter::Error)
41            .chain(std::io::stdout()),
42        _ => fern_logger.level(level_filter).chain(std::io::stdout()),
43    };
44
45    let (max_level, fern_logger) = fern_logger.into_log();
46    let log_wrapper = indicatif_log_bridge::LogWrapper::new(MULTI_PROGRESS.clone(), fern_logger);
47
48    // ref: fern.apply / LogWrapper.try_init
49    log::set_boxed_logger(Box::new(log_wrapper))?;
50    log::set_max_level(max_level);
51
52    let prev = std::panic::take_hook();
53    std::panic::set_hook(Box::new(move |info| {
54        log::error!("{}", info);
55        prev(info);
56    }));
57
58    Ok(())
59}
60
61fn log_level_from_env_var() -> Option<LevelFilter> {
62    (std::env::var("RUST_LOG").ok()).map(|value| log_level_from_str(&value))
63}
64
65fn log_level_from_str(value: &str) -> LevelFilter {
66    match value {
67        "trace" => LevelFilter::Trace,
68        "debug" => LevelFilter::Debug,
69        "info" => LevelFilter::Info,
70        "warn" => LevelFilter::Warn,
71        "error" => LevelFilter::Error,
72        "off" => LevelFilter::Off,
73        // frb-coverage:ignore-start
74        _ => panic!("{}", "unknown RUST_LOG level: {value}"),
75        // frb-coverage:ignore-end
76    }
77}
78
79fn verbose_to_level_filter(verbose: bool) -> LevelFilter {
80    if verbose {
81        LevelFilter::Debug
82    } else {
83        LevelFilter::Info
84    }
85}
86
87fn log_format_simple(d: fern::Dispatch) -> fern::Dispatch {
88    let colored_output = ColoredLevelConfig::new()
89        .error(Color::Red)
90        .warn(Color::Yellow)
91        .info(Color::Green)
92        .debug(Color::Blue)
93        .trace(Color::BrightBlack);
94
95    d.format(move |out, message, record| {
96        let time = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ");
97
98        let level = record.level();
99        let level = if std::io::stdout().is_terminal() {
100            colored_output.color(level).to_string()
101        } else {
102            level.to_string()
103        };
104
105        out.finish(format_args!(
106            "[{} {} {}:{}] {}",
107            time,
108            level,
109            record.file().unwrap_or(""),
110            record.line().unwrap_or(0),
111            message
112        ))
113    })
114}
115
116/// Configure an opinionated way of logging, useful in tests.
117pub fn configure_opinionated_test_logging() {
118    // https://github.com/daboross/fern/issues/54
119    // This will fail if called twice; don't worry.
120    let _ = log_format_simple(fern::Dispatch::new())
121        .level(log_level_from_env_var().unwrap_or(LevelFilter::Debug))
122        .chain(fern::Output::call(|record| println!("{}", record.args())))
123        .apply();
124}
125
126#[cfg(test)]
127mod tests {
128    use crate::utils::logs::log_level_from_str;
129    use log::LevelFilter;
130
131    #[test]
132    pub fn test_log_level_from_str() {
133        assert_eq!(log_level_from_str("trace"), LevelFilter::Trace);
134        assert_eq!(log_level_from_str("debug"), LevelFilter::Debug);
135        assert_eq!(log_level_from_str("info"), LevelFilter::Info);
136        assert_eq!(log_level_from_str("warn"), LevelFilter::Warn);
137        assert_eq!(log_level_from_str("error"), LevelFilter::Error);
138        assert_eq!(log_level_from_str("off"), LevelFilter::Off);
139    }
140}