#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(feature = "std"))]
use alloc::{borrow::Cow, collections::BTreeMap as HashMap, vec::Vec};
#[cfg(feature = "std")]
use std::{borrow::Cow, collections::HashMap};
use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum LogLevel {
Emergency = 0,
Alert = 1,
Critical = 2,
Error = 3,
Warning = 4,
Notice = 5,
Info = 6,
Debug = 7,
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogLevel::Emergency => write!(f, "EMERGENCY"),
LogLevel::Alert => write!(f, "ALERT"),
LogLevel::Critical => write!(f, "CRITICAL"),
LogLevel::Error => write!(f, "ERROR"),
LogLevel::Warning => write!(f, "WARNING"),
LogLevel::Notice => write!(f, "NOTICE"),
LogLevel::Info => write!(f, "INFO"),
LogLevel::Debug => write!(f, "DEBUG"),
}
}
}
#[derive(Debug, Clone)]
pub struct LogRecord<'a> {
pub level: LogLevel,
pub timestamp: Option<u64>,
pub component: Option<&'a str>,
pub message: &'a str,
pub metadata: Option<&'a [(&'a str, &'a str)]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogError {
IoError,
BufferFull,
BackendUnavailable,
}
impl fmt::Display for LogError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogError::IoError => write!(f, "I/O error"),
LogError::BufferFull => write!(f, "buffer full"),
LogError::BackendUnavailable => write!(f, "backend unavailable"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for LogError {}
pub trait LogBackend: Send + Sync {
fn emit(&self, record: &LogRecord) -> Result<(), LogError>;
fn accepts(&self, level: LogLevel) -> bool;
fn flush(&self) -> Result<(), LogError> {
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct LogFilter {
min_level: LogLevel,
component_overrides: HashMap<Cow<'static, str>, LogLevel>,
}
impl Default for LogFilter {
fn default() -> Self {
Self { min_level: LogLevel::Info, component_overrides: HashMap::new() }
}
}
impl LogFilter {
pub fn new(min_level: LogLevel) -> Self {
Self { min_level, component_overrides: HashMap::new() }
}
pub fn with_component(mut self, component: impl Into<Cow<'static, str>>, level: LogLevel) -> Self {
self.component_overrides.insert(component.into(), level);
self
}
pub fn should_log(&self, level: LogLevel, component: Option<&str>) -> bool {
let threshold = if let Some(comp) = component {
self.component_overrides.get(comp).copied().unwrap_or(self.min_level)
} else {
self.min_level
};
level <= threshold
}
}
pub struct LoggerConfig {
pub backend: Box<dyn LogBackend>,
pub filter: LogFilter,
pub default_level: Option<LogLevel>,
}
impl core::fmt::Debug for LoggerConfig {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("LoggerConfig")
.field("backend", &"<dyn LogBackend>")
.field("filter", &self.filter)
.field("default_level", &self.default_level)
.finish()
}
}
impl LoggerConfig {
pub fn new(backend: Box<dyn LogBackend>, filter: LogFilter) -> Self {
Self { backend, filter, default_level: None }
}
pub fn with_default_level(mut self, level: LogLevel) -> Self {
self.default_level = Some(level);
self
}
}
#[cfg(feature = "std")]
#[derive(Debug, Clone, Copy)]
pub struct StdoutBackend;
#[cfg(feature = "std")]
impl Default for StdoutBackend {
fn default() -> Self {
Self
}
}
#[cfg(feature = "std")]
impl LogBackend for StdoutBackend {
fn emit(&self, record: &LogRecord) -> Result<(), LogError> {
let component = record.component.unwrap_or("app");
let timestamp = record.timestamp.map(|ts| format!("[{}] ", ts)).unwrap_or_default();
println!("{}[{}] {}: {}", timestamp, record.level, component, record.message);
if let Some(metadata) = record.metadata {
for (key, value) in metadata {
println!(" {}: {}", key, value);
}
}
Ok(())
}
fn accepts(&self, _level: LogLevel) -> bool {
true
}
}
pub struct MultiplexBackend {
backends: Vec<Box<dyn LogBackend>>,
}
impl MultiplexBackend {
pub fn new(backends: Vec<Box<dyn LogBackend>>) -> Self {
Self { backends }
}
}
impl LogBackend for MultiplexBackend {
fn emit(&self, record: &LogRecord) -> Result<(), LogError> {
for backend in &self.backends {
if backend.accepts(record.level) {
backend.emit(record)?;
}
}
Ok(())
}
fn accepts(&self, level: LogLevel) -> bool {
self.backends.iter().any(|b| b.accepts(level))
}
fn flush(&self) -> Result<(), LogError> {
for backend in &self.backends {
backend.flush()?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::trace::TraceCollector;
#[test]
fn test_log_level_ordering() {
assert!(LogLevel::Emergency < LogLevel::Alert);
assert!(LogLevel::Alert < LogLevel::Critical);
assert!(LogLevel::Critical < LogLevel::Error);
assert!(LogLevel::Error < LogLevel::Warning);
assert!(LogLevel::Warning < LogLevel::Notice);
assert!(LogLevel::Notice < LogLevel::Info);
assert!(LogLevel::Info < LogLevel::Debug);
}
#[test]
fn test_log_level_display() {
assert_eq!(format!("{}", LogLevel::Emergency), "EMERGENCY");
assert_eq!(format!("{}", LogLevel::Error), "ERROR");
assert_eq!(format!("{}", LogLevel::Debug), "DEBUG");
}
#[test]
fn test_log_filter_global_level() {
let filter = LogFilter::new(LogLevel::Warning);
assert!(filter.should_log(LogLevel::Emergency, None));
assert!(filter.should_log(LogLevel::Error, None));
assert!(filter.should_log(LogLevel::Warning, None));
assert!(!filter.should_log(LogLevel::Notice, None));
assert!(!filter.should_log(LogLevel::Info, None));
assert!(!filter.should_log(LogLevel::Debug, None));
}
#[test]
fn test_log_filter_component_override() {
let filter = LogFilter::new(LogLevel::Warning).with_component("security", LogLevel::Debug);
assert!(!filter.should_log(LogLevel::Info, Some("database")));
assert!(filter.should_log(LogLevel::Error, Some("database")));
assert!(filter.should_log(LogLevel::Debug, Some("security")));
assert!(filter.should_log(LogLevel::Info, Some("security")));
}
#[derive(Debug, Clone)]
struct MockBackend {
captured: Arc<std::sync::Mutex<Vec<(LogLevel, String)>>>,
}
impl MockBackend {
fn with_captured(captured: Arc<std::sync::Mutex<Vec<(LogLevel, String)>>>) -> Self {
Self { captured }
}
}
impl LogBackend for MockBackend {
fn emit(&self, record: &LogRecord) -> Result<(), LogError> {
self.captured
.lock()
.map_err(|_| LogError::BackendUnavailable)?
.push((record.level, record.message.to_string()));
Ok(())
}
fn accepts(&self, _level: LogLevel) -> bool {
true
}
}
#[cfg(feature = "std")]
#[test]
fn test_logger_config_default_level() -> Result<(), crate::TightBeamError> {
let captured = Arc::new(std::sync::Mutex::new(Vec::new()));
let backend = MockBackend::with_captured(Arc::clone(&captured));
let config =
LoggerConfig::new(Box::new(backend), LogFilter::new(LogLevel::Debug)).with_default_level(LogLevel::Info);
let trace = TraceCollector::default().with_logger(config);
trace.event("test1")?.emit();
trace.event("test2")?.with_log_level(LogLevel::Error).emit();
let records = captured.lock().ok().map(|r| r.clone()).unwrap_or_default();
assert_eq!(records.len(), 2);
assert_eq!(records[0].0, LogLevel::Info);
assert_eq!(records[0].1, "test1");
assert_eq!(records[1].0, LogLevel::Error);
assert_eq!(records[1].1, "test2");
Ok(())
}
#[cfg(feature = "std")]
#[test]
fn test_logger_config_no_default_level() -> Result<(), crate::TightBeamError> {
let captured = Arc::new(std::sync::Mutex::new(Vec::new()));
let backend = MockBackend::with_captured(Arc::clone(&captured));
let config = LoggerConfig::new(Box::new(backend), LogFilter::new(LogLevel::Debug));
let trace = TraceCollector::default().with_logger(config);
trace.event("no_log")?.emit();
trace.event("should_log")?.with_log_level(LogLevel::Warning).emit();
let records = captured.lock().ok().map(|r| r.clone()).unwrap_or_default();
assert_eq!(records.len(), 1);
assert_eq!(records[0].0, LogLevel::Warning);
assert_eq!(records[0].1, "should_log");
Ok(())
}
#[cfg(feature = "std")]
#[test]
fn test_log_filter_with_logger() -> Result<(), crate::TightBeamError> {
let captured = Arc::new(std::sync::Mutex::new(Vec::new()));
let backend = MockBackend::with_captured(Arc::clone(&captured));
let config = LoggerConfig::new(Box::new(backend), LogFilter::new(LogLevel::Warning));
let trace = TraceCollector::default().with_logger(config);
trace.event("emergency")?.with_log_level(LogLevel::Emergency).emit();
trace.event("error")?.with_log_level(LogLevel::Error).emit();
trace.event("warning")?.with_log_level(LogLevel::Warning).emit();
trace.event("notice")?.with_log_level(LogLevel::Notice).emit();
trace.event("info")?.with_log_level(LogLevel::Info).emit();
trace.event("debug")?.with_log_level(LogLevel::Debug).emit();
let records = captured.lock().ok().map(|r| r.clone()).unwrap_or_default();
assert_eq!(records.len(), 3);
assert_eq!(records[0].1, "emergency");
assert_eq!(records[1].1, "error");
assert_eq!(records[2].1, "warning");
Ok(())
}
#[cfg(feature = "std")]
#[test]
fn test_multiplex_backend() -> Result<(), crate::TightBeamError> {
let captured1 = Arc::new(std::sync::Mutex::new(Vec::new()));
let captured2 = Arc::new(std::sync::Mutex::new(Vec::new()));
let backend1 = MockBackend::with_captured(Arc::clone(&captured1));
let backend2 = MockBackend::with_captured(Arc::clone(&captured2));
let multiplex = MultiplexBackend::new(vec![Box::new(backend1), Box::new(backend2)]);
let config = LoggerConfig::new(Box::new(multiplex), LogFilter::new(LogLevel::Debug));
let trace = TraceCollector::default().with_logger(config);
trace.event("test")?.with_log_level(LogLevel::Info).emit();
let records1 = captured1.lock().ok().map(|r| r.clone()).unwrap_or_default();
let records2 = captured2.lock().ok().map(|r| r.clone()).unwrap_or_default();
assert_eq!(records1.len(), 1);
assert_eq!(records2.len(), 1);
assert_eq!(records1[0].1, "test");
assert_eq!(records2[0].1, "test");
Ok(())
}
#[cfg(feature = "std")]
#[test]
fn test_trace_collector_default() -> Result<(), crate::TightBeamError> {
let trace = TraceCollector::default();
trace.event("test")?.emit();
Ok(())
}
#[cfg(feature = "std")]
#[test]
fn test_trace_collector_new() -> Result<(), crate::TightBeamError> {
let trace = TraceCollector::new();
trace.event("test")?.emit();
Ok(())
}
}