secra_logger/
module.rs

1//! LoggingModule 实现
2//!
3//! LoggingModule 是日志系统的 Module 实现,负责初始化日志系统。
4
5use crate::config::LoggingConfig;
6use anyhow::{Context as AnyhowContext, Result};
7use std::sync::{Arc, Mutex};
8use std::fs::OpenOptions;
9use tracing_appender::non_blocking;
10use tracing_subscriber::{
11    layer::SubscriberExt,
12    EnvFilter, Registry,
13    fmt,
14};
15
16/// 日志模块
17///
18/// LoggingModule 负责初始化日志系统,包括:
19/// - 配置 tracing subscriber
20/// - 设置文件和控制台输出
21/// - 注册 Logger 组件
22pub struct LoggingModule {
23    /// 文件写入器的 WorkerGuard(必须保持存活)
24    _file_guard: Arc<Mutex<Option<non_blocking::WorkerGuard>>>,
25}
26
27impl LoggingModule {
28    /// 创建新的 LoggingModule
29    pub fn new() -> Self {
30        Self {
31            _file_guard: Arc::new(Mutex::new(None)),
32        }
33    }
34}
35
36impl Default for LoggingModule {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl LoggingModule {
43    /// 初始化日志系统
44    ///
45    /// # 参数
46    /// * `config` - 日志配置,如果为 None 则使用默认配置
47    pub fn init(&self, config: Option<LoggingConfig>) -> Result<()> {
48        // 1. 获取日志配置
49        let log_config = config.unwrap_or_else(LoggingConfig::default);
50        
51        // 2. 生成 logrotate 配置文件(如果启用)
52        if let Some(file_config) = &log_config.file {
53            if let Some(logrotate_config) = &file_config.logrotate {
54                if logrotate_config.enabled {
55                    // 生成配置文件(不传 default_path,让 generate_config_file 自动使用当前可执行文件目录)
56                    // 如果 logrotate_config.output_path 为 None,会自动使用当前可执行文件目录下的 logrotate/app.conf
57                    match logrotate_config.generate_config_file(None) {
58                        Ok(path) => {
59                            // 使用 println! 因为此时 tracing subscriber 还未初始化
60                            println!("Logrotate config generated: {}", path);
61                            println!("To test it, run: logrotate -d {}", path);
62                        }
63                        Err(e) => {
64                            // 记录警告但不阻止初始化
65                            eprintln!("Warning: Failed to generate logrotate config: {}", e);
66                        }
67                    }
68                }
69            }
70        }
71        
72        // 3. 初始化 tracing subscriber
73        let (subscriber, file_guard) = build_subscriber(&log_config)
74            .context("Failed to build tracing subscriber")?;
75        
76        // 3. 保存 file_guard(必须保持存活)
77        if let Some(guard) = file_guard {
78            *self._file_guard.lock().unwrap() = Some(guard);
79        }
80        
81        // 4. 设置全局 subscriber
82        // 如果已经设置了全局 subscriber(例如在测试中),尝试设置会失败
83        // 在这种情况下,我们仍然继续初始化,因为日志系统已经可用
84        match tracing::subscriber::set_global_default(subscriber) {
85            Ok(_) => {
86                tracing::info!("Logging module initialized");
87            }
88            Err(e) => {
89                // 如果全局 subscriber 已经设置,记录警告但继续
90                // 这通常发生在测试环境中,多个测试用例共享同一个进程
91                let error_msg = format!("{}", e);
92                if error_msg.contains("already been set") {
93                    // 使用 println! 而不是 tracing::info!,因为可能已经有其他 subscriber
94                    println!("Logging module initialized (using existing subscriber)");
95                } else {
96                    // 其他错误,返回错误
97                    return Err(anyhow::anyhow!("Failed to set global subscriber: {}", e));
98                }
99            }
100        }
101        
102        Ok(())
103    }
104    
105    /// 关闭日志系统
106    ///
107    /// 清理资源(WorkerGuard 被 drop 时会等待所有日志写入完成)
108    pub fn shutdown(&self) {
109        tracing::info!("Logging module shutdown, flushing logs...");
110    }
111}
112
113
114/// 构建 tracing subscriber
115///
116/// 由于 Rust 类型系统的限制,我们需要明确处理每种 layer 组合
117/// 使用 boxed() 方法将 layer 转换为 trait object,解决类型推断问题
118fn build_subscriber(
119    config: &LoggingConfig,
120) -> Result<(
121    Box<dyn tracing::Subscriber + Send + Sync>,
122    Option<non_blocking::WorkerGuard>,
123)> {
124    // 1. 构建日志过滤器
125    let filter = EnvFilter::try_from_default_env()
126        .unwrap_or_else(|_| EnvFilter::new(&config.level));
127    
128    // 2. 构建基础 registry
129    let registry = Registry::default().with(filter);
130    
131    // 3. 根据配置组合不同的 layer
132    // 由于类型系统的限制,我们需要明确处理每种组合
133    let has_file = matches!(config.output_mode, crate::config::OutputMode::File | crate::config::OutputMode::Both)
134        && config.file.as_ref().map(|f| f.enabled).unwrap_or(false);
135    let has_console = matches!(config.output_mode, crate::config::OutputMode::Console | crate::config::OutputMode::Both)
136        && config.console.as_ref().map(|c| c.enabled).unwrap_or(false);
137    
138    // 根据组合情况构建 subscriber
139    // 使用链式调用,让类型系统自动推断
140    match (has_file, has_console) {
141        (true, true) => {
142            // 文件 + 控制台
143            let file_config = config.file.as_ref().unwrap();
144            let console_config = config.console.as_ref().unwrap();
145            
146            // 确保目录存在
147            if let Some(parent) = std::path::Path::new(&file_config.path).parent() {
148                std::fs::create_dir_all(parent)
149                    .with_context(|| format!("Failed to create log directory: {}", parent.display()))?;
150            }
151            
152            // 创建文件写入器
153            let file = OpenOptions::new()
154                .create(true)
155                .append(true)
156                .open(&file_config.path)
157                .with_context(|| format!("Failed to open log file: {}", file_config.path))?;
158            
159            let (non_blocking, guard) = non_blocking(file);
160            
161            // 构建文件层
162            let file_layer = fmt::layer()
163                .with_writer(non_blocking)
164                .json()
165                .with_target(true)
166                .with_file(true)
167                .with_line_number(true)
168                .with_ansi(false);
169            
170            // 构建控制台层
171            use std::io;
172            let console_layer = fmt::layer()
173                .with_writer(io::stdout)
174                .json()
175                .with_target(true)
176                .with_file(true)
177                .with_line_number(true)
178                .with_ansi(console_config.ansi);
179            
180            let subscriber = registry
181                .with(file_layer)
182                .with(console_layer);
183            
184            Ok((Box::new(subscriber), Some(guard)))
185        }
186        (true, false) => {
187            // 只有文件
188            let file_config = config.file.as_ref().unwrap();
189            
190            // 确保目录存在
191            if let Some(parent) = std::path::Path::new(&file_config.path).parent() {
192                std::fs::create_dir_all(parent)
193                    .with_context(|| format!("Failed to create log directory: {}", parent.display()))?;
194            }
195            
196            // 创建文件写入器
197            let file = OpenOptions::new()
198                .create(true)
199                .append(true)
200                .open(&file_config.path)
201                .with_context(|| format!("Failed to open log file: {}", file_config.path))?;
202            
203            let (non_blocking, guard) = non_blocking(file);
204            
205            // 构建文件层
206            let file_layer = fmt::layer()
207                .with_writer(non_blocking)
208                .json()
209                .with_target(true)
210                .with_file(true)
211                .with_line_number(true)
212                .with_ansi(false);
213            
214            let subscriber = registry.with(file_layer);
215            Ok((Box::new(subscriber), Some(guard)))
216        }
217        (false, true) => {
218            // 只有控制台
219            let console_config = config.console.as_ref().unwrap();
220            
221            use std::io;
222            let console_layer = fmt::layer()
223                .with_writer(io::stdout)
224                .json()
225                .with_target(true)
226                .with_file(true)
227                .with_line_number(true)
228                .with_ansi(console_config.ansi);
229            
230            let subscriber = registry.with(console_layer);
231            Ok((Box::new(subscriber), None))
232        }
233        (false, false) => {
234            // 没有任何输出层(只有 filter)
235            Ok((Box::new(registry), None))
236        }
237    }
238}