1use std::path::PathBuf;
2
3use directories::{BaseDirs, ProjectDirs};
4use fern::colors::{Color, ColoredLevelConfig};
5
6use crate::CrateSetup;
7
8#[cfg(feature = "log_rotation")]
9mod rotation;
10
11pub struct LoggingSetupBuilder<'a> {
12 crate_setup: &'a CrateSetup,
13 verbosity: Option<i8>,
14 log_to_file: Option<bool>,
15 log_filters: Vec<Box<fern::Filter>>,
16 #[cfg(feature = "log_rotation")]
17 log_rotation: Option<bool>,
18 #[cfg(feature = "panic_handler")]
19 log_panics: Option<bool>,
20}
21
22impl<'a> LoggingSetupBuilder<'a> {
23 pub fn new(crate_setup: &'a CrateSetup) -> Self {
24 Self {
25 crate_setup,
26 verbosity: None,
27 log_to_file: None,
28 log_filters: Vec::new(),
29 log_rotation: None,
30 log_panics: None,
31 }
32 }
33
34 pub fn with_verbosity(mut self, verbosity: i8) -> Self {
35 self.verbosity = Some(verbosity);
36 self
37 }
38
39 pub fn with_log_to_file(mut self, log_to_file: bool) -> Self {
40 self.log_to_file = Some(log_to_file);
41 self
42 }
43
44 pub fn with_filter<F>(mut self, filter: F) -> Self
45 where
46 F: Fn(&log::Metadata) -> bool + Send + Sync + 'static,
47 {
48 self.log_filters.push(Box::new(filter));
49 self
50 }
51
52 #[cfg(feature = "log_rotation")]
53 pub fn with_log_rotation(mut self, log_rotation: bool) -> Self {
54 self.log_rotation = Some(log_rotation);
55 self
56 }
57
58 #[cfg(feature = "panic_handler")]
59 pub fn with_log_panics(mut self, log_panics: bool) -> Self {
60 self.log_panics = Some(log_panics);
61 self
62 }
63
64 pub fn build(self) -> Result<(), fern::InitError> {
65 let verbosity = self
67 .verbosity
68 .unwrap_or_else(|| if cfg!(debug_assertions) { 2 } else { 1 });
69 let log_to_file = self.log_to_file.unwrap_or(false);
70 #[cfg(feature = "log_rotation")]
71 let log_rotation = self.log_rotation.unwrap_or(false);
72 #[cfg(not(feature = "log_rotation"))]
73 let log_rotation = false;
74 #[cfg(feature = "panic_handler")]
75 let log_panics = self.log_panics.unwrap_or(false);
76
77 let colors = ColoredLevelConfig::new()
79 .error(Color::Red)
80 .warn(Color::Yellow)
81 .info(Color::Green)
82 .debug(Color::White)
83 .trace(Color::BrightBlack);
84 let mut logger = fern::Dispatch::new();
85 logger = match verbosity {
86 e if e < 0 => logger.level(log::LevelFilter::Error),
87 0 => logger.level(log::LevelFilter::Warn),
88 1 => logger.level(log::LevelFilter::Info),
89 2 => logger.level(log::LevelFilter::Debug),
90 _ => logger.level(log::LevelFilter::Trace),
91 };
92 for filter in self.log_filters {
93 logger = logger.filter(filter);
94 }
95
96 let stdout_logger = fern::Dispatch::new()
98 .format(move |out, message, record| {
99 out.finish(format_args!(
100 "{}[{}][{}][{}] {}\x1B[0m",
101 format_args!("\x1B[{}m", colors.get_color(&record.level()).to_fg_str()),
102 chrono::Local::now().format("%H:%M:%S"),
103 record.level(),
104 record.target(),
105 message
106 ))
107 })
108 .chain(std::io::stdout());
109
110 #[allow(unused_mut, unused_variables)]
112 let mut old_logs = Vec::<PathBuf>::new();
113 if log_to_file {
114 let log_dir = get_log_path(
115 self.crate_setup.base_dirs(),
116 self.crate_setup.project_dirs(),
117 );
118 if log_rotation {
119 #[cfg(feature = "log_rotation")]
121 {
122 old_logs = rotation::get_old_log_files(&log_dir);
123 let log_file_latest = log_dir.join("latest.log");
124 let log_file_time = log_dir.join(
125 format_args!("{}.log", chrono::Local::now().format("%Y.%m.%d.%H.%M.%S"))
126 .to_string(),
127 );
128 logger = logger.chain(
129 fern::Dispatch::new()
130 .format(|out, message, record| {
131 out.finish(format_args!(
132 "[{}][{}][{}] {}",
133 chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
134 record.level(),
135 record.target(),
136 message
137 ))
138 })
139 .chain(
140 std::fs::OpenOptions::new()
141 .truncate(true)
142 .write(true)
143 .create(true)
144 .open(log_file_latest)?,
145 )
146 .chain(fern::log_file(log_file_time)?),
147 );
148 }
149 } else {
150 let log_file = log_dir.join(format!("{}.log", self.crate_setup.application_name()));
152 logger = logger.chain(
153 fern::Dispatch::new()
154 .format(|out, message, record| {
155 out.finish(format_args!(
156 "[{}][{}][{}] {}",
157 chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
158 record.level(),
159 record.target(),
160 message
161 ))
162 })
163 .chain(
164 std::fs::OpenOptions::new()
165 .append(true)
166 .create(true)
167 .open(log_file)?,
168 ),
169 );
170 }
171 }
172 logger.chain(stdout_logger).apply()?;
173
174 #[cfg(feature = "log_rotation")]
176 {
177 if !old_logs.is_empty() {
178 rotation::clean_up_old_logs(old_logs);
179 }
180 }
181
182 #[cfg(feature = "panic_handler")]
184 {
185 if log_panics {
186 std::panic::set_hook(Box::new(logging_panic_handler));
187 }
188 }
189 Ok(())
190 }
191}
192
193fn get_log_path(base_dirs: &BaseDirs, project_dirs: &ProjectDirs) -> PathBuf {
194 let dir = if cfg!(target_os = "macos") {
195 base_dirs
196 .home_dir()
197 .join("Library")
198 .join("Logs")
199 .join(project_dirs.project_path())
200 } else {
201 project_dirs.data_local_dir().join("logs")
202 };
203 std::fs::create_dir_all(&dir).expect("Failed to create log dir");
204 dir
205}
206
207#[cfg(feature = "panic_handler")]
208pub fn logging_panic_handler(info: &std::panic::PanicInfo) {
209 let location = info.location().unwrap(); let msg = match info.payload().downcast_ref::<&'static str>() {
211 Some(s) => *s,
212 None => match info.payload().downcast_ref::<String>() {
213 Some(s) => &s[..],
214 None => "Box<Any>",
215 },
216 };
217 let thread = std::thread::current();
218 let name = thread.name().unwrap_or("<unnamed>");
219
220 log::error!("thread '{}' panicked at '{}', {}", name, msg, location);
221
222 let mut frame_counter = 0;
223
224 backtrace::trace(|frame| {
225 let ip = frame.ip();
226 backtrace::resolve(ip, |symbol| {
227 let name = symbol
228 .name()
229 .map(|name| name.to_string())
230 .unwrap_or_else(|| "<unnamed>".into());
231 let addr = symbol.addr().unwrap_or_else(std::ptr::null_mut);
232 let filename = symbol.filename().map_or_else(
233 || "<unknown source>".into(),
234 |path| path.to_string_lossy().into_owned(),
235 );
236 let line_number = symbol.lineno().unwrap_or(0);
237
238 log::error!(
239 "#{}: {:?}:{} at {}:{}",
240 frame_counter,
241 addr,
242 name,
243 filename,
244 line_number
245 );
246 });
247 frame_counter += 1;
248 true
249 });
250}