mxl_base/
logging.rs

1use crate::localization::helper::fl;
2use anyhow::Result;
3use log::*;
4use std::{
5    collections::HashMap,
6    path::{Path, PathBuf},
7    sync::{LazyLock, OnceLock},
8};
9
10const DEFAULT_LEVEL: log::LevelFilter = log::LevelFilter::Trace;
11const LOG_FILE_EXTENSION: &str = "log";
12
13#[cfg(debug_assertions)] // Set debug level for console in debug builds
14const CONSOLE_LEVEL: log::LevelFilter = log::LevelFilter::Trace;
15
16#[cfg(not(debug_assertions))] // Set debug level for console in release builds
17const CONSOLE_LEVEL: log::LevelFilter = log::LevelFilter::Warn;
18
19static LOG_RECEIVER_LOG_LEVEL: LazyLock<std::sync::RwLock<log::LevelFilter>> =
20    LazyLock::new(|| std::sync::RwLock::new(DEFAULT_LEVEL));
21
22pub fn set_log_level(level: log::LevelFilter) {
23    *LOG_RECEIVER_LOG_LEVEL.write().unwrap() = level;
24}
25
26pub fn get_log_level() -> log::LevelFilter {
27    *LOG_RECEIVER_LOG_LEVEL.read().unwrap()
28}
29
30static CURRENT_LOG_FILE_HOLDER: OnceLock<PathBuf> = OnceLock::new();
31pub fn current_log_file() -> &'static PathBuf {
32    CURRENT_LOG_FILE_HOLDER.get().expect("init() must be called first")
33}
34
35pub struct Builder {
36    level_for: HashMap<&'static str, log::LevelFilter>,
37    console_level_for: HashMap<&'static str, log::LevelFilter>,
38    without_stderr: bool,
39}
40
41impl Default for Builder {
42    fn default() -> Self {
43        Self {
44            level_for: HashMap::new(),
45            console_level_for: HashMap::new(),
46            without_stderr: false,
47        }
48    }
49}
50
51impl Builder {
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    pub fn level_for(mut self, module: &'static str, level: log::LevelFilter) -> Self {
57        self.level_for.insert(module, level);
58        self
59    }
60
61    pub fn console_level_for(mut self, module: &'static str, level: log::LevelFilter) -> Self {
62        self.console_level_for.insert(module, level);
63        self
64    }
65
66    pub fn without_stderr(mut self) -> Self {
67        self.without_stderr = true;
68        self
69    }
70
71    fn build_with_panic_on_failure(&self, log_dir: &Path) {
72        // NOTE!!!
73        // Every error MUST be a panic here else the user will not be able to see the error!
74        let mut logger = fern::Dispatch::new().level(DEFAULT_LEVEL);
75        for (module, level) in &self.level_for {
76            logger = logger.level_for(*module, *level);
77        }
78        logger = logger
79            .filter(|metadata| metadata.level() <= *LOG_RECEIVER_LOG_LEVEL.read().unwrap())
80            .format(|out, message, record| {
81                out.finish(format_args!("{} [{}] {}", record.level(), record.target(), message))
82            });
83        let log_file = CURRENT_LOG_FILE_HOLDER
84            .get_or_init(|| log_dir.join(format!("{}.{}", super::about::about().binary_name, LOG_FILE_EXTENSION)));
85
86        std::fs::create_dir_all(log_dir).unwrap_or_else(|error| {
87            panic!(
88                "Cannot create logging directory '{}': {:?}",
89                log_dir.to_string_lossy(),
90                error
91            )
92        });
93        logger = logger.chain(
94            fern::log_file(log_file)
95                .unwrap_or_else(|error| panic!("Cannot open log file '{}': {:?}", log_file.to_string_lossy(), error)),
96        );
97        if !self.without_stderr {
98            let mut console_logger = fern::Dispatch::new().level(CONSOLE_LEVEL).chain(std::io::stderr());
99            for (module, level) in &self.level_for {
100                console_logger = console_logger.level_for(*module, *level);
101            }
102            for (module, level) in &self.console_level_for {
103                console_logger = console_logger.level_for(*module, *level);
104            }
105            logger = logger.chain(console_logger);
106        }
107        logger.apply().expect("Cannot start logging");
108    }
109
110    pub fn build(self, log_dir: &Path) -> Result<()> {
111        self.build_with_panic_on_failure(log_dir);
112        let about = super::about::about();
113
114        let log_file = current_log_file();
115        if !self.without_stderr {
116            // Currently not use translation. The log file name is wrapped to: <2068>log-file-name<2069>
117            // Some terminals copy these possibly invisible special characters when selecting and copying,
118            // so that the log file cannot be opened.
119            if false {
120                println!("{}", fl!("log-written-to", file_name = log_file.to_string_lossy()));
121            } else {
122                println!("Log is written to '{}'", log_file.to_string_lossy());
123            }
124        }
125
126        info!("{} {}", about.app_name, about.version);
127        info!("Log is written to '{}'", log_file.to_string_lossy());
128
129        Ok(())
130    }
131}