Skip to main content

sz_orm_logger/
lib.rs

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