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)] const CONSOLE_LEVEL: log::LevelFilter = log::LevelFilter::Trace;
17
18#[cfg(not(debug_assertions))] const 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 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 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 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 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}