mxl_base/
logging.rs

1use crate::localization::helper::fl;
2use anyhow::{Context, Result};
3use log::*;
4use std::{
5    path::{Path, PathBuf},
6    sync::{LazyLock, OnceLock},
7};
8
9const KEEP_NUMBER_OF_FILES: usize = 20;
10const DEFAULT_LEVEL: log::LevelFilter = log::LevelFilter::Trace;
11const LOG_FILE_EXTENSION: &str = "log";
12const LOG_DIR_GENERIC: &str = "log";
13const LOG_FILE_FMT: &str = const_format::formatcp!("%Y-%m-%d_%H_%M_%S.{}", LOG_FILE_EXTENSION);
14
15#[cfg(debug_assertions)] // Set debug level for console in debug builds
16const CONSOLE_LEVEL: log::LevelFilter = log::LevelFilter::Trace;
17
18#[cfg(not(debug_assertions))] // Set debug level for console in release builds
19const CONSOLE_LEVEL: log::LevelFilter = log::LevelFilter::Warn;
20
21static LOG_RECEIVER_LOG_LEVEL: LazyLock<std::sync::RwLock<log::LevelFilter>> =
22    LazyLock::new(|| std::sync::RwLock::new(DEFAULT_LEVEL));
23
24pub fn set_log_level(level: log::LevelFilter) {
25    *LOG_RECEIVER_LOG_LEVEL.write().unwrap() = level;
26}
27
28pub fn get_log_level() -> log::LevelFilter {
29    *LOG_RECEIVER_LOG_LEVEL.read().unwrap()
30}
31
32static CURRENT_LOG_FILE_HOLDER: OnceLock<PathBuf> = OnceLock::new();
33pub fn current_log_file() -> &'static PathBuf {
34    CURRENT_LOG_FILE_HOLDER.get().expect("init() must be called first")
35}
36
37pub struct Builder {
38    logger: Option<fern::Dispatch>,
39    without_stderr: bool,
40    without_generic_log_dir: bool,
41}
42
43impl Default for Builder {
44    fn default() -> Self {
45        Self {
46            logger: Some(fern::Dispatch::new().level(DEFAULT_LEVEL)),
47            without_stderr: false,
48            without_generic_log_dir: false,
49        }
50    }
51}
52
53impl Builder {
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    pub fn level_for<T: Into<std::borrow::Cow<'static, str>>>(mut self, module: T, level: log::LevelFilter) -> Self {
59        self.logger = Some(self.logger.unwrap().level_for(module, level));
60        self
61    }
62
63    pub fn without_stderr(mut self) -> Self {
64        self.without_stderr = true;
65        self
66    }
67
68    pub fn without_generic_log_dir(mut self) -> Self {
69        self.without_generic_log_dir = true;
70        self
71    }
72
73    fn generic_log_dir(&self) -> &'static PathBuf {
74        static DIR: OnceLock<PathBuf> = OnceLock::new();
75        DIR.get_or_init(|| {
76            super::misc::project_dirs()
77                .data_local_dir()
78                .join(std::path::Path::new(LOG_DIR_GENERIC))
79        })
80    }
81
82    fn generic_log_file(&self) -> &'static PathBuf {
83        static NAME: OnceLock<PathBuf> = OnceLock::new();
84        NAME.get_or_init(|| {
85            self.generic_log_dir().join(format!(
86                "{}_{}",
87                super::about::about().binary_name,
88                chrono::Local::now().format(LOG_FILE_FMT)
89            ))
90        })
91    }
92
93    fn build_with_panic_on_failure(&mut self, log_dir: &Path) {
94        // NOTE!!!
95        // Every error MUST be a panic here else the user will not be able to see the error!
96        let mut logger = self
97            .logger
98            .take()
99            .unwrap()
100            .filter(|metadata| metadata.level() <= *LOG_RECEIVER_LOG_LEVEL.read().unwrap())
101            .format(|out, message, record| {
102                out.finish(format_args!("{} [{}] {}", record.level(), record.target(), message))
103            });
104        let log_file = CURRENT_LOG_FILE_HOLDER
105            .get_or_init(|| log_dir.join(format!("{}.{}", super::about::about().binary_name, LOG_FILE_EXTENSION)));
106
107        std::fs::create_dir_all(log_dir).unwrap_or_else(|error| {
108            panic!(
109                "Cannot create logging directory '{}': {:?}",
110                log_dir.to_string_lossy(),
111                error
112            )
113        });
114        logger = logger.chain(
115            fern::log_file(log_file)
116                .unwrap_or_else(|error| panic!("Cannot open log file '{}': {:?}", log_file.to_string_lossy(), error)),
117        );
118        if !self.without_stderr {
119            logger = logger.chain(fern::Dispatch::new().level(CONSOLE_LEVEL).chain(std::io::stderr()));
120        }
121        if !self.without_generic_log_dir {
122            let log_dir = self.generic_log_dir();
123            std::fs::create_dir_all(log_dir).unwrap_or_else(|error| {
124                panic!(
125                    "Cannot create logging directory '{}': {:?}",
126                    log_dir.to_string_lossy(),
127                    error
128                )
129            });
130            let log_file = self.generic_log_file();
131            logger =
132                logger.chain(fern::log_file(log_file).unwrap_or_else(|error| {
133                    panic!("Cannot open log file '{}': {:?}", log_file.to_string_lossy(), error)
134                }));
135        }
136        logger.apply().expect("Cannot start logging");
137    }
138
139    fn cleanup_logfiles(binary_name: &str, path: &std::path::Path) -> Result<()> {
140        // Collect all matching logfiles in the directory:
141        let log_file_extension = std::ffi::OsString::from(LOG_FILE_EXTENSION);
142        let mut log_files = std::fs::read_dir(path)
143            .with_context(|| format!("Cannot list log directory '{}'", path.to_string_lossy()))?
144            .filter_map(|file| {
145                match file {
146                    Ok(entry) => {
147                        let path = entry.path();
148                        if path.is_file()
149                            && !path.is_symlink()
150                            && path.starts_with(binary_name)
151                            && path.extension() == Some(log_file_extension.as_os_str())
152                        {
153                            return Some(path);
154                        }
155                    }
156                    Err(error) => warn!("Cannot read log file: {error}"),
157                }
158                None
159            })
160            .collect::<Vec<_>>();
161
162        // Remove all logfiles that exceed the number of files to preserve:
163        if log_files.len() > KEEP_NUMBER_OF_FILES {
164            log_files.sort();
165            let mut len = log_files.len();
166            for file in log_files.iter() {
167                match std::fs::remove_file(file) {
168                    Ok(_) => {
169                        trace!("Removed logfile {file:?}");
170                        len -= 1;
171                        if len <= KEEP_NUMBER_OF_FILES {
172                            break;
173                        }
174                    }
175                    Err(error) => warn!("Cannot remove log file '{}': {}", file.to_string_lossy(), error),
176                }
177            }
178        }
179        Ok(())
180    }
181
182    pub fn build(mut self, log_dir: &Path) -> Result<()> {
183        self.build_with_panic_on_failure(log_dir);
184        let about = super::about::about();
185
186        if !self.without_generic_log_dir {
187            #[cfg(target_family = "unix")]
188            {
189                let log_dir = self.generic_log_dir();
190                let symlink = log_dir.join(format!("{}.{}", about.binary_name, LOG_FILE_EXTENSION));
191                _ = std::fs::remove_file(&symlink);
192                let log_file = self.generic_log_file();
193                _ = std::os::unix::fs::symlink(log_file, &symlink);
194            }
195        }
196        let log_file = current_log_file();
197        if !self.without_stderr {
198            // Currently not use translation. The log file name is wrapped to: <2068>log-file-name<2069>
199            // Some terminals copy these possibly invisible special characters when selecting and copying,
200            // so that the log file cannot be opened.
201            if false {
202                println!("{}", fl!("log-written-to", file_name = log_file.to_string_lossy()));
203            } else {
204                println!("Log is written to '{}'", log_file.to_string_lossy());
205            }
206        }
207
208        info!("{} {}", about.app_name, about.version);
209        info!("Log is written to '{}'", log_file.to_string_lossy());
210
211        if !self.without_generic_log_dir {
212            Self::cleanup_logfiles(about.binary_name, self.generic_log_dir().as_path())?;
213        }
214
215        Ok(())
216    }
217}