Skip to main content

sz_orm_logger/
lib.rs

1//! # SZ-ORM Logger — 结构化日志
2//!
3//! 提供多级别(Debug/Info/Warn/Error)、多输出目标的日志记录,支持异步写入与
4//! 结构化字段,可组合多个 Logger 实现输出到不同后端。
5//!
6//! ## 主要类型
7//!
8//! - [`Logger`] trait — 日志器接口
9//! - [`LogLevel`] — 日志级别
10//! - [`LogEntry`] — 日志条目
11//!
12//! ## 高级日志功能(`advanced` 模块)
13//!
14//! - [`advanced::LogRotator`] — 日志轮转(按大小/时间)
15//! - [`advanced::MultiOutputLogger`] / [`advanced::LogSink`] — 多输出扇出
16//! - [`advanced::LevelFilter`] — 按 target 细粒度级别过滤
17//! - [`advanced::StructuredLogEntry`] / [`advanced::StructuredLogWriter`] — 结构化字段
18
19pub mod advanced;
20
21use parking_lot::Mutex;
22use serde::{Deserialize, Serialize};
23use std::collections::HashMap;
24use std::sync::Arc;
25
26#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
27pub enum LogLevel {
28    Trace,
29    Debug,
30    Info,
31    Warn,
32    Error,
33}
34
35impl LogLevel {
36    pub fn as_str(&self) -> &'static str {
37        match self {
38            LogLevel::Trace => "TRACE",
39            LogLevel::Debug => "DEBUG",
40            LogLevel::Info => "INFO",
41            LogLevel::Warn => "WARN",
42            LogLevel::Error => "ERROR",
43        }
44    }
45}
46
47pub trait Logger: Send + Sync {
48    fn log(&self, level: LogLevel, msg: &str);
49}
50
51/// 环境类型
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53pub enum EnvKind {
54    Development,
55    Staging,
56    Production,
57}
58
59impl EnvKind {
60    pub fn as_str(&self) -> &'static str {
61        match self {
62            EnvKind::Development => "development",
63            EnvKind::Staging => "staging",
64            EnvKind::Production => "production",
65        }
66    }
67}
68
69#[cfg(feature = "prod-log-level")]
70mod prod {
71    use super::{EnvKind, LogLevel};
72    use serde::{Deserialize, Serialize};
73
74    /// 日志生产配置错误
75    #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
76    pub enum LoggerProdError {
77        #[error("log level {0} forbidden in production, minimum warn")]
78        LevelForbidden(String),
79    }
80
81    /// 日志生产配置:强制生产环境日志级别 warn 及以上
82    #[derive(Debug, Clone, Serialize, Deserialize)]
83    pub struct LoggerProdConfig {
84        pub level: LogLevel,
85        pub env: EnvKind,
86    }
87
88    impl LoggerProdConfig {
89        pub fn new(level: LogLevel, env: EnvKind) -> Self {
90            Self { level, env }
91        }
92
93        /// 校验:生产环境拒绝 level < Warn(Trace/Debug/Info 被拒绝)
94        pub fn validate(&self) -> Result<(), LoggerProdError> {
95            if self.env == EnvKind::Production && self.level < LogLevel::Warn {
96                return Err(LoggerProdError::LevelForbidden(
97                    self.level.as_str().to_string(),
98                ));
99            }
100            Ok(())
101        }
102    }
103}
104
105#[cfg(feature = "prod-log-level")]
106pub use prod::{LoggerProdConfig, LoggerProdError};
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct LogEntry {
110    pub level: LogLevel,
111    pub message: String,
112    pub timestamp: String,
113}
114
115pub struct StructuredLogger {
116    level: LogLevel,
117    entries: Arc<Mutex<Vec<LogEntry>>>,
118}
119
120impl StructuredLogger {
121    pub fn new() -> Self {
122        Self::with_level(LogLevel::Info)
123    }
124
125    pub fn with_level(level: LogLevel) -> Self {
126        Self {
127            level,
128            entries: Arc::new(Mutex::new(vec![])),
129        }
130    }
131
132    /// Convenience method equivalent to `log(LogLevel::Info, msg)`.
133    pub fn output(&self, msg: &str) {
134        self.log(LogLevel::Info, msg);
135    }
136
137    /// Return a snapshot of all log entries that passed the level filter.
138    pub fn entries(&self) -> Vec<LogEntry> {
139        let entries = self.entries.lock();
140        entries.iter().cloned().collect()
141    }
142
143    pub fn level(&self) -> LogLevel {
144        self.level
145    }
146
147    /// Internal shared handle so multiple loggers can write to the same sink.
148    pub fn shared_handle(&self) -> Arc<Mutex<Vec<LogEntry>>> {
149        Arc::clone(&self.entries)
150    }
151}
152
153impl Default for StructuredLogger {
154    fn default() -> Self {
155        Self::new()
156    }
157}
158
159impl Logger for StructuredLogger {
160    fn log(&self, level: LogLevel, msg: &str) {
161        // Filter: anything strictly below the configured level is dropped.
162        if level < self.level {
163            return;
164        }
165        let timestamp = chrono::Utc::now().to_rfc3339();
166        let entry = LogEntry {
167            level,
168            message: msg.to_string(),
169            timestamp: timestamp.clone(),
170        };
171        {
172            let mut entries = self.entries.lock();
173            entries.push(entry);
174        }
175        // Also emit to stdout for runtime observability, with level + timestamp.
176        println!("[{}] {} - {}", level.as_str(), timestamp, msg);
177    }
178}
179
180/// Factory that creates loggers with different configurations.
181pub struct LoggerFactory;
182
183impl LoggerFactory {
184    pub fn new() -> Self {
185        Self
186    }
187
188    pub fn create(&self, level: LogLevel) -> StructuredLogger {
189        StructuredLogger::with_level(level)
190    }
191
192    pub fn trace(&self) -> StructuredLogger {
193        self.create(LogLevel::Trace)
194    }
195
196    pub fn debug(&self) -> StructuredLogger {
197        self.create(LogLevel::Debug)
198    }
199
200    pub fn info(&self) -> StructuredLogger {
201        self.create(LogLevel::Info)
202    }
203
204    pub fn warn(&self) -> StructuredLogger {
205        self.create(LogLevel::Warn)
206    }
207
208    pub fn error(&self) -> StructuredLogger {
209        self.create(LogLevel::Error)
210    }
211}
212
213impl Default for LoggerFactory {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct MetricsSnapshot {
221    pub counters: HashMap<String, u64>,
222    pub gauges: HashMap<String, f64>,
223}
224
225pub struct Metrics {
226    counters: HashMap<String, u64>,
227    gauges: HashMap<String, f64>,
228}
229
230impl Metrics {
231    pub fn new() -> Self {
232        Self {
233            counters: HashMap::new(),
234            gauges: HashMap::new(),
235        }
236    }
237
238    pub fn increment_counter(&mut self, name: &str) {
239        *self.counters.entry(name.to_string()).or_insert(0) += 1;
240    }
241
242    pub fn add_counter(&mut self, name: &str, value: u64) {
243        *self.counters.entry(name.to_string()).or_insert(0) += value;
244    }
245
246    pub fn set_gauge(&mut self, name: &str, value: f64) {
247        self.gauges.insert(name.to_string(), value);
248    }
249
250    pub fn get_counter(&self, name: &str) -> Option<u64> {
251        self.counters.get(name).copied()
252    }
253
254    pub fn get_gauge(&self, name: &str) -> Option<f64> {
255        self.gauges.get(name).copied()
256    }
257
258    pub fn snapshot(&self) -> MetricsSnapshot {
259        MetricsSnapshot {
260            counters: self.counters.clone(),
261            gauges: self.gauges.clone(),
262        }
263    }
264}
265
266impl Default for Metrics {
267    fn default() -> Self {
268        Self::new()
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_log_level_ordering() {
278        // Verifies the PartialOrd derivation: Trace < Debug < Info < Warn < Error
279        assert!(LogLevel::Trace < LogLevel::Debug);
280        assert!(LogLevel::Debug < LogLevel::Info);
281        assert!(LogLevel::Info < LogLevel::Warn);
282        assert!(LogLevel::Warn < LogLevel::Error);
283    }
284
285    #[test]
286    fn test_log_level_trace_as_str() {
287        assert_eq!(LogLevel::Trace.as_str(), "TRACE");
288    }
289
290    #[test]
291    fn test_logger_with_level_info_filters_trace_and_debug() {
292        let l = StructuredLogger::with_level(LogLevel::Info);
293        l.log(LogLevel::Trace, "trace msg");
294        l.log(LogLevel::Debug, "debug msg");
295        l.log(LogLevel::Info, "info msg");
296        l.log(LogLevel::Warn, "warn msg");
297        l.log(LogLevel::Error, "error msg");
298        let entries = l.entries();
299        assert_eq!(entries.len(), 3, "trace and debug should be filtered out");
300        assert!(entries.iter().all(|e| e.level >= LogLevel::Info));
301    }
302
303    #[test]
304    fn test_logger_with_level_trace_passes_everything() {
305        let l = StructuredLogger::with_level(LogLevel::Trace);
306        l.log(LogLevel::Trace, "t");
307        l.log(LogLevel::Debug, "d");
308        l.log(LogLevel::Info, "i");
309        l.log(LogLevel::Warn, "w");
310        l.log(LogLevel::Error, "e");
311        assert_eq!(l.entries().len(), 5);
312    }
313
314    #[test]
315    fn test_logger_factory_trace() {
316        let factory = LoggerFactory::new();
317        let logger = factory.trace();
318        logger.log(LogLevel::Trace, "trace");
319        assert_eq!(logger.entries().len(), 1);
320        assert_eq!(logger.entries()[0].level, LogLevel::Trace);
321    }
322
323    #[test]
324    fn test_env_kind_as_str() {
325        assert_eq!(EnvKind::Development.as_str(), "development");
326        assert_eq!(EnvKind::Staging.as_str(), "staging");
327        assert_eq!(EnvKind::Production.as_str(), "production");
328    }
329
330    #[test]
331    fn test_logger_default_level_info_filters_debug() {
332        let l = StructuredLogger::new(); // default Info
333        l.log(LogLevel::Debug, "debug msg");
334        l.log(LogLevel::Info, "info msg");
335        let entries = l.entries();
336        assert_eq!(entries.len(), 1, "debug should be filtered out");
337        assert_eq!(entries[0].message, "info msg");
338        assert_eq!(entries[0].level, LogLevel::Info);
339        // Timestamp should be a non-empty RFC3339 string
340        assert!(!entries[0].timestamp.is_empty());
341        assert!(entries[0].timestamp.contains('T'));
342    }
343
344    #[test]
345    fn test_logger_with_level_warn_filters_info() {
346        let l = StructuredLogger::with_level(LogLevel::Warn);
347        l.log(LogLevel::Debug, "debug msg");
348        l.log(LogLevel::Info, "info msg");
349        l.log(LogLevel::Warn, "warn msg");
350        l.log(LogLevel::Error, "error msg");
351        let entries = l.entries();
352        assert_eq!(entries.len(), 2, "only Warn and Error should pass");
353        assert!(entries.iter().all(|e| e.level >= LogLevel::Warn));
354        assert!(entries.iter().any(|e| e.message == "warn msg"));
355        assert!(entries.iter().any(|e| e.message == "error msg"));
356    }
357
358    #[test]
359    fn test_logger_with_level_error_only_error() {
360        let l = StructuredLogger::with_level(LogLevel::Error);
361        l.log(LogLevel::Warn, "warn msg");
362        l.log(LogLevel::Error, "error msg");
363        let entries = l.entries();
364        assert_eq!(entries.len(), 1);
365        assert_eq!(entries[0].level, LogLevel::Error);
366    }
367
368    #[test]
369    fn test_logger_with_level_debug_passes_everything() {
370        let l = StructuredLogger::with_level(LogLevel::Debug);
371        l.log(LogLevel::Debug, "d");
372        l.log(LogLevel::Info, "i");
373        l.log(LogLevel::Warn, "w");
374        l.log(LogLevel::Error, "e");
375        assert_eq!(l.entries().len(), 4);
376    }
377
378    #[test]
379    fn test_logger_factory_creates_loggers_with_different_levels() {
380        let factory = LoggerFactory::new();
381        let debug_logger = factory.debug();
382        let error_logger = factory.error();
383        debug_logger.log(LogLevel::Debug, "debug");
384        debug_logger.log(LogLevel::Info, "info");
385        error_logger.log(LogLevel::Info, "should be filtered");
386        error_logger.log(LogLevel::Error, "error");
387        assert_eq!(debug_logger.entries().len(), 2);
388        assert_eq!(error_logger.entries().len(), 1);
389        assert_eq!(error_logger.entries()[0].message, "error");
390    }
391
392    #[test]
393    fn test_logger_factory_default_creates_info_logger() {
394        let factory = LoggerFactory;
395        let logger = factory.info();
396        logger.log(LogLevel::Debug, "should be filtered");
397        logger.log(LogLevel::Info, "should pass");
398        assert_eq!(logger.entries().len(), 1);
399    }
400
401    #[test]
402    fn test_logger_output_method_logs_at_info() {
403        let l = StructuredLogger::with_level(LogLevel::Debug);
404        l.output("hello");
405        let entries = l.entries();
406        assert_eq!(entries.len(), 1);
407        assert_eq!(entries[0].level, LogLevel::Info);
408        assert_eq!(entries[0].message, "hello");
409    }
410
411    #[test]
412    fn test_metrics_increment_and_read() {
413        let mut m = Metrics::new();
414        m.increment_counter("req");
415        m.increment_counter("req");
416        m.increment_counter("err");
417        assert_eq!(m.get_counter("req"), Some(2));
418        assert_eq!(m.get_counter("err"), Some(1));
419        assert_eq!(m.get_counter("missing"), None);
420    }
421
422    #[test]
423    fn test_metrics_add_counter() {
424        let mut m = Metrics::new();
425        m.add_counter("bytes", 100);
426        m.add_counter("bytes", 50);
427        assert_eq!(m.get_counter("bytes"), Some(150));
428    }
429
430    #[test]
431    fn test_metrics_gauge_overwrites() {
432        let mut m = Metrics::new();
433        m.set_gauge("cpu", 0.5);
434        assert_eq!(m.get_gauge("cpu"), Some(0.5));
435        m.set_gauge("cpu", 0.8); // overwrite
436        assert_eq!(m.get_gauge("cpu"), Some(0.8));
437        assert_eq!(m.get_gauge("missing"), None);
438    }
439
440    #[test]
441    fn test_metrics_snapshot_captures_state() {
442        let mut m = Metrics::new();
443        m.increment_counter("a");
444        m.increment_counter("a");
445        m.increment_counter("b");
446        m.set_gauge("g1", 1.0);
447        m.set_gauge("g2", 2.5);
448        let snap = m.snapshot();
449        assert_eq!(snap.counters.get("a"), Some(&2));
450        assert_eq!(snap.counters.get("b"), Some(&1));
451        assert_eq!(snap.gauges.get("g1"), Some(&1.0));
452        assert_eq!(snap.gauges.get("g2"), Some(&2.5));
453        // Snapshot is independent of subsequent changes
454        m.increment_counter("a");
455        assert_eq!(snap.counters.get("a"), Some(&2));
456        assert_eq!(m.get_counter("a"), Some(3));
457    }
458
459    #[test]
460    fn test_log_entry_has_timestamp_and_level() {
461        let l = StructuredLogger::with_level(LogLevel::Debug);
462        l.log(LogLevel::Warn, "warning");
463        let e = &l.entries()[0];
464        assert_eq!(e.level, LogLevel::Warn);
465        assert_eq!(e.message, "warning");
466        // RFC3339 timestamps contain 'T' separator and 'Z' for UTC
467        assert!(e.timestamp.contains('T'));
468        assert!(e.timestamp.ends_with('Z') || e.timestamp.contains('+'));
469    }
470}
471
472#[cfg(all(test, feature = "prod-log-level"))]
473mod prod_tests {
474    use super::*;
475
476    #[test]
477    fn test_prod_config_debug_in_production_rejected() {
478        let cfg = LoggerProdConfig::new(LogLevel::Debug, EnvKind::Production);
479        let err = cfg.validate().unwrap_err();
480        assert!(err.to_string().contains("DEBUG"));
481        assert!(err.to_string().contains("forbidden in production"));
482    }
483
484    #[test]
485    fn test_prod_config_trace_in_production_rejected() {
486        let cfg = LoggerProdConfig::new(LogLevel::Trace, EnvKind::Production);
487        assert!(cfg.validate().is_err());
488    }
489
490    #[test]
491    fn test_prod_config_info_in_production_rejected() {
492        let cfg = LoggerProdConfig::new(LogLevel::Info, EnvKind::Production);
493        assert!(cfg.validate().is_err());
494    }
495
496    #[test]
497    fn test_prod_config_warn_in_production_accepted() {
498        let cfg = LoggerProdConfig::new(LogLevel::Warn, EnvKind::Production);
499        assert!(cfg.validate().is_ok());
500    }
501
502    #[test]
503    fn test_prod_config_error_in_production_accepted() {
504        let cfg = LoggerProdConfig::new(LogLevel::Error, EnvKind::Production);
505        assert!(cfg.validate().is_ok());
506    }
507
508    #[test]
509    fn test_prod_config_debug_in_development_accepted() {
510        let cfg = LoggerProdConfig::new(LogLevel::Debug, EnvKind::Development);
511        assert!(cfg.validate().is_ok());
512    }
513
514    #[test]
515    fn test_prod_config_trace_in_staging_accepted() {
516        let cfg = LoggerProdConfig::new(LogLevel::Trace, EnvKind::Staging);
517        assert!(cfg.validate().is_ok());
518    }
519}