Skip to main content

sz_rust_core/
log.rs

1//! 日志系统 — 对齐 PHP `think-logger`
2//!
3//! ## 设计
4//!
5//! - 基于 `sz-orm-logger` 的 `StructuredLogger` 提供日志收集
6//! - 同时通过 `tracing` 宏输出(与 SZ-ORM-Tracing 协同,未来接入 OpenTelemetry)
7//! - 全局单例 `LogFacade`,通过 [`LogFacade::init()`] 初始化、[`LogFacade::instance()`] 获取
8//! - 支持多通道(file/console),对齐 PHP `config/log.php` 的 `channels` 配置
9//!
10//! ## PHP 对齐
11//!
12//! ```php
13//! // PHP think-logger
14//! Log::info('hello');
15//! Log::error('error occurred', ['exception' => $e]);
16//! Log::channel('file')->info('file log');
17//! ```
18//!
19//! ```rust,ignore
20//! // SZ-Rust 等价
21//! use sz_rust_core::log::LogFacade;
22//! LogFacade::instance().unwrap().info("hello");
23//! LogFacade::instance().unwrap().error("error occurred");
24//! ```
25
26use crate::config::{LogChannel, LogSection};
27use parking_lot::RwLock;
28use std::collections::HashMap;
29use std::sync::OnceLock;
30
31// 重导出 sz-orm-logger 核心类型,方便上层直接使用
32pub use sz_orm_logger::{LogEntry, LogLevel, Logger, LoggerFactory, StructuredLogger};
33
34/// 全局日志 facade 单例
35static LOG_FACADE: OnceLock<LogFacade> = OnceLock::new();
36
37/// 日志 facade — 持有默认 `StructuredLogger` 和命名通道
38///
39/// 对齐 PHP `think\facade\Log`,提供全局日志访问点。
40pub struct LogFacade {
41    /// 默认通道名(对应 PHP `config/log.php` 的 `default`)
42    default_channel: String,
43    /// 默认 logger 实例
44    logger: StructuredLogger,
45    /// 命名通道集合(对应 PHP `channels`)
46    channels: RwLock<HashMap<String, StructuredLogger>>,
47}
48
49impl LogFacade {
50    /// 构造 LogFacade 实例(不注册到全局单例)
51    pub fn new(section: &LogSection) -> Self {
52        let default_channel = section.default.clone();
53        let default_log_level = section
54            .channels
55            .get(&default_channel)
56            .map(|c| parse_level(&c.level))
57            .unwrap_or(LogLevel::Info);
58        let logger = StructuredLogger::with_level(default_log_level);
59
60        let mut channels = HashMap::new();
61        for (name, channel_cfg) in &section.channels {
62            channels.insert(name.clone(), channel_to_logger(channel_cfg));
63        }
64
65        LogFacade {
66            default_channel,
67            logger,
68            channels: RwLock::new(channels),
69        }
70    }
71
72    /// 初始化全局日志 facade
73    ///
74    /// 重复调用返回已有实例(不覆盖)。
75    pub fn init(section: &LogSection) -> &'static LogFacade {
76        LOG_FACADE.get_or_init(|| LogFacade::new(section))
77    }
78
79    /// 获取全局日志 facade 实例
80    ///
81    /// 必须先调用 [`LogFacade::init()`] 初始化,否则返回 `None`。
82    pub fn instance() -> Option<&'static LogFacade> {
83        LOG_FACADE.get()
84    }
85
86    /// 获取默认通道名
87    pub fn default_channel(&self) -> &str {
88        &self.default_channel
89    }
90
91    /// 获取默认 logger 引用
92    pub fn logger(&self) -> &StructuredLogger {
93        &self.logger
94    }
95
96    /// 获取指定通道的 logger 引用
97    ///
98    /// 对齐 PHP `Log::channel('file')->info(...)`。
99    pub fn channel(&self, name: &str) -> Option<ChannelRef<'_>> {
100        if self.channels.read().contains_key(name) {
101            Some(ChannelRef {
102                facade: self,
103                name: name.to_string(),
104            })
105        } else {
106            None
107        }
108    }
109
110    /// 获取所有通道名
111    pub fn channel_names(&self) -> Vec<String> {
112        self.channels.read().keys().cloned().collect()
113    }
114
115    /// 记录日志(同时输出到 StructuredLogger 和 tracing)
116    pub fn log(&self, level: LogLevel, msg: &str) {
117        self.logger.log(level, msg);
118        match level {
119            LogLevel::Debug => tracing::debug!("{}", msg),
120            LogLevel::Info => tracing::info!("{}", msg),
121            LogLevel::Warn => tracing::warn!("{}", msg),
122            LogLevel::Error => tracing::error!("{}", msg),
123        }
124    }
125
126    /// 记录 DEBUG 级别日志
127    pub fn debug(&self, msg: &str) {
128        self.log(LogLevel::Debug, msg);
129    }
130
131    /// 记录 INFO 级别日志
132    pub fn info(&self, msg: &str) {
133        self.log(LogLevel::Info, msg);
134    }
135
136    /// 记录 WARN 级别日志
137    pub fn warn(&self, msg: &str) {
138        self.log(LogLevel::Warn, msg);
139    }
140
141    /// 记录 ERROR 级别日志
142    pub fn error(&self, msg: &str) {
143        self.log(LogLevel::Error, msg);
144    }
145}
146
147impl std::fmt::Debug for LogFacade {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.debug_struct("LogFacade")
150            .field("default_channel", &self.default_channel)
151            .field("channels", &self.channels.read().keys().collect::<Vec<_>>())
152            .finish()
153    }
154}
155
156/// 命名通道引用
157///
158/// 通过 [`LogFacade::channel()`] 获取,提供与默认 logger 相同的日志方法。
159pub struct ChannelRef<'a> {
160    facade: &'a LogFacade,
161    name: String,
162}
163
164impl<'a> ChannelRef<'a> {
165    /// 通道名
166    pub fn name(&self) -> &str {
167        &self.name
168    }
169
170    /// 记录日志到指定通道
171    pub fn log(&self, level: LogLevel, msg: &str) {
172        let guard = self.facade.channels.read();
173        if let Some(logger) = guard.get(&self.name) {
174            logger.log(level, msg);
175        }
176        match level {
177            LogLevel::Debug => tracing::debug!("[{}] {}", self.name, msg),
178            LogLevel::Info => tracing::info!("[{}] {}", self.name, msg),
179            LogLevel::Warn => tracing::warn!("[{}] {}", self.name, msg),
180            LogLevel::Error => tracing::error!("[{}] {}", self.name, msg),
181        }
182    }
183
184    /// 记录 debug 级别日志
185    pub fn debug(&self, msg: &str) {
186        self.log(LogLevel::Debug, msg);
187    }
188
189    /// 记录 info 级别日志
190    pub fn info(&self, msg: &str) {
191        self.log(LogLevel::Info, msg);
192    }
193
194    /// 记录 warn 级别日志
195    pub fn warn(&self, msg: &str) {
196        self.log(LogLevel::Warn, msg);
197    }
198
199    /// 记录 error 级别日志
200    pub fn error(&self, msg: &str) {
201        self.log(LogLevel::Error, msg);
202    }
203}
204
205/// 从字符串解析日志级别
206///
207/// 支持大小写不敏感:`"DEBUG"` / `"debug"` / `"Debug"` 均解析为 `LogLevel::Debug`。
208/// 未知字符串默认为 `LogLevel::Info`。
209pub fn parse_level(s: &str) -> LogLevel {
210    match s.to_lowercase().as_str() {
211        "debug" => LogLevel::Debug,
212        "info" => LogLevel::Info,
213        "warn" | "warning" => LogLevel::Warn,
214        "error" => LogLevel::Error,
215        _ => LogLevel::Info,
216    }
217}
218
219/// 从 `LogChannel` 配置构造 `StructuredLogger`
220fn channel_to_logger(channel: &LogChannel) -> StructuredLogger {
221    StructuredLogger::with_level(parse_level(&channel.level))
222}
223
224// ============================================================================
225// 单元测试
226// ============================================================================
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::config::{LogChannel, LogSection};
232
233    /// 构造测试用的 LogSection(含 file + console 两个通道)
234    fn make_log_section() -> LogSection {
235        let mut channels = HashMap::new();
236        channels.insert(
237            "file".to_string(),
238            LogChannel {
239                r#type: "file".to_string(),
240                path: "runtime/logs".to_string(),
241                level: "info".to_string(),
242                max_files: 30,
243                format: "%{time} [%{level}] %{message}".to_string(),
244            },
245        );
246        channels.insert(
247            "console".to_string(),
248            LogChannel {
249                r#type: "console".to_string(),
250                path: String::new(),
251                level: "debug".to_string(),
252                max_files: 0,
253                format: "%{time} [%{level}] %{message}".to_string(),
254            },
255        );
256        LogSection {
257            default: "file".to_string(),
258            channels,
259        }
260    }
261
262    /// 测试 parse_level 各种输入
263    #[test]
264    fn test_parse_level() {
265        assert_eq!(parse_level("debug"), LogLevel::Debug);
266        assert_eq!(parse_level("DEBUG"), LogLevel::Debug);
267        assert_eq!(parse_level("Debug"), LogLevel::Debug);
268        assert_eq!(parse_level("info"), LogLevel::Info);
269        assert_eq!(parse_level("INFO"), LogLevel::Info);
270        assert_eq!(parse_level("warn"), LogLevel::Warn);
271        assert_eq!(parse_level("warning"), LogLevel::Warn);
272        assert_eq!(parse_level("WARN"), LogLevel::Warn);
273        assert_eq!(parse_level("error"), LogLevel::Error);
274        assert_eq!(parse_level("ERROR"), LogLevel::Error);
275        // 未知字符串默认 Info
276        assert_eq!(parse_level("unknown"), LogLevel::Info);
277        assert_eq!(parse_level(""), LogLevel::Info);
278    }
279
280    /// 测试 LogFacade 构造和默认通道
281    #[test]
282    fn test_log_facade_new() {
283        let section = make_log_section();
284        let facade = LogFacade::new(&section);
285
286        assert_eq!(facade.default_channel(), "file");
287        let names = facade.channel_names();
288        assert_eq!(names.len(), 2);
289        assert!(names.contains(&"file".to_string()));
290        assert!(names.contains(&"console".to_string()));
291    }
292
293    /// 测试默认 logger 级别取自 default 通道
294    #[test]
295    fn test_default_logger_level() {
296        let section = make_log_section();
297        let facade = LogFacade::new(&section);
298
299        // file 通道 level=info,所以默认 logger 级别为 Info
300        assert_eq!(facade.logger().level(), LogLevel::Info);
301
302        // Debug 级别应被过滤
303        facade.debug("debug msg - should be filtered");
304        let entries = facade.logger().entries();
305        assert!(entries.iter().all(|e| e.level != LogLevel::Debug));
306    }
307
308    /// 测试日志记录到默认 logger
309    #[test]
310    fn test_log_to_default_logger() {
311        let section = make_log_section();
312        let facade = LogFacade::new(&section);
313
314        facade.info("test info message");
315        facade.warn("test warn message");
316        facade.error("test error message");
317
318        let entries = facade.logger().entries();
319        assert!(entries.iter().any(|e| e.message == "test info message"));
320        assert!(entries.iter().any(|e| e.message == "test warn message"));
321        assert!(entries.iter().any(|e| e.message == "test error message"));
322    }
323
324    /// 测试通过 ChannelRef 访问命名通道
325    #[test]
326    fn test_channel_access() {
327        let section = make_log_section();
328        let facade = LogFacade::new(&section);
329
330        // file 通道存在
331        let file_channel = facade.channel("file");
332        assert!(file_channel.is_some());
333        let file_channel = file_channel.unwrap();
334        assert_eq!(file_channel.name(), "file");
335
336        // console 通道存在
337        let console_channel = facade.channel("console");
338        assert!(console_channel.is_some());
339
340        // 不存在的通道返回 None
341        assert!(facade.channel("nonexistent").is_none());
342    }
343
344    /// 测试 console 通道(level=debug)能记录所有级别
345    #[test]
346    fn test_console_channel_debug_level() {
347        let section = make_log_section();
348        let facade = LogFacade::new(&section);
349
350        let console = facade.channel("console").unwrap();
351        console.debug("debug msg");
352        console.info("info msg");
353        console.warn("warn msg");
354        console.error("error msg");
355
356        // console 通道 level=debug,所有级别都应记录
357        let guard = facade.channels.read();
358        let console_logger = guard.get("console").unwrap();
359        let entries = console_logger.entries();
360        assert_eq!(entries.len(), 4);
361    }
362
363    /// 测试 LogFacade init 全局单例
364    #[test]
365    fn test_log_facade_init_singleton() {
366        let section = make_log_section();
367        let facade = LogFacade::init(&section);
368
369        // instance() 应返回同一实例
370        let facade2 = LogFacade::instance();
371        assert!(facade2.is_some());
372        assert!(std::ptr::eq(facade, facade2.unwrap()));
373
374        // 再次 init 应返回同一实例(不覆盖)
375        let section2 = make_log_section();
376        let facade3 = LogFacade::init(&section2);
377        assert!(std::ptr::eq(facade, facade3));
378    }
379
380    /// 测试从实际配置文件加载日志配置
381    #[test]
382    fn test_load_from_config_file() {
383        // 查找 config 目录
384        let config_dir = std::env::current_dir().ok().and_then(|d| {
385            let mut current = d.clone();
386            for _ in 0..5 {
387                if current.join("config").exists() {
388                    return Some(current.join("config"));
389                }
390                if let Some(parent) = current.parent() {
391                    current = parent.to_path_buf();
392                } else {
393                    break;
394                }
395            }
396            None
397        });
398
399        let Some(config_dir) = config_dir else {
400            eprintln!("跳过:未找到 config 目录");
401            return;
402        };
403
404        let log_path = config_dir.join("log.yml");
405        if !log_path.exists() {
406            eprintln!("跳过:未找到 log.yml");
407            return;
408        }
409
410        let content = std::fs::read_to_string(&log_path).unwrap();
411        let section: LogSection = serde_yml::from_str(&content).unwrap();
412
413        // 验证默认通道为 file
414        assert_eq!(section.default, "file");
415
416        // 验证有 file 和 console 两个通道
417        assert!(section.channels.contains_key("file"));
418        assert!(section.channels.contains_key("console"));
419
420        // 验证 file 通道配置
421        let file_channel = section.channels.get("file").unwrap();
422        assert_eq!(file_channel.r#type, "file");
423        assert_eq!(file_channel.level, "info");
424        assert_eq!(file_channel.max_files, 30);
425
426        // 验证 console 通道配置
427        let console_channel = section.channels.get("console").unwrap();
428        assert_eq!(console_channel.r#type, "console");
429        assert_eq!(console_channel.level, "debug");
430    }
431
432    /// 测试 LogFacade::new 处理空 channels(默认通道不存在时用 Info 级别)
433    #[test]
434    fn test_log_facade_with_empty_channels() {
435        let section = LogSection::default();
436        let facade = LogFacade::new(&section);
437
438        // 默认通道为空,logger 级别应为 Info(fallback)
439        assert_eq!(facade.logger().level(), LogLevel::Info);
440        assert_eq!(facade.default_channel(), "");
441    }
442
443    /// 测试 LogFacade::Debug 输出
444    #[test]
445    fn test_log_facade_debug_format() {
446        let section = make_log_section();
447        let facade = LogFacade::new(&section);
448
449        let debug_str = format!("{:?}", facade);
450        assert!(debug_str.contains("LogFacade"));
451        assert!(debug_str.contains("file"));
452    }
453}