pub mod advanced;
pub mod log_pipeline;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
pub fn as_str(&self) -> &'static str {
match self {
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
}
}
}
pub trait Logger: Send + Sync {
fn log(&self, level: LogLevel, msg: &str);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EnvKind {
Development,
Staging,
Production,
}
impl EnvKind {
pub fn as_str(&self) -> &'static str {
match self {
EnvKind::Development => "development",
EnvKind::Staging => "staging",
EnvKind::Production => "production",
}
}
}
#[cfg(feature = "prod-log-level")]
mod prod {
use super::{EnvKind, LogLevel};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum LoggerProdError {
#[error("log level {0} forbidden in production, minimum warn")]
LevelForbidden(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggerProdConfig {
pub level: LogLevel,
pub env: EnvKind,
}
impl LoggerProdConfig {
pub fn new(level: LogLevel, env: EnvKind) -> Self {
Self { level, env }
}
pub fn validate(&self) -> Result<(), LoggerProdError> {
if self.env == EnvKind::Production && self.level < LogLevel::Warn {
return Err(LoggerProdError::LevelForbidden(
self.level.as_str().to_string(),
));
}
Ok(())
}
}
}
#[cfg(feature = "prod-log-level")]
pub use prod::{LoggerProdConfig, LoggerProdError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogEntry {
pub level: LogLevel,
pub message: String,
pub timestamp: String,
}
pub struct StructuredLogger {
level: LogLevel,
entries: Arc<Mutex<Vec<LogEntry>>>,
}
impl StructuredLogger {
pub fn new() -> Self {
Self::with_level(LogLevel::Info)
}
pub fn with_level(level: LogLevel) -> Self {
Self {
level,
entries: Arc::new(Mutex::new(vec![])),
}
}
pub fn output(&self, msg: &str) {
self.log(LogLevel::Info, msg);
}
pub fn entries(&self) -> Vec<LogEntry> {
let entries = self.entries.lock();
entries.iter().cloned().collect()
}
pub fn level(&self) -> LogLevel {
self.level
}
pub fn shared_handle(&self) -> Arc<Mutex<Vec<LogEntry>>> {
Arc::clone(&self.entries)
}
}
impl Default for StructuredLogger {
fn default() -> Self {
Self::new()
}
}
impl Logger for StructuredLogger {
fn log(&self, level: LogLevel, msg: &str) {
if level < self.level {
return;
}
let timestamp = chrono::Utc::now().to_rfc3339();
let entry = LogEntry {
level,
message: msg.to_string(),
timestamp: timestamp.clone(),
};
{
let mut entries = self.entries.lock();
entries.push(entry);
}
println!("[{}] {} - {}", level.as_str(), timestamp, msg);
}
}
pub struct LoggerFactory;
impl LoggerFactory {
pub fn new() -> Self {
Self
}
pub fn create(&self, level: LogLevel) -> StructuredLogger {
StructuredLogger::with_level(level)
}
pub fn trace(&self) -> StructuredLogger {
self.create(LogLevel::Trace)
}
pub fn debug(&self) -> StructuredLogger {
self.create(LogLevel::Debug)
}
pub fn info(&self) -> StructuredLogger {
self.create(LogLevel::Info)
}
pub fn warn(&self) -> StructuredLogger {
self.create(LogLevel::Warn)
}
pub fn error(&self) -> StructuredLogger {
self.create(LogLevel::Error)
}
}
impl Default for LoggerFactory {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsSnapshot {
pub counters: HashMap<String, u64>,
pub gauges: HashMap<String, f64>,
}
pub struct Metrics {
counters: HashMap<String, u64>,
gauges: HashMap<String, f64>,
}
impl Metrics {
pub fn new() -> Self {
Self {
counters: HashMap::new(),
gauges: HashMap::new(),
}
}
pub fn increment_counter(&mut self, name: &str) {
*self.counters.entry(name.to_string()).or_insert(0) += 1;
}
pub fn add_counter(&mut self, name: &str, value: u64) {
*self.counters.entry(name.to_string()).or_insert(0) += value;
}
pub fn set_gauge(&mut self, name: &str, value: f64) {
self.gauges.insert(name.to_string(), value);
}
pub fn get_counter(&self, name: &str) -> Option<u64> {
self.counters.get(name).copied()
}
pub fn get_gauge(&self, name: &str) -> Option<f64> {
self.gauges.get(name).copied()
}
pub fn snapshot(&self) -> MetricsSnapshot {
MetricsSnapshot {
counters: self.counters.clone(),
gauges: self.gauges.clone(),
}
}
}
impl Default for Metrics {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_level_ordering() {
assert!(LogLevel::Trace < LogLevel::Debug);
assert!(LogLevel::Debug < LogLevel::Info);
assert!(LogLevel::Info < LogLevel::Warn);
assert!(LogLevel::Warn < LogLevel::Error);
}
#[test]
fn test_log_level_trace_as_str() {
assert_eq!(LogLevel::Trace.as_str(), "TRACE");
}
#[test]
fn test_logger_with_level_info_filters_trace_and_debug() {
let l = StructuredLogger::with_level(LogLevel::Info);
l.log(LogLevel::Trace, "trace msg");
l.log(LogLevel::Debug, "debug msg");
l.log(LogLevel::Info, "info msg");
l.log(LogLevel::Warn, "warn msg");
l.log(LogLevel::Error, "error msg");
let entries = l.entries();
assert_eq!(entries.len(), 3, "trace and debug should be filtered out");
assert!(entries.iter().all(|e| e.level >= LogLevel::Info));
}
#[test]
fn test_logger_with_level_trace_passes_everything() {
let l = StructuredLogger::with_level(LogLevel::Trace);
l.log(LogLevel::Trace, "t");
l.log(LogLevel::Debug, "d");
l.log(LogLevel::Info, "i");
l.log(LogLevel::Warn, "w");
l.log(LogLevel::Error, "e");
assert_eq!(l.entries().len(), 5);
}
#[test]
fn test_logger_factory_trace() {
let factory = LoggerFactory::new();
let logger = factory.trace();
logger.log(LogLevel::Trace, "trace");
assert_eq!(logger.entries().len(), 1);
assert_eq!(logger.entries()[0].level, LogLevel::Trace);
}
#[test]
fn test_env_kind_as_str() {
assert_eq!(EnvKind::Development.as_str(), "development");
assert_eq!(EnvKind::Staging.as_str(), "staging");
assert_eq!(EnvKind::Production.as_str(), "production");
}
#[test]
fn test_logger_default_level_info_filters_debug() {
let l = StructuredLogger::new(); l.log(LogLevel::Debug, "debug msg");
l.log(LogLevel::Info, "info msg");
let entries = l.entries();
assert_eq!(entries.len(), 1, "debug should be filtered out");
assert_eq!(entries[0].message, "info msg");
assert_eq!(entries[0].level, LogLevel::Info);
assert!(!entries[0].timestamp.is_empty());
assert!(entries[0].timestamp.contains('T'));
}
#[test]
fn test_logger_with_level_warn_filters_info() {
let l = StructuredLogger::with_level(LogLevel::Warn);
l.log(LogLevel::Debug, "debug msg");
l.log(LogLevel::Info, "info msg");
l.log(LogLevel::Warn, "warn msg");
l.log(LogLevel::Error, "error msg");
let entries = l.entries();
assert_eq!(entries.len(), 2, "only Warn and Error should pass");
assert!(entries.iter().all(|e| e.level >= LogLevel::Warn));
assert!(entries.iter().any(|e| e.message == "warn msg"));
assert!(entries.iter().any(|e| e.message == "error msg"));
}
#[test]
fn test_logger_with_level_error_only_error() {
let l = StructuredLogger::with_level(LogLevel::Error);
l.log(LogLevel::Warn, "warn msg");
l.log(LogLevel::Error, "error msg");
let entries = l.entries();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].level, LogLevel::Error);
}
#[test]
fn test_logger_with_level_debug_passes_everything() {
let l = StructuredLogger::with_level(LogLevel::Debug);
l.log(LogLevel::Debug, "d");
l.log(LogLevel::Info, "i");
l.log(LogLevel::Warn, "w");
l.log(LogLevel::Error, "e");
assert_eq!(l.entries().len(), 4);
}
#[test]
fn test_logger_factory_creates_loggers_with_different_levels() {
let factory = LoggerFactory::new();
let debug_logger = factory.debug();
let error_logger = factory.error();
debug_logger.log(LogLevel::Debug, "debug");
debug_logger.log(LogLevel::Info, "info");
error_logger.log(LogLevel::Info, "should be filtered");
error_logger.log(LogLevel::Error, "error");
assert_eq!(debug_logger.entries().len(), 2);
assert_eq!(error_logger.entries().len(), 1);
assert_eq!(error_logger.entries()[0].message, "error");
}
#[test]
fn test_logger_factory_default_creates_info_logger() {
let factory = LoggerFactory;
let logger = factory.info();
logger.log(LogLevel::Debug, "should be filtered");
logger.log(LogLevel::Info, "should pass");
assert_eq!(logger.entries().len(), 1);
}
#[test]
fn test_logger_output_method_logs_at_info() {
let l = StructuredLogger::with_level(LogLevel::Debug);
l.output("hello");
let entries = l.entries();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].level, LogLevel::Info);
assert_eq!(entries[0].message, "hello");
}
#[test]
fn test_metrics_increment_and_read() {
let mut m = Metrics::new();
m.increment_counter("req");
m.increment_counter("req");
m.increment_counter("err");
assert_eq!(m.get_counter("req"), Some(2));
assert_eq!(m.get_counter("err"), Some(1));
assert_eq!(m.get_counter("missing"), None);
}
#[test]
fn test_metrics_add_counter() {
let mut m = Metrics::new();
m.add_counter("bytes", 100);
m.add_counter("bytes", 50);
assert_eq!(m.get_counter("bytes"), Some(150));
}
#[test]
fn test_metrics_gauge_overwrites() {
let mut m = Metrics::new();
m.set_gauge("cpu", 0.5);
assert_eq!(m.get_gauge("cpu"), Some(0.5));
m.set_gauge("cpu", 0.8); assert_eq!(m.get_gauge("cpu"), Some(0.8));
assert_eq!(m.get_gauge("missing"), None);
}
#[test]
fn test_metrics_snapshot_captures_state() {
let mut m = Metrics::new();
m.increment_counter("a");
m.increment_counter("a");
m.increment_counter("b");
m.set_gauge("g1", 1.0);
m.set_gauge("g2", 2.5);
let snap = m.snapshot();
assert_eq!(snap.counters.get("a"), Some(&2));
assert_eq!(snap.counters.get("b"), Some(&1));
assert_eq!(snap.gauges.get("g1"), Some(&1.0));
assert_eq!(snap.gauges.get("g2"), Some(&2.5));
m.increment_counter("a");
assert_eq!(snap.counters.get("a"), Some(&2));
assert_eq!(m.get_counter("a"), Some(3));
}
#[test]
fn test_log_entry_has_timestamp_and_level() {
let l = StructuredLogger::with_level(LogLevel::Debug);
l.log(LogLevel::Warn, "warning");
let e = &l.entries()[0];
assert_eq!(e.level, LogLevel::Warn);
assert_eq!(e.message, "warning");
assert!(e.timestamp.contains('T'));
assert!(e.timestamp.ends_with('Z') || e.timestamp.contains('+'));
}
}
#[cfg(all(test, feature = "prod-log-level"))]
mod prod_tests {
use super::*;
#[test]
fn test_prod_config_debug_in_production_rejected() {
let cfg = LoggerProdConfig::new(LogLevel::Debug, EnvKind::Production);
let err = cfg.validate().unwrap_err();
assert!(err.to_string().contains("DEBUG"));
assert!(err.to_string().contains("forbidden in production"));
}
#[test]
fn test_prod_config_trace_in_production_rejected() {
let cfg = LoggerProdConfig::new(LogLevel::Trace, EnvKind::Production);
assert!(cfg.validate().is_err());
}
#[test]
fn test_prod_config_info_in_production_rejected() {
let cfg = LoggerProdConfig::new(LogLevel::Info, EnvKind::Production);
assert!(cfg.validate().is_err());
}
#[test]
fn test_prod_config_warn_in_production_accepted() {
let cfg = LoggerProdConfig::new(LogLevel::Warn, EnvKind::Production);
assert!(cfg.validate().is_ok());
}
#[test]
fn test_prod_config_error_in_production_accepted() {
let cfg = LoggerProdConfig::new(LogLevel::Error, EnvKind::Production);
assert!(cfg.validate().is_ok());
}
#[test]
fn test_prod_config_debug_in_development_accepted() {
let cfg = LoggerProdConfig::new(LogLevel::Debug, EnvKind::Development);
assert!(cfg.validate().is_ok());
}
#[test]
fn test_prod_config_trace_in_staging_accepted() {
let cfg = LoggerProdConfig::new(LogLevel::Trace, EnvKind::Staging);
assert!(cfg.validate().is_ok());
}
}