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