secra-logger 3.0.3

一个生产级的 Rust 日志系统库,基于 tracing 生态系统构建,支持结构化 JSON 日志、文件滚动、UTC+8 时区等特性
Documentation
use crate::config::{LogConfig, LogFormat, RotationStrategy};
use crate::detector::{LoggingMode, SubscriberDetector};
use crate::error::Result;
use parking_lot::RwLock;
use std::path::Path;
use std::sync::Arc;

/// 日志器(支持自适应模式)
pub struct Logger {
    /// 日志配置
    config: LogConfig,
    /// 运行模式(自动检测或显式指定)
    mode: Option<LoggingMode>,
    /// 是否已初始化
    initialized: Arc<RwLock<bool>>,
    /// 文件输出 guard(保持 non-blocking writer 存活)
    _file_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
}

impl Logger {
    /// 创建新的日志器(自动检测模式)
    pub fn new(config: LogConfig) -> Result<Self> {
        let mode = SubscriberDetector::detect_mode();
        Ok(Self {
            config,
            mode: Some(mode),
            initialized: Arc::new(RwLock::new(false)),
            _file_guard: None,
        })
    }

    /// 创建日志器并显式指定模式
    pub fn new_with_mode(config: LogConfig, mode: LoggingMode) -> Self {
        Self {
            config,
            mode: Some(mode),
            initialized: Arc::new(RwLock::new(false)),
            _file_guard: None,
        }
    }

    /// 初始化日志系统(幂等)
    ///
    /// - **库模式**:不初始化 subscriber
    /// - **应用模式**:初始化 subscriber,安装 console/file layers
    pub fn init(&mut self) -> Result<()> {
        {
            let initialized = self.initialized.read();
            if *initialized {
                return Ok(());
            }
        }

        let mode = self
            .mode
            .unwrap_or_else(|| SubscriberDetector::detect_mode());

        match mode {
            LoggingMode::Library => {
                // 库模式:零侵入
                tracing::debug!("使用库模式,不初始化 subscriber");
                *self.initialized.write() = true;
                Ok(())
            }
            LoggingMode::Application => {
                tracing::debug!("使用应用模式,初始化 subscriber");
                let guard = self.init_subscriber()?;
                self._file_guard = guard;
                *self.initialized.write() = true;
                Ok(())
            }
        }
    }

    fn init_subscriber(&self) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
        use tracing_subscriber::filter::EnvFilter;
        use tracing_subscriber::layer::SubscriberExt;
        use tracing_subscriber::util::SubscriberInitExt;
        use tracing_subscriber::Layer;

        // 兼容 log crate
        let _ = tracing_log::LogTracer::init();

        let env_filter = EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| EnvFilter::new(self.config.level.to_string()));

        // 1) console layer(可选)
        let console_layer = if self.config.console_output {
            let base = tracing_subscriber::fmt::layer()
                .with_target(self.config.console_show_target)
                .with_file(self.config.console_show_file)
                .with_line_number(self.config.console_show_line)
                .with_ansi(self.config.console_colors);

            Some(match self.config.console_format {
                LogFormat::Json => base.json().boxed(),
                LogFormat::Human => base.boxed(),
            })
        } else {
            None
        };

        // 2) file layer(可选)
        let mut guard: Option<tracing_appender::non_blocking::WorkerGuard> = None;
        let file_layer = if self.config.file_output {
            if let Some(path) = self.config.log_file.as_ref() {
                let (writer, g) = self.build_file_writer(path)?;
                guard = Some(g);

                let base = tracing_subscriber::fmt::layer().with_writer(writer);
                Some(match self.config.file_format {
                    LogFormat::Json => base.json().boxed(),
                    LogFormat::Human => base.boxed(),
                })
            } else {
                None
            }
        } else {
            None
        };

        let subscriber = tracing_subscriber::registry()
            .with(env_filter)
            .with(console_layer)
            .with(file_layer);

        // 3) 初始化(如果外部已 init,会失败;应用模式通常只在未初始化时走到这里)
        let _ = subscriber.try_init();

        Ok(guard)
    }

    fn build_file_writer(
        &self,
        log_file: &Path,
    ) -> Result<(
        tracing_appender::non_blocking::NonBlocking,
        tracing_appender::non_blocking::WorkerGuard,
    )> {
        use tracing_appender::{non_blocking, rolling};

        let dir = log_file.parent().unwrap_or_else(|| Path::new("."));
        let stem = log_file
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("app");

        let appender = match self.config.rotation.strategy {
            RotationStrategy::Daily => rolling::daily(dir, stem),
            RotationStrategy::Size(_) => {
                // tracing-appender 没有内建按大小轮转,这里做“可用优先”的降级
                rolling::hourly(dir, stem)
            }
        };

        let (nb, guard) = non_blocking(appender);
        Ok((nb, guard))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tracing_subscriber::layer::SubscriberExt;
    use tracing_subscriber::util::SubscriberInitExt;

    #[test]
    fn can_force_library_mode_without_panicking() {
        // 尝试先 init 一个全局 subscriber(如果已被其他测试 init,则 try_init 会失败但不影响)
        let _ = tracing_subscriber::registry()
            .with(tracing_subscriber::fmt::layer())
            .try_init();

        let mut logger = Logger::new_with_mode(LogConfig::default(), LoggingMode::Library);
        logger.init().unwrap();
    }

    #[test]
    fn can_try_application_mode_init_idempotently() {
        // 如果测试进程里已经存在全局 subscriber,这里 try_init 会无效;我们只验证不会 panic 且幂等
        let mut cfg = LogConfig::default();
        cfg.console_output = true;
        cfg.file_output = false;

        let mut logger = Logger::new_with_mode(cfg, LoggingMode::Application);
        logger.init().unwrap();
        logger.init().unwrap();
    }
}