use chrono::{prelude::Local, Utc};
use lazy_static::lazy_static;
use simplicio::*;
use std::io::Write;
use dekor::*;
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.clone();
}
#[derive(Clone)]
pub struct Logger {
pub(crate) path: Option<String>,
pub(crate) terminal_output: bool,
pub(crate) file_output: bool,
pub(crate) output_level: Level,
pub(crate) ignore: Vec<Level>,
pub(crate) file_ignore: Vec<Level>,
pub(crate) terminal_ignore: Vec<Level>,
pub(crate) log_format: String,
pub(crate) timezone: TimeZone,
pub(crate) timestamp_format: String,
pub(crate) styles: std::collections::HashMap<Level, Vec<Style>>,
}
impl Logger {
pub fn new() -> Self {
Self {
path: None,
terminal_output: true,
file_output: false,
output_level: Level::Trace,
ignore: vec![],
file_ignore: vec![],
terminal_ignore: vec![],
log_format: s!("[{timestamp} {level} {module_path}] {message}"),
timezone: TimeZone::Local,
timestamp_format: s!("%Y-%m-%d %H:%M:%S"),
styles: map!(
Level::Trace => vec![Style::FGPurple],
Level::Debug => vec![Style::FGBlue],
Level::Info => vec![Style::FGGreen],
Level::Warning => vec![Style::FGYellow],
Level::Error => vec![Style::FGRed],
Level::Critical => vec![Style::Bold, Style::FGRed],
Level::None => vec![],
),
}
}
pub fn path(&mut self, path: &str) -> Self {
self.path = Some(s!(path));
set_logger(self);
return self.to_owned();
}
pub fn terminal(&mut self, value: bool) -> Self {
self.terminal_output = value;
set_logger(self);
return self.to_owned();
}
pub fn file(&mut self, value: bool) -> Self {
self.file_output = value;
set_logger(self);
return self.to_owned();
}
pub fn level(&mut self, level: Level) -> Self {
self.output_level = level;
set_logger(self);
return self.to_owned();
}
pub fn ignore(&mut self, level: Level) -> Self {
self.ignore.push(level);
set_logger(self);
return self.to_owned();
}
pub fn file_ignore(&mut self, level: Level) -> Self {
self.file_ignore.push(level);
set_logger(self);
return self.to_owned();
}
pub fn terminal_ignore(&mut self, level: Level) -> Self {
self.terminal_ignore.push(level);
set_logger(self);
return self.to_owned();
}
pub fn log_format(&mut self, format: &str) -> Self {
self.log_format = s!(format);
set_logger(&self);
return self.to_owned();
}
pub fn timezone(&mut self, timezone: TimeZone) -> Self {
self.timezone = timezone;
set_logger(&self);
return self.to_owned();
}
pub fn timestamp_format(&mut self, format: &str) -> Self {
self.timestamp_format = s!(format);
set_logger(self);
return self.to_owned();
}
pub fn style(&mut self, level: Level, style_set: Vec<Style>) -> Self {
self.styles.insert(level, style_set);
return self.to_owned();
}
pub fn add_style(&mut self, level: Level, style: Style) -> Self {
let styles = self.styles.get_mut(&level).expect("Magic has occured");
styles.push(style);
set_logger(&self);
return self.to_owned();
}
pub fn remove_style(&mut self, level: Level, style: Style) -> Self {
let styles = self.styles.get_mut(&level).expect("Magic has occured");
styles.retain(|s| *s != style);
set_logger(&self);
return self.to_owned();
}
pub fn styles(&self, level: Level) -> Vec<Style> {
return self.styles.get(&level).expect("Magic has occured").clone();
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Trace = 0,
Debug = 1,
Info = 2,
Warning = 3,
Error = 4,
Critical = 5,
None = 255,
}
impl std::fmt::Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Level::Trace => write!(f,"TRACE"),
Level::Debug => write!(f,"DEBUG"),
Level::Info => write!(f,"INFO"),
Level::Warning => write!(f,"WARNING"),
Level::Error => write!(f,"ERROR"),
Level::Critical => write!(f,"CRITICAL"),
Level::None => write!(f,"NONE"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum TimeZone {
Local,
Utc,
}
pub fn log(level: Level, module_path: &str, args: std::fmt::Arguments) {
let logger = LOGGER.read().expect("Could not read logger").clone();
if level < logger.output_level || logger.ignore.contains(&level) {
return;
}
let message = format!("{}",args);
let time = match logger.timezone {
TimeZone::Local => {
let now = Local::now();
s!(now.format(&logger.timestamp_format))
},
TimeZone::Utc => {
let now = Utc::now();
s!(now.format(&logger.timestamp_format))
},
};
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 && !logger.file_ignore.contains(&level) {
let mut path = logger.path.unwrap();
if path.is_empty() {
path = std::env::current_dir().unwrap().to_str().unwrap().to_string();
path += ".logger";
}
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.read(true)
.append(true)
.open(&path);
let file = match file {
Ok(f) => f,
Err(_e) => { std::fs::File::create(path).expect("Could not create file") },
};
let format = log_format.replace("{level}", &s!(level));
let file_mutex = std::sync::Mutex::new(file);
{
let mut file = file_mutex.lock().unwrap();
match writeln!(file, "{}", format) { _ => () } }
}
if logger.terminal_output && !logger.terminal_ignore.contains(&level) {
let styles = logger.styles.get(&level).unwrap();
let format = log_format.replace(
"{level}", &style(styles.clone(), level)
);
println!("{}", format);
}
}
#[macro_export]
macro_rules! trace {
($($arg:tt)*) => {{
$crate::log($crate::Level::Trace, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! debug {
($($arg:tt)*) => {{
$crate::log($crate::Level::Debug, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! info {
($($arg:tt)*) => {{
$crate::log($crate::Level::Info, module_path!(), format_args!($($arg)*));
}};
}
#[macro_export]
macro_rules! warning {
($($arg:tt)*) => {{
$crate::log($crate::Level::Warning, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! warn {
($($arg:tt)*) => {{
$crate::log($crate::Level::Warning, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! error {
($($arg:tt)*) => {{
$crate::log($crate::Level::Error, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! critical {
($($arg:tt)*) => {{
$crate::log($crate::Level::Critical, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! crit {
($($arg:tt)*) => {{
$crate::log($crate::Level::Critical, module_path!(), format_args!($($arg)*))
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[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_output_enablement() {
let mut logger = Logger::new();
assert!(logger.terminal_output, "Terminal output should be enabled by default");
assert!(!logger.file_output, "File output should be disabled by default");
logger.file(true);
logger.terminal(false);
assert!(logger.file_output, "File output was not enabled");
assert!(!logger.terminal_output, "Terminal output was not disabled");
}
#[test]
fn test_ignore_levels() {
let mut logger = Logger::new();
logger.ignore(Level::Debug);
logger.ignore(Level::Warning);
assert!(logger.ignore.contains(&Level::Debug), "Debug level should be ignored");
assert!(logger.ignore.contains(&Level::Warning), "Warning level should be ignored");
assert!(!logger.ignore.contains(&Level::Error), "Error level should not be ignored");
}
#[test]
fn test_style_assignment() {
let mut logger = Logger::new();
logger.style(Level::Info, vec![Style::FGGreen, Style::Bold]);
let styles = logger.styles(Level::Info);
assert!(styles.contains(&Style::FGGreen) && styles.contains(&Style::Bold), "Info level should have green and bold styles");
}
}