tin-logger 0.1.0

A logging library based on flexi_logger with file rotation, console output, and custom formatting support
Documentation
use flexi_logger::{
    Age, Cleanup, Criterion, DeferredNow, FileSpec, Logger as FlexiLogger, Naming, WriteMode,
};

use crate::config::LoggerConfig;

/// 日志工具,提供日志系统初始化方法
pub struct Logger;

impl Logger {
    /// 使用默认配置初始化日志系统
    ///
    /// # Panics
    /// 初始化失败时会 panic。
    pub fn init() {
        Self::init_with_config(LoggerConfig::default()).expect("Logger init failed");
    }

    /// 只设置日志级别并初始化日志系统,其他配置使用默认值
    ///
    /// # 参数
    /// * `level` - 日志级别
    ///
    /// # Panics
    /// 初始化失败时会 panic。
    pub fn init_with_level(level: String) {
        let level = level
            .parse::<log::LevelFilter>()
            .expect("Invalid log level");

        let config = LoggerConfig {
            level,
            duplicate_to_stdout: if level == log::LevelFilter::Debug {
                crate::config::Duplicate::All
            } else {
                crate::config::Duplicate::None
            },
            ..LoggerConfig::default()
        };

        Self::init_with_config(config).expect("Logger init failed");
    }

    /// 使用自定义配置初始化日志系统
    ///
    /// # 参数
    /// * `config` - 日志配置
    ///
    /// # 返回
    /// * `Ok(())` 初始化成功
    /// * `Err` 初始化失败
    pub fn init_with_config(config: LoggerConfig) -> Result<(), Box<dyn std::error::Error>> {
        let mut logger = FlexiLogger::try_with_env_or_str(config.level.as_str())?
            .log_to_file(
                FileSpec::default()
                    .directory(config.directory)
                    .basename(config.basename)
                    .suffix(config.suffix),
            )
            .rotate(
                Criterion::AgeOrSize(Age::Day, config.rotate_size),
                Naming::Timestamps,
                Cleanup::KeepLogAndCompressedFiles(3, 90),
            )
            .write_mode(WriteMode::BufferAndFlush)
            .duplicate_to_stdout(config.duplicate_to_stdout.into());

        logger = logger.format_for_files(detailed_format);
        logger = logger.format_for_stdout(console_format);

        logger.start()?;
        log::info!("Logger initialized, log level: {}", log::max_level());
        Ok(())
    }
}

/// 文件日志格式化函数
///
/// # 参数
/// * `w` - 写入目标
/// * `now` - 当前时间
/// * `record` - 日志记录
fn detailed_format(
    w: &mut dyn std::io::Write,
    now: &mut DeferredNow,
    record: &log::Record,
) -> std::io::Result<()> {
    let path = record
        .file_static()
        .unwrap_or(record.module_path().unwrap_or("<unnamed>"));
    let path = extract_crate_path(path);

    write!(
        w,
        "[{}] {:<5} [{:<55}] : {}",
        now.now().format("%Y-%m-%d %H:%M:%S%.6f %:z"),
        record.level(),
        format!("{}:{}", path, record.line().unwrap_or(0)),
        &record.args()
    )
}

/// 控制台日志格式化函数(带颜色)
///
/// # 参数
/// * `w` - 写入目标
/// * `now` - 当前时间
/// * `record` - 日志记录
fn console_format(
    w: &mut dyn std::io::Write,
    now: &mut DeferredNow,
    record: &log::Record,
) -> std::io::Result<()> {
    let level_str = match record.level() {
        log::Level::Error => "\x1b[31mERROR\x1b[0m",
        log::Level::Warn => "\x1b[33mWARN\x1b[0m",
        log::Level::Info => "\x1b[32mINFO\x1b[0m",
        log::Level::Debug => "\x1b[34mDEBUG\x1b[0m",
        log::Level::Trace => "\x1b[36mTRACE\x1b[0m",
    };

    let path = record
        .file_static()
        .unwrap_or(record.module_path().unwrap_or("<unnamed>"));

    let path = extract_crate_path(path);

    write!(
        w,
        "[{}] {:<15} [{:<55}] {}",
        now.now().format("%Y-%m-%d %H:%M:%S"),
        level_str,
        format!("{}:{}", path, record.line().unwrap_or(0)),
        record.args()
    )
}

fn extract_crate_path(full_path: &str) -> &str {
    let src_tags = ["\\src\\", "/src/"];
    for src_tag in &src_tags {
        if let Some(src_pos) = full_path.rfind(src_tag) {
            let before_src = &full_path[..src_pos];
            let seps: Vec<_> = before_src
                .char_indices()
                .filter(|&(_, c)| c == '\\' || c == '/')
                .map(|(i, _)| i)
                .collect();
            let start = if seps.len() >= 2 {
                seps[seps.len() - 1] + 1
            } else if seps.len() == 1 {
                seps[0] + 1
            } else {
                0
            };
            return &full_path[start..];
        }
    }
    full_path
}