use std::collections::HashSet;
use std::fmt;
use std::time::SystemTime;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
impl LogLevel {
#[must_use]
#[inline]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Error => "ERROR",
Self::Warn => "WARN",
Self::Info => "INFO",
Self::Debug => "DEBUG",
Self::Trace => "TRACE",
}
}
#[must_use]
#[inline]
pub const fn colored(&self) -> &'static str {
match self {
Self::Error => "\x1b[31mERROR\x1b[0m", Self::Warn => "\x1b[33mWARN\x1b[0m", Self::Info => "\x1b[32mINFO\x1b[0m", Self::Debug => "\x1b[36mDEBUG\x1b[0m", Self::Trace => "\x1b[90mTRACE\x1b[0m", }
}
#[must_use]
#[inline]
pub const fn should_log(&self, configured_level: &Self) -> bool {
(*self as u8) <= (*configured_level as u8)
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct LogConfig {
pub level: LogLevel,
pub include_timestamps: bool,
pub include_module_path: bool,
pub include_line_numbers: bool,
pub filter_modules: Vec<String>,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
level: LogLevel::Info,
include_timestamps: true,
include_module_path: true,
include_line_numbers: false,
filter_modules: Vec::new(),
}
}
}
impl LogConfig {
#[must_use]
#[inline]
pub const fn new(level: LogLevel) -> Self {
Self {
level,
include_timestamps: true,
include_module_path: true,
include_line_numbers: false,
filter_modules: Vec::new(),
}
}
#[must_use]
#[inline]
pub const fn minimal(level: LogLevel) -> Self {
Self {
level,
include_timestamps: false,
include_module_path: false,
include_line_numbers: false,
filter_modules: Vec::new(),
}
}
#[must_use]
#[inline]
pub const fn verbose(level: LogLevel) -> Self {
Self {
level,
include_timestamps: true,
include_module_path: true,
include_line_numbers: true,
filter_modules: Vec::new(),
}
}
#[must_use]
pub fn with_module_filter(mut self, module: String) -> Self {
self.filter_modules.push(module);
self
}
}
pub struct Logger {
config: LogConfig,
filter_set: HashSet<String>,
use_color: bool,
}
impl Logger {
#[must_use]
pub fn new(config: LogConfig) -> Self {
let filter_set: HashSet<String> = config.filter_modules.iter().cloned().collect();
Self {
config,
filter_set,
use_color: is_terminal(),
}
}
#[must_use]
#[inline]
pub fn default_config() -> Self {
Self::new(LogConfig::default())
}
#[must_use]
#[inline]
fn should_log_module(&self, module: &str) -> bool {
if self.filter_set.is_empty() {
return true;
}
self.filter_set
.iter()
.any(|filter| module.starts_with(filter) || filter.starts_with(module))
}
pub fn log(&self, level: LogLevel, module: &str, message: &str, line: Option<u32>) {
if !level.should_log(&self.config.level) {
return;
}
if !self.should_log_module(module) {
return;
}
let mut parts = Vec::new();
if self.config.include_timestamps {
let timestamp = format_timestamp();
parts.push(timestamp);
}
let level_str = if self.use_color {
level.colored().to_string()
} else {
level.as_str().to_string()
};
parts.push(format!("[{}]", level_str));
if self.config.include_module_path {
parts.push(format!("[{}]", module));
}
if self.config.include_line_numbers {
if let Some(line_num) = line {
parts.push(format!("[L{}]", line_num));
}
}
parts.push(message.to_string());
println!("{}", parts.join(" "));
}
#[inline]
pub fn error(&self, module: &str, message: &str) {
self.log(LogLevel::Error, module, message, None);
}
#[inline]
pub fn warn(&self, module: &str, message: &str) {
self.log(LogLevel::Warn, module, message, None);
}
#[inline]
pub fn info(&self, module: &str, message: &str) {
self.log(LogLevel::Info, module, message, None);
}
#[inline]
pub fn debug(&self, module: &str, message: &str) {
self.log(LogLevel::Debug, module, message, None);
}
#[inline]
pub fn trace(&self, module: &str, message: &str) {
self.log(LogLevel::Trace, module, message, None);
}
#[inline]
pub fn error_at(&self, module: &str, message: &str, line: u32) {
self.log(LogLevel::Error, module, message, Some(line));
}
#[inline]
pub fn warn_at(&self, module: &str, message: &str, line: u32) {
self.log(LogLevel::Warn, module, message, Some(line));
}
pub fn structured(
&self,
level: LogLevel,
module: &str,
message: &str,
fields: &[(&str, &str)],
) {
if !level.should_log(&self.config.level) {
return;
}
if !self.should_log_module(module) {
return;
}
let fields_str: Vec<String> = fields.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
let full_message = if fields_str.is_empty() {
message.to_string()
} else {
format!("{} {}", message, fields_str.join(" "))
};
self.log(level, module, &full_message, None);
}
#[inline]
pub fn perf(&self, module: &str, operation: &str, duration_ms: u64) {
let message = format!("{} completed in {}ms", operation, duration_ms);
self.structured(
LogLevel::Debug,
module,
&message,
&[
("operation", operation),
("duration_ms", &duration_ms.to_string()),
],
);
}
#[must_use]
#[inline]
pub const fn level(&self) -> LogLevel {
self.config.level
}
pub fn set_level(&mut self, level: LogLevel) {
self.config.level = level;
}
#[inline]
pub fn set_color(&mut self, use_color: bool) {
self.use_color = use_color;
}
}
#[must_use]
fn format_timestamp() -> String {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
let secs = now.as_secs();
let millis = now.subsec_millis();
let hours = (secs / 3600) % 24;
let minutes = (secs / 60) % 60;
let seconds = secs % 60;
format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
}
#[must_use]
#[inline]
fn is_terminal() -> bool {
std::env::var("TERM").is_ok()
}
#[macro_export]
macro_rules! log_error {
($logger:expr, $($arg:tt)*) => {
$logger.error(module_path!(), &format!($($arg)*))
};
}
#[macro_export]
macro_rules! log_warn {
($logger:expr, $($arg:tt)*) => {
$logger.warn(module_path!(), &format!($($arg)*))
};
}
#[macro_export]
macro_rules! log_info {
($logger:expr, $($arg:tt)*) => {
$logger.info(module_path!(), &format!($($arg)*))
};
}
#[macro_export]
macro_rules! log_debug {
($logger:expr, $($arg:tt)*) => {
$logger.debug(module_path!(), &format!($($arg)*))
};
}
#[macro_export]
macro_rules! log_trace {
($logger:expr, $($arg:tt)*) => {
$logger.trace(module_path!(), &format!($($arg)*))
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_level_ordering() {
assert!(LogLevel::Error < LogLevel::Warn);
assert!(LogLevel::Warn < LogLevel::Info);
assert!(LogLevel::Info < LogLevel::Debug);
assert!(LogLevel::Debug < LogLevel::Trace);
}
#[test]
fn test_should_log() {
let configured_level = LogLevel::Info;
assert!(LogLevel::Error.should_log(&configured_level));
assert!(LogLevel::Warn.should_log(&configured_level));
assert!(LogLevel::Info.should_log(&configured_level));
assert!(!LogLevel::Debug.should_log(&configured_level));
assert!(!LogLevel::Trace.should_log(&configured_level));
}
#[test]
fn test_log_config_default() {
let config = LogConfig::default();
assert_eq!(config.level, LogLevel::Info);
assert!(config.include_timestamps);
assert!(config.include_module_path);
assert!(!config.include_line_numbers);
}
#[test]
fn test_log_config_minimal() {
let config = LogConfig::minimal(LogLevel::Debug);
assert_eq!(config.level, LogLevel::Debug);
assert!(!config.include_timestamps);
assert!(!config.include_module_path);
assert!(!config.include_line_numbers);
}
#[test]
fn test_log_config_verbose() {
let config = LogConfig::verbose(LogLevel::Trace);
assert_eq!(config.level, LogLevel::Trace);
assert!(config.include_timestamps);
assert!(config.include_module_path);
assert!(config.include_line_numbers);
}
#[test]
fn test_logger_creation() {
let config = LogConfig::default();
let logger = Logger::new(config);
assert_eq!(logger.level(), LogLevel::Info);
}
#[test]
fn test_module_filtering() {
let config = LogConfig::default().with_module_filter("chie_core::storage".to_string());
let logger = Logger::new(config);
assert!(logger.should_log_module("chie_core::storage"));
assert!(logger.should_log_module("chie_core::storage::chunk"));
assert!(!logger.should_log_module("chie_core::network"));
}
#[test]
fn test_logger_level_change() {
let mut logger = Logger::default_config();
assert_eq!(logger.level(), LogLevel::Info);
logger.set_level(LogLevel::Debug);
assert_eq!(logger.level(), LogLevel::Debug);
}
#[test]
fn test_log_level_display() {
assert_eq!(LogLevel::Error.to_string(), "ERROR");
assert_eq!(LogLevel::Warn.to_string(), "WARN");
assert_eq!(LogLevel::Info.to_string(), "INFO");
assert_eq!(LogLevel::Debug.to_string(), "DEBUG");
assert_eq!(LogLevel::Trace.to_string(), "TRACE");
}
}