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 serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23use std::sync::{Arc, Mutex};
24
25#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
26pub enum LogLevel {
27    Debug,
28    Info,
29    Warn,
30    Error,
31}
32
33impl LogLevel {
34    pub fn as_str(&self) -> &'static str {
35        match self {
36            LogLevel::Debug => "DEBUG",
37            LogLevel::Info => "INFO",
38            LogLevel::Warn => "WARN",
39            LogLevel::Error => "ERROR",
40        }
41    }
42}
43
44pub trait Logger: Send + Sync {
45    fn log(&self, level: LogLevel, msg: &str);
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct LogEntry {
50    pub level: LogLevel,
51    pub message: String,
52    pub timestamp: String,
53}
54
55pub struct StructuredLogger {
56    level: LogLevel,
57    entries: Arc<Mutex<Vec<LogEntry>>>,
58}
59
60impl StructuredLogger {
61    pub fn new() -> Self {
62        Self::with_level(LogLevel::Info)
63    }
64
65    pub fn with_level(level: LogLevel) -> Self {
66        Self {
67            level,
68            entries: Arc::new(Mutex::new(vec![])),
69        }
70    }
71
72    /// Convenience method equivalent to `log(LogLevel::Info, msg)`.
73    pub fn output(&self, msg: &str) {
74        self.log(LogLevel::Info, msg);
75    }
76
77    /// Return a snapshot of all log entries that passed the level filter.
78    pub fn entries(&self) -> Vec<LogEntry> {
79        let entries = self.entries.lock().unwrap();
80        entries.iter().cloned().collect()
81    }
82
83    pub fn level(&self) -> LogLevel {
84        self.level
85    }
86
87    /// Internal shared handle so multiple loggers can write to the same sink.
88    pub fn shared_handle(&self) -> Arc<Mutex<Vec<LogEntry>>> {
89        Arc::clone(&self.entries)
90    }
91}
92
93impl Default for StructuredLogger {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99impl Logger for StructuredLogger {
100    fn log(&self, level: LogLevel, msg: &str) {
101        // Filter: anything strictly below the configured level is dropped.
102        if level < self.level {
103            return;
104        }
105        let timestamp = chrono::Utc::now().to_rfc3339();
106        let entry = LogEntry {
107            level,
108            message: msg.to_string(),
109            timestamp: timestamp.clone(),
110        };
111        {
112            let mut entries = self.entries.lock().unwrap();
113            entries.push(entry);
114        }
115        // Also emit to stdout for runtime observability, with level + timestamp.
116        println!("[{}] {} - {}", level.as_str(), timestamp, msg);
117    }
118}
119
120/// Factory that creates loggers with different configurations.
121pub struct LoggerFactory;
122
123impl LoggerFactory {
124    pub fn new() -> Self {
125        Self
126    }
127
128    pub fn create(&self, level: LogLevel) -> StructuredLogger {
129        StructuredLogger::with_level(level)
130    }
131
132    pub fn debug(&self) -> StructuredLogger {
133        self.create(LogLevel::Debug)
134    }
135
136    pub fn info(&self) -> StructuredLogger {
137        self.create(LogLevel::Info)
138    }
139
140    pub fn warn(&self) -> StructuredLogger {
141        self.create(LogLevel::Warn)
142    }
143
144    pub fn error(&self) -> StructuredLogger {
145        self.create(LogLevel::Error)
146    }
147}
148
149impl Default for LoggerFactory {
150    fn default() -> Self {
151        Self::new()
152    }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct MetricsSnapshot {
157    pub counters: HashMap<String, u64>,
158    pub gauges: HashMap<String, f64>,
159}
160
161pub struct Metrics {
162    counters: HashMap<String, u64>,
163    gauges: HashMap<String, f64>,
164}
165
166impl Metrics {
167    pub fn new() -> Self {
168        Self {
169            counters: HashMap::new(),
170            gauges: HashMap::new(),
171        }
172    }
173
174    pub fn increment_counter(&mut self, name: &str) {
175        *self.counters.entry(name.to_string()).or_insert(0) += 1;
176    }
177
178    pub fn add_counter(&mut self, name: &str, value: u64) {
179        *self.counters.entry(name.to_string()).or_insert(0) += value;
180    }
181
182    pub fn set_gauge(&mut self, name: &str, value: f64) {
183        self.gauges.insert(name.to_string(), value);
184    }
185
186    pub fn get_counter(&self, name: &str) -> Option<u64> {
187        self.counters.get(name).copied()
188    }
189
190    pub fn get_gauge(&self, name: &str) -> Option<f64> {
191        self.gauges.get(name).copied()
192    }
193
194    pub fn snapshot(&self) -> MetricsSnapshot {
195        MetricsSnapshot {
196            counters: self.counters.clone(),
197            gauges: self.gauges.clone(),
198        }
199    }
200}
201
202impl Default for Metrics {
203    fn default() -> Self {
204        Self::new()
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_log_level_ordering() {
214        // Verifies the PartialOrd derivation: Debug < Info < Warn < Error
215        assert!(LogLevel::Debug < LogLevel::Info);
216        assert!(LogLevel::Info < LogLevel::Warn);
217        assert!(LogLevel::Warn < LogLevel::Error);
218    }
219
220    #[test]
221    fn test_logger_default_level_info_filters_debug() {
222        let l = StructuredLogger::new(); // default Info
223        l.log(LogLevel::Debug, "debug msg");
224        l.log(LogLevel::Info, "info msg");
225        let entries = l.entries();
226        assert_eq!(entries.len(), 1, "debug should be filtered out");
227        assert_eq!(entries[0].message, "info msg");
228        assert_eq!(entries[0].level, LogLevel::Info);
229        // Timestamp should be a non-empty RFC3339 string
230        assert!(!entries[0].timestamp.is_empty());
231        assert!(entries[0].timestamp.contains('T'));
232    }
233
234    #[test]
235    fn test_logger_with_level_warn_filters_info() {
236        let l = StructuredLogger::with_level(LogLevel::Warn);
237        l.log(LogLevel::Debug, "debug msg");
238        l.log(LogLevel::Info, "info msg");
239        l.log(LogLevel::Warn, "warn msg");
240        l.log(LogLevel::Error, "error msg");
241        let entries = l.entries();
242        assert_eq!(entries.len(), 2, "only Warn and Error should pass");
243        assert!(entries.iter().all(|e| e.level >= LogLevel::Warn));
244        assert!(entries.iter().any(|e| e.message == "warn msg"));
245        assert!(entries.iter().any(|e| e.message == "error msg"));
246    }
247
248    #[test]
249    fn test_logger_with_level_error_only_error() {
250        let l = StructuredLogger::with_level(LogLevel::Error);
251        l.log(LogLevel::Warn, "warn msg");
252        l.log(LogLevel::Error, "error msg");
253        let entries = l.entries();
254        assert_eq!(entries.len(), 1);
255        assert_eq!(entries[0].level, LogLevel::Error);
256    }
257
258    #[test]
259    fn test_logger_with_level_debug_passes_everything() {
260        let l = StructuredLogger::with_level(LogLevel::Debug);
261        l.log(LogLevel::Debug, "d");
262        l.log(LogLevel::Info, "i");
263        l.log(LogLevel::Warn, "w");
264        l.log(LogLevel::Error, "e");
265        assert_eq!(l.entries().len(), 4);
266    }
267
268    #[test]
269    fn test_logger_factory_creates_loggers_with_different_levels() {
270        let factory = LoggerFactory::new();
271        let debug_logger = factory.debug();
272        let error_logger = factory.error();
273        debug_logger.log(LogLevel::Debug, "debug");
274        debug_logger.log(LogLevel::Info, "info");
275        error_logger.log(LogLevel::Info, "should be filtered");
276        error_logger.log(LogLevel::Error, "error");
277        assert_eq!(debug_logger.entries().len(), 2);
278        assert_eq!(error_logger.entries().len(), 1);
279        assert_eq!(error_logger.entries()[0].message, "error");
280    }
281
282    #[test]
283    fn test_logger_factory_default_creates_info_logger() {
284        let factory = LoggerFactory;
285        let logger = factory.info();
286        logger.log(LogLevel::Debug, "should be filtered");
287        logger.log(LogLevel::Info, "should pass");
288        assert_eq!(logger.entries().len(), 1);
289    }
290
291    #[test]
292    fn test_logger_output_method_logs_at_info() {
293        let l = StructuredLogger::with_level(LogLevel::Debug);
294        l.output("hello");
295        let entries = l.entries();
296        assert_eq!(entries.len(), 1);
297        assert_eq!(entries[0].level, LogLevel::Info);
298        assert_eq!(entries[0].message, "hello");
299    }
300
301    #[test]
302    fn test_metrics_increment_and_read() {
303        let mut m = Metrics::new();
304        m.increment_counter("req");
305        m.increment_counter("req");
306        m.increment_counter("err");
307        assert_eq!(m.get_counter("req"), Some(2));
308        assert_eq!(m.get_counter("err"), Some(1));
309        assert_eq!(m.get_counter("missing"), None);
310    }
311
312    #[test]
313    fn test_metrics_add_counter() {
314        let mut m = Metrics::new();
315        m.add_counter("bytes", 100);
316        m.add_counter("bytes", 50);
317        assert_eq!(m.get_counter("bytes"), Some(150));
318    }
319
320    #[test]
321    fn test_metrics_gauge_overwrites() {
322        let mut m = Metrics::new();
323        m.set_gauge("cpu", 0.5);
324        assert_eq!(m.get_gauge("cpu"), Some(0.5));
325        m.set_gauge("cpu", 0.8); // overwrite
326        assert_eq!(m.get_gauge("cpu"), Some(0.8));
327        assert_eq!(m.get_gauge("missing"), None);
328    }
329
330    #[test]
331    fn test_metrics_snapshot_captures_state() {
332        let mut m = Metrics::new();
333        m.increment_counter("a");
334        m.increment_counter("a");
335        m.increment_counter("b");
336        m.set_gauge("g1", 1.0);
337        m.set_gauge("g2", 2.5);
338        let snap = m.snapshot();
339        assert_eq!(snap.counters.get("a"), Some(&2));
340        assert_eq!(snap.counters.get("b"), Some(&1));
341        assert_eq!(snap.gauges.get("g1"), Some(&1.0));
342        assert_eq!(snap.gauges.get("g2"), Some(&2.5));
343        // Snapshot is independent of subsequent changes
344        m.increment_counter("a");
345        assert_eq!(snap.counters.get("a"), Some(&2));
346        assert_eq!(m.get_counter("a"), Some(3));
347    }
348
349    #[test]
350    fn test_log_entry_has_timestamp_and_level() {
351        let l = StructuredLogger::with_level(LogLevel::Debug);
352        l.log(LogLevel::Warn, "warning");
353        let e = &l.entries()[0];
354        assert_eq!(e.level, LogLevel::Warn);
355        assert_eq!(e.message, "warning");
356        // RFC3339 timestamps contain 'T' separator and 'Z' for UTC
357        assert!(e.timestamp.contains('T'));
358        assert!(e.timestamp.ends_with('Z') || e.timestamp.contains('+'));
359    }
360}