dobby_rs_framework/
logging.rs1use 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}