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