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_LOG_FILE_LOG_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 DEFAULT_CONSOLE_LOG_LEVEL: log::LevelFilter = log::LevelFilter::Trace;
15
16#[cfg(not(debug_assertions))] // Set debug level for console in release builds
17const DEFAULT_CONSOLE_LOG_LEVEL: log::LevelFilter = log::LevelFilter::Warn;
18
19static LOG_FILE_LOG_LEVEL: LazyLock<std::sync::RwLock<log::LevelFilter>> =
20    LazyLock::new(|| std::sync::RwLock::new(DEFAULT_LOG_FILE_LOG_LEVEL));
21pub fn set_log_file_log_level(level: log::LevelFilter) {
22    *LOG_FILE_LOG_LEVEL.write().unwrap() = level;
23}
24pub fn get_log_file_log_level() -> log::LevelFilter {
25    *LOG_FILE_LOG_LEVEL.read().unwrap()
26}
27
28static CONSOLE_LOG_LEVEL: LazyLock<std::sync::RwLock<log::LevelFilter>> =
29    LazyLock::new(|| std::sync::RwLock::new(DEFAULT_CONSOLE_LOG_LEVEL));
30pub fn set_console_log_level(level: log::LevelFilter) {
31    *CONSOLE_LOG_LEVEL.write().unwrap() = level;
32}
33pub fn get_console_log_level() -> log::LevelFilter {
34    *CONSOLE_LOG_LEVEL.read().unwrap()
35}
36
37static CURRENT_LOG_FILE_HOLDER: OnceLock<PathBuf> = OnceLock::new();
38pub fn current_log_file() -> &'static PathBuf {
39    CURRENT_LOG_FILE_HOLDER.get().expect("init() must be called first")
40}
41
42#[derive(Default)]
43pub struct Builder {
44    level_for: HashMap<&'static str, log::LevelFilter>,
45    console_level_for: HashMap<&'static str, log::LevelFilter>,
46    without_console: bool,
47    dispatches: Vec<fern::Dispatch>,
48}
49
50impl Builder {
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    pub fn add_dispatch(mut self, dispatch: fern::Dispatch) -> Self {
56        self.dispatches.push(dispatch);
57        self
58    }
59
60    pub fn level_for(mut self, module: &'static str, level: log::LevelFilter) -> Self {
61        self.level_for.insert(module, level);
62        self
63    }
64
65    pub fn console_level_for(mut self, module: &'static str, level: log::LevelFilter) -> Self {
66        self.console_level_for.insert(module, level);
67        self
68    }
69
70    pub fn without_console(mut self) -> Self {
71        self.without_console = true;
72        self
73    }
74
75    fn add_log_level_for(
76        mut logger: fern::Dispatch,
77        levels_for: &HashMap<&'static str, log::LevelFilter>,
78    ) -> fern::Dispatch {
79        for (module, level) in levels_for {
80            logger = logger.level_for(*module, *level);
81        }
82        logger
83    }
84
85    fn build_with_panic_on_failure(&mut self, log_dir: &Path) {
86        // NOTE!!!
87        // Every error MUST be a panic here else the user will not be able to see the error!
88
89        let mut basic_logger = fern::Dispatch::new().format(|out, message, record| {
90            out.finish(format_args!("{} [{}] {}", record.level(), record.target(), message))
91        });
92        basic_logger = Self::add_log_level_for(basic_logger, &self.level_for);
93
94        {
95            // log file logger
96            let log_file = CURRENT_LOG_FILE_HOLDER
97                .get_or_init(|| log_dir.join(format!("{}.{}", super::about::about().binary_name, LOG_FILE_EXTENSION)));
98            std::fs::create_dir_all(log_dir).unwrap_or_else(|error| {
99                panic!(
100                    "Cannot create logging directory '{}': {:?}",
101                    log_dir.to_string_lossy(),
102                    error
103                )
104            });
105            let mut file_logger = fern::Dispatch::new()
106                .filter(|metadata| metadata.level() <= get_log_file_log_level())
107                .format(|out, message, record| {
108                    out.finish(format_args!("{} [{}] {}", record.level(), record.target(), message))
109                })
110                .chain(fern::log_file(log_file).unwrap_or_else(|error| {
111                    panic!("Cannot open log file '{}': {:?}", log_file.to_string_lossy(), error)
112                }));
113            file_logger = Self::add_log_level_for(file_logger, &self.level_for);
114
115            basic_logger = basic_logger.chain(file_logger)
116        }
117
118        if !self.without_console {
119            // console logger
120            let mut console_logger = fern::Dispatch::new()
121                .filter(|metadata| metadata.level() <= get_console_log_level())
122                .chain(std::io::stderr());
123            console_logger = Self::add_log_level_for(console_logger, &self.level_for);
124            console_logger = Self::add_log_level_for(console_logger, &self.console_level_for);
125
126            basic_logger = basic_logger.chain(console_logger);
127        }
128        for dispatch in std::mem::take(&mut self.dispatches) {
129            basic_logger = basic_logger.chain(dispatch);
130        }
131        basic_logger.apply().expect("Cannot start logging");
132    }
133
134    pub fn build(mut self, log_dir: &Path) -> Result<()> {
135        self.build_with_panic_on_failure(log_dir);
136        let about = super::about::about();
137
138        let log_file = current_log_file();
139        if !self.without_console {
140            // Currently not use translation. The log file name is wrapped to: <2068>log-file-name<2069>
141            // Some terminals copy these possibly invisible special characters when selecting and copying,
142            // so that the log file cannot be opened.
143            if false {
144                println!("{}", fl!("log-written-to", file_name = log_file.to_string_lossy()));
145            } else {
146                println!("Log is written to '{}'", log_file.to_string_lossy());
147            }
148        }
149
150        info!("Application: {} Version: {}", about.app_name, about.version);
151
152        Ok(())
153    }
154}