use chrono::prelude::*;
use lazy_static::lazy_static;
use std::io::Write;
use ansi_term::Color;
use fs4::FileExt;
lazy_static! {
static ref LOGGER: std::sync::RwLock<Logger> = std::sync::RwLock::new(Logger::new());
}
pub fn set_logger(new_logger: Logger) {
let mut logger = LOGGER.write().expect("Could not access the logger");
*logger = new_logger;
}
#[derive(Clone)]
pub struct Logger {
pub path: Option<String>,
pub terminal_output: bool,
pub file_output: bool,
pub output_level: Level,
pub ignore_levels: Vec<Level>,
pub log_format: String,
pub timestamp_format: String,
}
impl Logger {
pub fn new() -> Self {
Self {
path: None,
terminal_output: true,
file_output: false,
output_level: Level::Info,
ignore_levels: Vec::new(),
log_format: String::from("[{timestamp} {level} {module_path}] {message}"),
timestamp_format: String::from("%Y-%m-%d %H:%M:%S"),
}
}
pub fn path(&mut self, path: &str) {
self.path = Some(path.to_string());
set_logger(self.clone());
}
pub fn terminal(&mut self, value: bool) {
self.terminal_output = value;
set_logger(self.clone());
}
pub fn file(&mut self, value: bool) {
self.file_output = value;
set_logger(self.clone());
}
pub fn level(&mut self, level: Level) {
self.output_level = level;
set_logger(self.clone());
}
pub fn ignore(&mut self, level: Level) {
self.ignore_levels.push(level);
set_logger(self.clone());
}
pub fn log_format(&mut self, format: &str) {
self.log_format = format.to_string();
set_logger(self.clone());
}
pub fn timestamp_format(&mut self, format: &str) {
self.timestamp_format = format.to_string();
set_logger(self.clone());
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Info = 0,
Debug = 1,
Warning = 2,
Error = 3,
Critical = 4,
None = 255,
}
impl ToString for Level {
fn to_string(&self) -> String {
match self {
Level::Info => String::from("INFO"),
Level::Debug => String::from("DEBUG"),
Level::Warning => String::from("WARNING"),
Level::Error => String::from("ERROR"),
Level::Critical => String::from("CRITICAL"),
Level::None => String::from("NONE"),
}
}
}
pub fn log(level: Level, module_path: &str, message: &str) {
let logger = LOGGER.read().expect("Could not read logger").clone();
if level < logger.output_level {
return;
}
let now = Local::now();
let time = now.format(&logger.timestamp_format).to_string();
let log_format = logger.log_format
.replace("{timestamp}", &time)
.replace("{module_path}", module_path)
.replace("{message}", message);
if logger.path.is_some() && logger.file_output {
let path = logger.path.unwrap();
let mut file = std::fs::OpenOptions::new()
.read(true)
.append(true)
.create(true)
.open(path.as_str())
.expect("Failed to open file");
let format = log_format.replace("{level}", &level.to_string());
file.lock_exclusive().expect("Could not lock file for logging");
match writeln!(file, "{}", format) { _ => () } file.unlock().expect("Could not unlock file after writing");
}
if logger.terminal_output {
let level_color = match level {
Level::Info => Color::Green.normal(),
Level::Debug => Color::Blue.normal(),
Level::Warning => Color::Yellow.normal(),
Level::Error => Color::Red.normal(),
Level::Critical => Color::Red.bold(),
Level::None => Color::White.normal(), };
let format = log_format.replace(
"{level}", &level_color.paint(level.to_string()).to_string()
);
println!("{}", format);
}
}
#[macro_export]
macro_rules! info {
($message:expr) => {
log(Level::Info, module_path!(), $message);
};
}
#[macro_export]
macro_rules! debug {
($message:expr) => {
log(Level::Debug, module_path!(), $message);
};
}
#[macro_export]
macro_rules! warning {
($message:expr) => {
log(Level::Warning, module_path!(), $message);
};
}
#[macro_export]
macro_rules! error {
($message:expr) => {
log(Level::Error, module_path!(), $message);
};
}
#[macro_export]
macro_rules! critical {
($message:expr) => {
log(Level::Critical, module_path!(), $message);
};
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{self, File};
use std::io::Read;
#[test]
fn test_level_filtering() {
let mut logger = Logger::new();
logger.level(Level::Error);
assert!(Level::Info < logger.output_level);
assert!(Level::Debug < logger.output_level);
assert!(Level::Warning < logger.output_level);
assert!(Level::Error >= logger.output_level);
assert!(Level::Critical >= logger.output_level);
}
#[test]
fn test_level_none() {
let mut logger = Logger::new();
logger.level(Level::None);
assert!(Level::Info < logger.output_level);
assert!(Level::Debug < logger.output_level);
assert!(Level::Warning < logger.output_level);
assert!(Level::Error < logger.output_level);
assert!(Level::Critical < logger.output_level);
}
#[test]
fn test_log_format() {
let mut logger = Logger::new();
logger.log_format("{level} - {message}");
let formatted_message = logger.log_format
.replace("{level}", "INFO")
.replace("{message}", "Test message");
assert_eq!(formatted_message, "INFO - Test message");
}
#[test]
fn test_file_output() {
let mut logger = Logger::new();
let test_file_path = "test_log.txt";
logger.file(true);
logger.path(test_file_path);
log(Level::Info, "test_file_output", "Test log message for file output");
let mut file = File::open(test_file_path).expect("Failed to open log file");
let mut contents = String::new();
file.read_to_string(&mut contents).expect("Failed to read log file");
assert!(contents.contains("Test log message for file output"), "Log message not found in file");
fs::remove_file(test_file_path).expect("Failed to delete test log file");
logger.file(false);
log(Level::Info, "test_file_output", "Second test message for file output");
assert!(!fs::metadata(test_file_path).is_ok(), "Log file should not exist when file output is disabled");
}
}