Skip to main content

dobby_rs_framework/
logging.rs

1use log::LevelFilter;
2use simplelog::{
3    ColorChoice, CombinedLogger, ConfigBuilder, SharedLogger, TermLogger, TerminalMode, WriteLogger,
4};
5use std::fs::File;
6use std::path::{Path, PathBuf};
7use std::sync::OnceLock;
8
9#[derive(Debug, Clone, Copy)]
10pub enum LogLevel {
11    Error,
12    Warn,
13    Info,
14    Debug,
15    Trace,
16}
17impl From<LogLevel> for LevelFilter {
18    fn from(v: LogLevel) -> Self {
19        match v {
20            LogLevel::Error => LevelFilter::Error,
21            LogLevel::Warn => LevelFilter::Warn,
22            LogLevel::Info => LevelFilter::Info,
23            LogLevel::Debug => LevelFilter::Debug,
24            LogLevel::Trace => LevelFilter::Trace,
25        }
26    }
27}
28
29#[derive(Debug, Clone)]
30pub enum LogOutput {
31    Terminal,
32    File(PathBuf),
33    Both(PathBuf),
34}
35
36#[derive(Debug, Clone)]
37pub struct LogOptions {
38    pub level: LogLevel,
39    pub output: LogOutput,
40}
41impl Default for LogOptions {
42    fn default() -> Self {
43        Self {
44            level: LogLevel::Info,
45            output: LogOutput::Terminal,
46        }
47    }
48}
49
50#[derive(Debug)]
51pub enum LoggingError {
52    AlreadyInitialized,
53    Io(std::io::Error),
54    SetLogger(log::SetLoggerError),
55}
56impl core::fmt::Display for LoggingError {
57    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
58        match self {
59            LoggingError::AlreadyInitialized => write!(f, "logger is already initialized"),
60            LoggingError::Io(e) => write!(f, "io error: {e}"),
61            LoggingError::SetLogger(e) => write!(f, "logger setup error: {e}"),
62        }
63    }
64}
65impl std::error::Error for LoggingError {}
66
67static LOG_INIT: OnceLock<()> = OnceLock::new();
68pub fn init_logging(options: LogOptions) -> Result<(), LoggingError> {
69    if LOG_INIT.get().is_some() {
70        return Err(LoggingError::AlreadyInitialized);
71    }
72    let mut b = ConfigBuilder::new();
73    b.set_time_level(LevelFilter::Info)
74        .set_thread_level(LevelFilter::Debug)
75        .set_target_level(LevelFilter::Debug)
76        .set_location_level(LevelFilter::Debug);
77    b.set_time_format_rfc3339();
78    let cfg = b.build();
79    let level = LevelFilter::from(options.level);
80    let mut loggers: Vec<Box<dyn SharedLogger>> = Vec::new();
81    match options.output {
82        LogOutput::Terminal => loggers.push(TermLogger::new(
83            level,
84            cfg.clone(),
85            TerminalMode::Mixed,
86            ColorChoice::Auto,
87        )),
88        LogOutput::File(path) => {
89            loggers.push(WriteLogger::new(level, cfg.clone(), open_log_file(&path)?))
90        }
91        LogOutput::Both(path) => {
92            loggers.push(TermLogger::new(
93                level,
94                cfg.clone(),
95                TerminalMode::Mixed,
96                ColorChoice::Auto,
97            ));
98            loggers.push(WriteLogger::new(level, cfg.clone(), open_log_file(&path)?));
99        }
100    }
101    CombinedLogger::init(loggers).map_err(LoggingError::SetLogger)?;
102    let _ = LOG_INIT.set(());
103    Ok(())
104}
105
106fn open_log_file(path: &Path) -> Result<File, LoggingError> {
107    if let Some(parent) = path.parent()
108        && !parent.as_os_str().is_empty()
109    {
110        std::fs::create_dir_all(parent).map_err(LoggingError::Io)?;
111    }
112    File::create(path).map_err(LoggingError::Io)
113}