tin_logger/
logger.rs

1use flexi_logger::{
2    Age, Cleanup, Criterion, DeferredNow, FileSpec, Logger as FlexiLogger, Naming, WriteMode,
3};
4
5use crate::config::LoggerConfig;
6
7/// 日志工具,提供日志系统初始化方法
8pub struct Logger;
9
10impl Logger {
11    /// 使用默认配置初始化日志系统
12    ///
13    /// # Panics
14    /// 初始化失败时会 panic。
15    pub fn init() {
16        Self::init_with_config(LoggerConfig::default()).expect("Logger init failed");
17    }
18
19    /// 只设置日志级别并初始化日志系统,其他配置使用默认值
20    ///
21    /// # 参数
22    /// * `level` - 日志级别
23    ///
24    /// # Panics
25    /// 初始化失败时会 panic。
26    pub fn init_with_level(level: String) {
27        let level = level
28            .parse::<log::LevelFilter>()
29            .expect("Invalid log level");
30
31        let config = LoggerConfig {
32            level,
33            duplicate_to_stdout: if level == log::LevelFilter::Debug {
34                crate::config::Duplicate::All
35            } else {
36                crate::config::Duplicate::None
37            },
38            ..LoggerConfig::default()
39        };
40
41        Self::init_with_config(config).expect("Logger init failed");
42    }
43
44    /// 使用自定义配置初始化日志系统
45    ///
46    /// # 参数
47    /// * `config` - 日志配置
48    ///
49    /// # 返回
50    /// * `Ok(())` 初始化成功
51    /// * `Err` 初始化失败
52    pub fn init_with_config(config: LoggerConfig) -> Result<(), Box<dyn std::error::Error>> {
53        let mut logger = FlexiLogger::try_with_env_or_str(config.level.as_str())?
54            .log_to_file(
55                FileSpec::default()
56                    .directory(config.directory)
57                    .basename(config.basename)
58                    .suffix(config.suffix),
59            )
60            .rotate(
61                Criterion::AgeOrSize(Age::Day, config.rotate_size),
62                Naming::Timestamps,
63                Cleanup::KeepLogAndCompressedFiles(3, 90),
64            )
65            .write_mode(WriteMode::BufferAndFlush)
66            .duplicate_to_stdout(config.duplicate_to_stdout.into());
67
68        logger = logger.format_for_files(detailed_format);
69        logger = logger.format_for_stdout(console_format);
70
71        logger.start()?;
72        log::info!("Logger initialized, log level: {}", log::max_level());
73        Ok(())
74    }
75}
76
77/// 文件日志格式化函数
78///
79/// # 参数
80/// * `w` - 写入目标
81/// * `now` - 当前时间
82/// * `record` - 日志记录
83fn detailed_format(
84    w: &mut dyn std::io::Write,
85    now: &mut DeferredNow,
86    record: &log::Record,
87) -> std::io::Result<()> {
88    let path = record
89        .file_static()
90        .unwrap_or(record.module_path().unwrap_or("<unnamed>"));
91    let path = extract_crate_path(path);
92
93    write!(
94        w,
95        "[{}] {:<5} [{:<55}] : {}",
96        now.now().format("%Y-%m-%d %H:%M:%S%.6f %:z"),
97        record.level(),
98        format!("{}:{}", path, record.line().unwrap_or(0)),
99        &record.args()
100    )
101}
102
103/// 控制台日志格式化函数(带颜色)
104///
105/// # 参数
106/// * `w` - 写入目标
107/// * `now` - 当前时间
108/// * `record` - 日志记录
109fn console_format(
110    w: &mut dyn std::io::Write,
111    now: &mut DeferredNow,
112    record: &log::Record,
113) -> std::io::Result<()> {
114    let level_str = match record.level() {
115        log::Level::Error => "\x1b[31mERROR\x1b[0m",
116        log::Level::Warn => "\x1b[33mWARN\x1b[0m",
117        log::Level::Info => "\x1b[32mINFO\x1b[0m",
118        log::Level::Debug => "\x1b[34mDEBUG\x1b[0m",
119        log::Level::Trace => "\x1b[36mTRACE\x1b[0m",
120    };
121
122    let path = record
123        .file_static()
124        .unwrap_or(record.module_path().unwrap_or("<unnamed>"));
125
126    let path = extract_crate_path(path);
127
128    write!(
129        w,
130        "[{}] {:<15} [{:<55}] {}",
131        now.now().format("%Y-%m-%d %H:%M:%S"),
132        level_str,
133        format!("{}:{}", path, record.line().unwrap_or(0)),
134        record.args()
135    )
136}
137
138fn extract_crate_path(full_path: &str) -> &str {
139    let src_tags = ["\\src\\", "/src/"];
140    for src_tag in &src_tags {
141        if let Some(src_pos) = full_path.rfind(src_tag) {
142            let before_src = &full_path[..src_pos];
143            let seps: Vec<_> = before_src
144                .char_indices()
145                .filter(|&(_, c)| c == '\\' || c == '/')
146                .map(|(i, _)| i)
147                .collect();
148            let start = if seps.len() >= 2 {
149                seps[seps.len() - 1] + 1
150            } else if seps.len() == 1 {
151                seps[0] + 1
152            } else {
153                0
154            };
155            return &full_path[start..];
156        }
157    }
158    full_path
159}