1pub 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#[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 #[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 #[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 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 pub fn output(&self, msg: &str) {
135 self.log(LogLevel::Info, msg);
136 }
137
138 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 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 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 println!("[{}] {} - {}", level.as_str(), timestamp, msg);
178 }
179}
180
181pub 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 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(); 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 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); 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 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 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}