dm_database_sqllog2db/
logging.rs1use crate::config::LoggingConfig;
2use crate::constants::LOG_LEVELS;
3use crate::error::{Error, FileError, Result};
4use chrono::Local;
5use log::SetLoggerError;
6use log::{Level, LevelFilter, Metadata, Record};
7use std::collections::HashMap;
8use std::fs::OpenOptions;
9use std::io::Write;
10use std::path::Path;
11use std::sync::{Arc, LazyLock, Mutex};
12
13static LOG_LEVEL_MAP: LazyLock<HashMap<&'static str, LevelFilter>> = LazyLock::new(|| {
15 let mut map = HashMap::with_capacity(5);
16 map.insert("trace", LevelFilter::Trace);
17 map.insert("debug", LevelFilter::Debug);
18 map.insert("info", LevelFilter::Info);
19 map.insert("warn", LevelFilter::Warn);
20 map.insert("error", LevelFilter::Error);
21 map
22});
23
24static LOG_TO_CONSOLE: LazyLock<Mutex<bool>> = LazyLock::new(|| Mutex::new(true));
26
27#[cfg(feature = "tui")]
29pub fn set_log_to_console(enabled: bool) {
30 if let Ok(mut console_enabled) = LOG_TO_CONSOLE.lock() {
31 *console_enabled = enabled;
32 }
33}
34
35pub fn init_logging(config: &LoggingConfig) -> Result<()> {
37 let level = parse_log_level(&config.level)?;
39
40 let log_path = Path::new(&config.file);
42 let parent_dir = log_path.parent().ok_or_else(|| {
43 Error::File(FileError::CreateDirectoryFailed {
44 path: log_path.to_path_buf(),
45 reason: "Failed to get parent directory".to_string(),
46 })
47 })?;
48
49 if !parent_dir.exists() {
51 std::fs::create_dir_all(parent_dir).map_err(|e| {
52 Error::File(FileError::CreateDirectoryFailed {
53 path: parent_dir.to_path_buf(),
54 reason: e.to_string(),
55 })
56 })?;
57 }
58
59 let file_stem = log_path
61 .file_stem()
62 .and_then(|n| n.to_str())
63 .ok_or_else(|| {
64 Error::File(FileError::CreateDirectoryFailed {
65 path: log_path.to_path_buf(),
66 reason: "Invalid filename".to_string(),
67 })
68 })?;
69
70 let extension = log_path
71 .extension()
72 .and_then(|e| e.to_str())
73 .unwrap_or("log");
74
75 let log_file_path = parent_dir.join(format!("{file_stem}.{extension}"));
77 let file = OpenOptions::new()
78 .create(true)
79 .append(true)
80 .open(&log_file_path)
81 .map_err(|e| {
82 Error::File(FileError::CreateDirectoryFailed {
83 path: log_file_path.clone(),
84 reason: e.to_string(),
85 })
86 })?;
87
88 let shared_file = Arc::new(Mutex::new(file));
89
90 struct SimpleLogger {
92 level: LevelFilter,
93 file: Arc<Mutex<std::fs::File>>,
94 }
95
96 impl log::Log for SimpleLogger {
97 fn enabled(&self, metadata: &Metadata) -> bool {
98 match self.level {
99 LevelFilter::Off => false,
100 LevelFilter::Error => metadata.level() == Level::Error,
101 LevelFilter::Warn => metadata.level() <= Level::Warn,
102 LevelFilter::Info => metadata.level() <= Level::Info,
103 LevelFilter::Debug => metadata.level() <= Level::Debug,
104 LevelFilter::Trace => true,
105 }
106 }
107
108 fn log(&self, record: &Record) {
109 if !self.enabled(record.metadata()) {
110 return;
111 }
112 let now = Local::now().format("%Y-%m-%d %H:%M:%S");
113 let msg = format!(
114 "[{}][{}] {} - {}\n",
115 now,
116 record.level(),
117 record.target(),
118 record.args()
119 );
120 if let Ok(console_enabled) = LOG_TO_CONSOLE.lock() {
122 if *console_enabled {
123 let _ = std::io::stdout().write_all(msg.as_bytes());
124 }
125 }
126 if let Ok(mut f) = self.file.lock() {
128 let _ = f.write_all(msg.as_bytes());
129 }
130 }
131
132 fn flush(&self) {}
133 }
134
135 let logger = SimpleLogger {
136 level,
137 file: shared_file.clone(),
138 };
139
140 log::set_max_level(level);
142 log::set_boxed_logger(Box::new(logger)).map_err(|e: SetLoggerError| {
143 Error::File(FileError::CreateDirectoryFailed {
144 path: log_file_path,
145 reason: format!("Failed to set logger: {e}"),
146 })
147 })?;
148
149 log::info!(
150 "Logging initialized - level: {:?}, file: {}, retention_days: {}",
151 level,
152 config.file,
153 config.retention_days()
154 );
155
156 Ok(())
157}
158
159fn parse_log_level(level_str: &str) -> Result<LevelFilter> {
161 let lower = level_str.to_lowercase();
162 LOG_LEVEL_MAP.get(lower.as_str()).copied().ok_or_else(|| {
163 Error::Config(crate::error::ConfigError::InvalidLogLevel {
164 level: level_str.to_string(),
165 valid_levels: LOG_LEVELS.iter().map(|s| (*s).to_string()).collect(),
166 })
167 })
168}