use crate::enums::log_level::LogLevel;
use std::collections::HashMap;
use std::io::{self, Write};
use std::sync::RwLock;
pub struct Logger {
level: LogLevel,
time_format: String,
format_strings: HashMap<LogLevel, String>,
targets: Vec<RwLock<Box<dyn Write + Send + Sync>>>,
}
impl Logger {
pub fn new() -> Self {
Self {
level: LogLevel::Info,
time_format: "%Y-%m-%d %H:%M:%S".to_string(),
format_strings: Self::default_format_strings(),
targets: vec![RwLock::new(Box::new(io::stdout()))],
}
}
pub fn with_level(level: LogLevel) -> Self {
Self {
level,
time_format: "%Y-%m-%d %H:%M:%S".to_string(),
format_strings: Self::default_format_strings(),
targets: vec![RwLock::new(Box::new(io::stdout()))],
}
}
pub fn from_env() -> Self {
use crate::utils::log_util::parse_log_level_from_env;
let level = parse_log_level_from_env();
Self::with_level(level)
}
pub fn time_format(mut self, format: &str) -> Self {
self.time_format = format.to_string();
self
}
pub fn no_time_prefix(mut self) -> Self {
self.time_format = String::new();
self
}
pub fn format(mut self, format: String) -> Self {
let levels = [
LogLevel::Error,
LogLevel::Warning,
LogLevel::Info,
LogLevel::Debug,
LogLevel::Trace,
];
for level in levels {
self.format_strings.insert(level, format.clone());
}
self
}
pub fn format_for_level(mut self, level: LogLevel, format: String) -> Self {
self.format_strings.insert(level, format);
self
}
pub fn stdout(mut self) -> Self {
self.targets = vec![RwLock::new(Box::new(io::stdout()))];
self
}
pub fn stderr(mut self) -> Self {
self.targets = vec![RwLock::new(Box::new(io::stderr()))];
self
}
pub fn file(mut self, path: &str) -> io::Result<Self> {
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
self.targets = vec![RwLock::new(Box::new(file))];
Ok(self)
}
pub fn custom<W>(mut self, target: W) -> Self
where
W: Write + Send + Sync + 'static
{
self.targets = vec![RwLock::new(Box::new(target))];
self
}
pub fn add_stdout(mut self) -> Self {
self.targets.push(RwLock::new(Box::new(io::stdout())));
self
}
pub fn add_stderr(mut self) -> Self {
self.targets.push(RwLock::new(Box::new(io::stderr())));
self
}
pub fn add_file(mut self, path: &str) -> io::Result<Self> {
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
self.targets.push(RwLock::new(Box::new(file)));
Ok(self)
}
pub fn add_target<W>(mut self, target: W) -> Self
where
W: Write + Send + Sync + 'static
{
self.targets.push(RwLock::new(Box::new(target)));
self
}
pub fn log(&self, message: &str) -> io::Result<()> {
let formatted = self.format_message_simple(message);
for target in &self.targets {
let mut target = target.write().unwrap();
writeln!(target, "{}", formatted)?;
target.flush()?;
}
Ok(())
}
pub fn log_lazy<F>(&self, message_fn: F) -> io::Result<()>
where
F: FnOnce() -> String,
{
let message = message_fn();
self.log(&message)
}
pub(crate) fn log_lazy_with_level<F>(&self, level: LogLevel, message_fn: F) -> io::Result<()>
where
F: FnOnce() -> String,
{
if level < self.level {
return Ok(());
}
let message = message_fn();
self.log_with_level(level, &message)
}
pub(crate) fn log_with_level(&self, level: LogLevel, message: &str) -> io::Result<()> {
if level < self.level {
return Ok(());
}
let formatted = self.format_message(level, message);
for target in &self.targets {
let mut target = target.write().unwrap();
writeln!(target, "{}", formatted)?;
target.flush()?;
}
Ok(())
}
pub fn error(&self, message: &str) -> io::Result<()> {
self.log_with_level(LogLevel::Error, message)
}
pub fn warning(&self, message: &str) -> io::Result<()> {
self.log_with_level(LogLevel::Warning, message)
}
pub fn info(&self, message: &str) -> io::Result<()> {
self.log_with_level(LogLevel::Info, message)
}
pub fn debug(&self, message: &str) -> io::Result<()> {
self.log_with_level(LogLevel::Debug, message)
}
pub fn trace(&self, message: &str) -> io::Result<()> {
self.log_with_level(LogLevel::Trace, message)
}
pub fn error_lazy<F>(&self, message_fn: F) -> io::Result<()>
where
F: FnOnce() -> String,
{
self.log_lazy_with_level(LogLevel::Error, message_fn)
}
pub fn warning_lazy<F>(&self, message_fn: F) -> io::Result<()>
where
F: FnOnce() -> String,
{
self.log_lazy_with_level(LogLevel::Warning, message_fn)
}
pub fn info_lazy<F>(&self, message_fn: F) -> io::Result<()>
where
F: FnOnce() -> String,
{
self.log_lazy_with_level(LogLevel::Info, message_fn)
}
pub fn debug_lazy<F>(&self, message_fn: F) -> io::Result<()>
where
F: FnOnce() -> String,
{
self.log_lazy_with_level(LogLevel::Debug, message_fn)
}
pub fn trace_lazy<F>(&self, message_fn: F) -> io::Result<()>
where
F: FnOnce() -> String,
{
self.log_lazy_with_level(LogLevel::Trace, message_fn)
}
pub fn set_level(&mut self, level: LogLevel) {
self.level = level;
}
pub fn set_time_format(&mut self, format: &str) {
self.time_format = format.to_string();
}
pub fn disable_time_prefix(&mut self) {
self.time_format = String::new();
}
pub fn set_format_for_level(&mut self, level: LogLevel, format: &str) {
self.format_strings.insert(level, format.to_string());
}
pub fn level(&self) -> LogLevel {
self.level
}
pub fn get_level(&self) -> LogLevel {
self.level
}
pub fn clear_targets(mut self) -> Self {
self.targets.clear();
self
}
fn default_format_strings() -> HashMap<LogLevel, String> {
let mut formats = HashMap::new();
formats.insert(LogLevel::Error, "{time} [{level}] {message}".to_string());
formats.insert(LogLevel::Warning, "{time} [{level}] {message}".to_string());
formats.insert(LogLevel::Info, "{time} [{level}] {message}".to_string());
formats.insert(LogLevel::Debug, "{time} [{level}] {message}".to_string());
formats.insert(LogLevel::Trace, "{time} [{level}] {message}".to_string());
formats
}
fn format_message(&self, level: LogLevel, message: &str) -> String {
let format_string = self
.format_strings
.get(&level)
.unwrap_or(&self.format_strings[&LogLevel::Info])
.clone();
let time_str = if self.time_format.is_empty() {
String::new()
} else {
use simple_datetime_rs::{DateTime, Format};
DateTime::now()
.format(&self.time_format)
.unwrap_or_default()
};
format_string
.replace("{time}", &time_str)
.replace("{level}", &level.to_string())
.replace("{message}", message)
}
fn format_message_simple(&self, message: &str) -> String {
if self.time_format.is_empty() {
message.to_string()
} else {
use simple_datetime_rs::{DateTime, Format};
let time_str = DateTime::now()
.format(&self.time_format)
.unwrap_or_default();
format!("{} {}", time_str, message)
}
}
}
impl Default for Logger {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn create_test_logger() -> (Logger, std::io::Cursor<Vec<u8>>) {
let cursor = Cursor::new(Vec::<u8>::new());
let cursor_clone = Cursor::new(Vec::<u8>::new());
let logger = Logger {
level: LogLevel::Info,
time_format: String::new(), format_strings: Logger::default_format_strings(),
targets: vec![RwLock::new(Box::new(cursor_clone))],
};
(logger, cursor)
}
fn create_test_logger_with_level(level: LogLevel) -> (Logger, std::io::Cursor<Vec<u8>>) {
let (mut logger, cursor) = create_test_logger();
logger.level = level;
(logger, cursor)
}
fn create_capturable_test_logger() -> (Logger, std::sync::Arc<std::sync::Mutex<Vec<u8>>>) {
use std::io::Write;
let buffer = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
let buffer_clone = buffer.clone();
struct CapturingWriter {
buffer: std::sync::Arc<std::sync::Mutex<Vec<u8>>>,
}
impl Write for CapturingWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.buffer.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let logger = Logger {
level: LogLevel::Info,
time_format: String::new(), format_strings: Logger::default_format_strings(),
targets: vec![RwLock::new(Box::new(CapturingWriter {
buffer: buffer_clone,
}))],
};
(logger, buffer)
}
#[test]
fn test_simple_logging() -> io::Result<()> {
let (logger, _cursor) = create_test_logger();
assert_eq!(logger.level(), LogLevel::Info);
logger.info("Hello, world!")?;
logger.warning("This is a warning")?;
logger.error("This is an error")?;
assert_eq!(logger.level(), LogLevel::Info);
Ok(())
}
#[test]
fn test_time_format() -> io::Result<()> {
let (mut logger, _cursor) = create_test_logger();
logger.time_format = "%Y-%m-%d %H:%M:%S".to_string();
assert_eq!(logger.level(), LogLevel::Info);
logger.info("Test message")?;
logger.warning("Another test message")?;
Ok(())
}
#[test]
fn test_custom_format() -> io::Result<()> {
let (mut logger, _cursor) = create_test_logger();
logger
.format_strings
.insert(LogLevel::Error, "ERROR: {message}".to_string());
assert_eq!(logger.level(), LogLevel::Info);
logger.error("Something went wrong")?;
logger.info("This should use default format")?;
logger.warning("This should also use default format")?;
Ok(())
}
#[test]
fn test_log_level_filtering() -> io::Result<()> {
let (logger, _cursor) = create_test_logger_with_level(LogLevel::Warning);
assert_eq!(logger.level(), LogLevel::Warning);
logger.info("This should not appear")?;
logger.debug("This should not appear")?;
logger.trace("This should not appear")?;
logger.warning("This should appear")?;
logger.error("This should also appear")?;
assert_eq!(logger.level(), LogLevel::Warning);
Ok(())
}
#[test]
fn test_multiple_loggers() -> io::Result<()> {
let (logger1, _cursor1) = create_test_logger();
let (logger2, _cursor2) = create_test_logger_with_level(LogLevel::Warning);
assert_eq!(logger1.level(), LogLevel::Info);
assert_eq!(logger2.level(), LogLevel::Warning);
logger1.info("Message from logger 1")?;
logger2.warning("Message from logger 2")?;
logger1.info("Another message from logger 1")?;
logger2.info("This should be filtered by logger 2")?;
assert_eq!(logger1.level(), LogLevel::Info);
assert_eq!(logger2.level(), LogLevel::Warning);
Ok(())
}
#[test]
fn test_lazy_logging_execution() -> io::Result<()> {
let (logger, _cursor) = create_test_logger_with_level(LogLevel::Info);
let mut expensive_called = false;
logger.debug_lazy(|| {
expensive_called = true;
"This should not be computed".to_string()
})?;
assert!(
!expensive_called,
"Expensive computation should not have been called"
);
expensive_called = false;
logger.info_lazy(|| {
expensive_called = true;
"This should be computed".to_string()
})?;
assert!(
expensive_called,
"Expensive computation should have been called"
);
Ok(())
}
#[test]
fn test_lazy_logging_with_expensive_computation() -> io::Result<()> {
let (logger, _cursor) = create_test_logger_with_level(LogLevel::Warning);
use std::cell::RefCell;
let computation_count = RefCell::new(0);
logger.trace_lazy(|| {
*computation_count.borrow_mut() += 1;
"Trace message".to_string()
})?;
logger.debug_lazy(|| {
*computation_count.borrow_mut() += 1;
"Debug message".to_string()
})?;
logger.info_lazy(|| {
*computation_count.borrow_mut() += 1;
"Info message".to_string()
})?;
assert_eq!(
*computation_count.borrow(),
0,
"No expensive computations should have been executed"
);
logger.warning_lazy(|| {
*computation_count.borrow_mut() += 1;
"Warning message".to_string()
})?;
assert_eq!(
*computation_count.borrow(),
1,
"One expensive computation should have been executed"
);
logger.error_lazy(|| {
*computation_count.borrow_mut() += 1;
"Error message".to_string()
})?;
assert_eq!(
*computation_count.borrow(),
2,
"Two expensive computations should have been executed"
);
Ok(())
}
#[test]
fn test_lazy_logging_vs_regular_logging() -> io::Result<()> {
let (logger, _cursor) = create_test_logger_with_level(LogLevel::Warning);
let mut lazy_called = false;
logger.warning_lazy(|| {
lazy_called = true;
"Lazy warning".to_string()
})?;
logger.warning("Regular warning")?;
assert!(lazy_called, "Lazy closure should have been called");
Ok(())
}
#[test]
fn test_multi_target_logging() -> io::Result<()> {
let (logger, _cursor) = create_test_logger();
logger.info("Test message for multi-target logging")?;
Ok(())
}
#[test]
fn test_multi_target_lazy_logging() -> io::Result<()> {
let (logger, _cursor) = create_test_logger();
let mut call_count = 0;
logger.info_lazy(|| {
call_count += 1;
"Lazy message for multiple targets".to_string()
})?;
assert_eq!(call_count, 1, "Lazy closure should be called only once");
Ok(())
}
#[test]
fn test_log_output_verification() -> io::Result<()> {
let (logger, buffer) = create_capturable_test_logger();
logger.info("Test message")?;
std::thread::sleep(std::time::Duration::from_millis(10));
let captured = buffer.lock().unwrap();
let output = String::from_utf8_lossy(&captured);
assert!(
output.contains("Test message"),
"Output should contain the log message"
);
assert!(
output.contains("[INFO]"),
"Output should contain the log level"
);
Ok(())
}
#[test]
fn test_custom_format_output_verification() -> io::Result<()> {
let (mut logger, buffer) = create_capturable_test_logger();
logger
.format_strings
.insert(LogLevel::Error, "ERROR: {message}".to_string());
logger.error("Something went wrong")?;
std::thread::sleep(std::time::Duration::from_millis(10));
let captured = buffer.lock().unwrap();
let output = String::from_utf8_lossy(&captured);
assert!(
output.contains("ERROR: Something went wrong"),
"Output should contain the custom formatted message"
);
Ok(())
}
#[test]
fn test_format_sets_same_format_for_all_levels_output() -> io::Result<()> {
const LOG_PREFIX: &str = "worker-1";
let (logger, buffer) = create_capturable_test_logger();
let logger = logger.format(format!("[{{level}}][{}] {{message}}", LOG_PREFIX));
logger.error("Error message")?;
logger.info("Info message")?;
std::thread::sleep(std::time::Duration::from_millis(10));
let captured = buffer.lock().unwrap();
let output = String::from_utf8_lossy(&captured);
assert!(
output.contains("[ERROR][worker-1] Error message"),
"Output should contain formatted error message with prefix"
);
assert!(
output.contains("[INFO][worker-1] Info message"),
"Output should contain formatted info message with prefix"
);
Ok(())
}
}