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)] const CONSOLE_LEVEL: log::LevelFilter = log::LevelFilter::Trace;
15
16#[cfg(not(debug_assertions))] const 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
35#[derive(Default)]
36pub struct Builder {
37 level_for: HashMap<&'static str, log::LevelFilter>,
38 console_level_for: HashMap<&'static str, log::LevelFilter>,
39 without_stderr: bool,
40}
41
42impl Builder {
43 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn level_for(mut self, module: &'static str, level: log::LevelFilter) -> Self {
48 self.level_for.insert(module, level);
49 self
50 }
51
52 pub fn console_level_for(mut self, module: &'static str, level: log::LevelFilter) -> Self {
53 self.console_level_for.insert(module, level);
54 self
55 }
56
57 pub fn without_stderr(mut self) -> Self {
58 self.without_stderr = true;
59 self
60 }
61
62 fn build_with_panic_on_failure(&self, log_dir: &Path) {
63 let mut logger = fern::Dispatch::new().level(DEFAULT_LEVEL);
66 for (module, level) in &self.level_for {
67 logger = logger.level_for(*module, *level);
68 }
69 logger = logger
70 .filter(|metadata| metadata.level() <= *LOG_RECEIVER_LOG_LEVEL.read().unwrap())
71 .format(|out, message, record| {
72 out.finish(format_args!("{} [{}] {}", record.level(), record.target(), message))
73 });
74 let log_file = CURRENT_LOG_FILE_HOLDER
75 .get_or_init(|| log_dir.join(format!("{}.{}", super::about::about().binary_name, LOG_FILE_EXTENSION)));
76
77 std::fs::create_dir_all(log_dir).unwrap_or_else(|error| {
78 panic!(
79 "Cannot create logging directory '{}': {:?}",
80 log_dir.to_string_lossy(),
81 error
82 )
83 });
84 logger = logger.chain(
85 fern::log_file(log_file)
86 .unwrap_or_else(|error| panic!("Cannot open log file '{}': {:?}", log_file.to_string_lossy(), error)),
87 );
88 if !self.without_stderr {
89 let mut console_logger = fern::Dispatch::new().level(CONSOLE_LEVEL).chain(std::io::stderr());
90 for (module, level) in &self.level_for {
91 console_logger = console_logger.level_for(*module, *level);
92 }
93 for (module, level) in &self.console_level_for {
94 console_logger = console_logger.level_for(*module, *level);
95 }
96 logger = logger.chain(console_logger);
97 }
98 logger.apply().expect("Cannot start logging");
99 }
100
101 pub fn build(self, log_dir: &Path) -> Result<()> {
102 self.build_with_panic_on_failure(log_dir);
103 let about = super::about::about();
104
105 let log_file = current_log_file();
106 if !self.without_stderr {
107 if false {
111 println!("{}", fl!("log-written-to", file_name = log_file.to_string_lossy()));
112 } else {
113 println!("Log is written to '{}'", log_file.to_string_lossy());
114 }
115 }
116
117 info!("{} {}", about.app_name, about.version);
118 info!("Log is written to '{}'", log_file.to_string_lossy());
119
120 Ok(())
121 }
122}