Skip to main content

dm_database_sqllog2db/
logging.rs

1use crate::config::LoggingConfig;
2use crate::constants::LOG_LEVELS;
3use crate::error::{Error, FileError, Result};
4use chrono::Local;
5use log::{Level, LevelFilter, Metadata, Record};
6use std::collections::HashMap;
7use std::fs::OpenOptions;
8use std::io::Write;
9use std::path::Path;
10use std::sync::{Arc, LazyLock, Mutex};
11
12// 使用 LazyLock 缓存日志级别映射表,避免每次查找时重新构建
13static LOG_LEVEL_MAP: LazyLock<HashMap<&'static str, LevelFilter>> = LazyLock::new(|| {
14    let mut map = HashMap::with_capacity(5);
15    map.insert("trace", LevelFilter::Trace);
16    map.insert("debug", LevelFilter::Debug);
17    map.insert("info", LevelFilter::Info);
18    map.insert("warn", LevelFilter::Warn);
19    map.insert("error", LevelFilter::Error);
20    map
21});
22
23/// 初始化日志系统
24pub fn init_logging(config: &LoggingConfig) -> Result<()> {
25    // 解析日志级别
26    let level = parse_log_level(&config.level)?;
27    // 获取日志文件路径和目录
28    let log_path = Path::new(&config.file);
29    let parent_dir = log_path.parent().ok_or_else(|| {
30        Error::File(FileError::CreateDirectoryFailed {
31            path: log_path.to_path_buf(),
32            reason: "Failed to get parent directory".to_string(),
33        })
34    })?;
35
36    // 创建日志目录(如果不存在)
37    if !parent_dir.exists() {
38        std::fs::create_dir_all(parent_dir).map_err(|e| {
39            Error::File(FileError::CreateDirectoryFailed {
40                path: parent_dir.to_path_buf(),
41                reason: e.to_string(),
42            })
43        })?;
44    }
45
46    // 从路径中提取基础文件名(去掉扩展名)
47    let file_stem = log_path
48        .file_stem()
49        .and_then(|n| n.to_str())
50        .ok_or_else(|| {
51            Error::File(FileError::CreateDirectoryFailed {
52                path: log_path.to_path_buf(),
53                reason: "Invalid filename".to_string(),
54            })
55        })?;
56
57    let extension = log_path
58        .extension()
59        .and_then(|e| e.to_str())
60        .unwrap_or("log");
61
62    // 创建简单的追加日志文件(不做滚动),更轻量:使用 Arc<Mutex<File>> 作为共享 writer
63    let log_file_path = parent_dir.join(format!("{file_stem}.{extension}"));
64    let file = OpenOptions::new()
65        .create(true)
66        .append(true)
67        .open(&log_file_path)
68        .map_err(|e| {
69            Error::File(FileError::CreateDirectoryFailed {
70                path: log_file_path.clone(),
71                reason: e.to_string(),
72            })
73        })?;
74
75    let shared_file = Arc::new(Mutex::new(file));
76
77    // 自定义简单 Logger,写入文件与 stdout
78    struct SimpleLogger {
79        level: LevelFilter,
80        file: Arc<Mutex<std::fs::File>>,
81    }
82
83    impl log::Log for SimpleLogger {
84        fn enabled(&self, metadata: &Metadata) -> bool {
85            match self.level {
86                LevelFilter::Off => false,
87                LevelFilter::Error => metadata.level() == Level::Error,
88                LevelFilter::Warn => metadata.level() <= Level::Warn,
89                LevelFilter::Info => metadata.level() <= Level::Info,
90                LevelFilter::Debug => metadata.level() <= Level::Debug,
91                LevelFilter::Trace => true,
92            }
93        }
94
95        fn log(&self, record: &Record) {
96            if !self.enabled(record.metadata()) {
97                return;
98            }
99            let now = Local::now().format("%Y-%m-%d %H:%M:%S");
100            let msg = format!(
101                "[{}][{}] {} - {}\n",
102                now,
103                record.level(),
104                record.target(),
105                record.args()
106            );
107            // 写到 stdout
108            let _ = std::io::stdout().write_all(msg.as_bytes());
109
110            // 写到文件
111            if let Ok(mut f) = self.file.lock() {
112                let _ = f.write_all(msg.as_bytes());
113            }
114        }
115
116        fn flush(&self) {}
117    }
118
119    let logger = SimpleLogger {
120        level,
121        file: shared_file.clone(),
122    };
123
124    // 注册 logger
125    match log::set_boxed_logger(Box::new(logger)) {
126        Ok(()) => {
127            log::set_max_level(level);
128        }
129        Err(e) => {
130            // If already initialized, we just ignore it for integration tests
131            // but we can still print a message to the original stdout if needed
132            // However, in a CLI tool, this usually only happens during tests
133            let _ = e;
134        }
135    }
136
137    log::info!(
138        "Logging initialized - level: {:?}, file: {}, retention_days: {}",
139        level,
140        config.file,
141        config.retention_days
142    );
143
144    Ok(())
145}
146
147/// 解析日志级别字符串
148fn parse_log_level(level_str: &str) -> Result<LevelFilter> {
149    let lower = level_str.to_lowercase();
150    LOG_LEVEL_MAP.get(lower.as_str()).copied().ok_or_else(|| {
151        Error::Config(crate::error::ConfigError::InvalidLogLevel {
152            level: level_str.to_string(),
153            valid_levels: LOG_LEVELS.iter().map(|s| (*s).to_string()).collect(),
154        })
155    })
156}