bin_common/
logging.rs

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        // set defaults
66        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        // set up logger
78        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        // create stdout logger
97        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        // set up file logger
111        #[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                // get old log files and log to two different log files
120                #[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                // always append to a single log file
151                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        // do this after the logger has been applied so the logger can be used
175        #[cfg(feature = "log_rotation")]
176        {
177            if !old_logs.is_empty() {
178                rotation::clean_up_old_logs(old_logs);
179            }
180        }
181
182        // hook panic handler if requested
183        #[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(); // The current implementation always returns Some
210    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}