secra_logger/
logger.rs

1use crate::config::{LogConfig, LogFormat, RotationStrategy};
2use crate::detector::{LoggingMode, SubscriberDetector};
3use crate::error::Result;
4use parking_lot::RwLock;
5use std::path::Path;
6use std::sync::Arc;
7
8/// 日志器(支持自适应模式)
9pub struct Logger {
10    /// 日志配置
11    config: LogConfig,
12    /// 运行模式(自动检测或显式指定)
13    mode: Option<LoggingMode>,
14    /// 是否已初始化
15    initialized: Arc<RwLock<bool>>,
16    /// 文件输出 guard(保持 non-blocking writer 存活)
17    _file_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
18}
19
20impl Logger {
21    /// 创建新的日志器(自动检测模式)
22    pub fn new(config: LogConfig) -> Result<Self> {
23        let mode = SubscriberDetector::detect_mode();
24        Ok(Self {
25            config,
26            mode: Some(mode),
27            initialized: Arc::new(RwLock::new(false)),
28            _file_guard: None,
29        })
30    }
31
32    /// 创建日志器并显式指定模式
33    pub fn new_with_mode(config: LogConfig, mode: LoggingMode) -> Self {
34        Self {
35            config,
36            mode: Some(mode),
37            initialized: Arc::new(RwLock::new(false)),
38            _file_guard: None,
39        }
40    }
41
42    /// 初始化日志系统(幂等)
43    ///
44    /// - **库模式**:不初始化 subscriber
45    /// - **应用模式**:初始化 subscriber,安装 console/file layers
46    pub fn init(&mut self) -> Result<()> {
47        {
48            let initialized = self.initialized.read();
49            if *initialized {
50                return Ok(());
51            }
52        }
53
54        let mode = self
55            .mode
56            .unwrap_or_else(|| SubscriberDetector::detect_mode());
57
58        match mode {
59            LoggingMode::Library => {
60                // 库模式:零侵入
61                tracing::debug!("使用库模式,不初始化 subscriber");
62                *self.initialized.write() = true;
63                Ok(())
64            }
65            LoggingMode::Application => {
66                tracing::debug!("使用应用模式,初始化 subscriber");
67                let guard = self.init_subscriber()?;
68                self._file_guard = guard;
69                *self.initialized.write() = true;
70                Ok(())
71            }
72        }
73    }
74
75    fn init_subscriber(&self) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
76        use tracing_subscriber::filter::EnvFilter;
77        use tracing_subscriber::layer::SubscriberExt;
78        use tracing_subscriber::util::SubscriberInitExt;
79        use tracing_subscriber::Layer;
80
81        // 兼容 log crate
82        let _ = tracing_log::LogTracer::init();
83
84        let env_filter = EnvFilter::try_from_default_env()
85            .unwrap_or_else(|_| EnvFilter::new(self.config.level.to_string()));
86
87        // 1) console layer(可选)
88        let console_layer = if self.config.console_output {
89            let base = tracing_subscriber::fmt::layer()
90                .with_target(self.config.console_show_target)
91                .with_file(self.config.console_show_file)
92                .with_line_number(self.config.console_show_line)
93                .with_ansi(self.config.console_colors);
94
95            Some(match self.config.console_format {
96                LogFormat::Json => base.json().boxed(),
97                LogFormat::Human => base.boxed(),
98            })
99        } else {
100            None
101        };
102
103        // 2) file layer(可选)
104        let mut guard: Option<tracing_appender::non_blocking::WorkerGuard> = None;
105        let file_layer = if self.config.file_output {
106            if let Some(path) = self.config.log_file.as_ref() {
107                let (writer, g) = self.build_file_writer(path)?;
108                guard = Some(g);
109
110                let base = tracing_subscriber::fmt::layer().with_writer(writer);
111                Some(match self.config.file_format {
112                    LogFormat::Json => base.json().boxed(),
113                    LogFormat::Human => base.boxed(),
114                })
115            } else {
116                None
117            }
118        } else {
119            None
120        };
121
122        let subscriber = tracing_subscriber::registry()
123            .with(env_filter)
124            .with(console_layer)
125            .with(file_layer);
126
127        // 3) 初始化(如果外部已 init,会失败;应用模式通常只在未初始化时走到这里)
128        let _ = subscriber.try_init();
129
130        Ok(guard)
131    }
132
133    fn build_file_writer(
134        &self,
135        log_file: &Path,
136    ) -> Result<(
137        tracing_appender::non_blocking::NonBlocking,
138        tracing_appender::non_blocking::WorkerGuard,
139    )> {
140        use tracing_appender::{non_blocking, rolling};
141
142        let dir = log_file.parent().unwrap_or_else(|| Path::new("."));
143        let stem = log_file
144            .file_stem()
145            .and_then(|s| s.to_str())
146            .unwrap_or("app");
147
148        let appender = match self.config.rotation.strategy {
149            RotationStrategy::Daily => rolling::daily(dir, stem),
150            RotationStrategy::Size(_) => {
151                // tracing-appender 没有内建按大小轮转,这里做“可用优先”的降级
152                rolling::hourly(dir, stem)
153            }
154        };
155
156        let (nb, guard) = non_blocking(appender);
157        Ok((nb, guard))
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use tracing_subscriber::layer::SubscriberExt;
165    use tracing_subscriber::util::SubscriberInitExt;
166
167    #[test]
168    fn can_force_library_mode_without_panicking() {
169        // 尝试先 init 一个全局 subscriber(如果已被其他测试 init,则 try_init 会失败但不影响)
170        let _ = tracing_subscriber::registry()
171            .with(tracing_subscriber::fmt::layer())
172            .try_init();
173
174        let mut logger = Logger::new_with_mode(LogConfig::default(), LoggingMode::Library);
175        logger.init().unwrap();
176    }
177
178    #[test]
179    fn can_try_application_mode_init_idempotently() {
180        // 如果测试进程里已经存在全局 subscriber,这里 try_init 会无效;我们只验证不会 panic 且幂等
181        let mut cfg = LogConfig::default();
182        cfg.console_output = true;
183        cfg.file_output = false;
184
185        let mut logger = Logger::new_with_mode(cfg, LoggingMode::Application);
186        logger.init().unwrap();
187        logger.init().unwrap();
188    }
189}