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