use crate::error::*;
use chrono::{prelude::Local, Utc};
use dekor::*;
use lazy_static::lazy_static;
use simplicio::*;
use std::{io::Write, path::PathBuf};
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, Debug)]
pub struct Logger {
pub(crate) path: Option<PathBuf>,
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 Default for Logger {
fn default() -> Self {
return Self::new();
}
}
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::Diagnostic => vec![Style::Bold, Style::FGCyan],
Level::None => vec![],
),
}
}
pub fn path(&mut self, path: &str) -> Self {
self.path = Some(PathBuf::from(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,
Diagnostic = 245,
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::Diagnostic => write!(f, "DIAGNOSTIC"),
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.file_output && !logger.file_ignore.contains(&level) {
if let Some(mut path) = logger.path {
if path.as_os_str().is_empty() {
path = std::env::current_dir()
.unwrap_or_else(|_| panic!(
"{}\n{}",
"ERROR: No path given. Attempted to write to current directory.",
" FAILURE: insufficient permissions or the current directory does not exist."
));
path.push(".logger");
}
if let Some(parent) = PathBuf::from(&path).parent() {
std::fs::create_dir_all(parent).expect("failed to create missing sub-directories");
}
let file = std::fs::OpenOptions::new()
.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();
_ = 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);
}
}
pub fn result_log(level: Level, mod_path: &str, args: std::fmt::Arguments) -> LogfatherResult {
let logger = LOGGER.read().map_err(LogfatherError::from)?.clone();
if level < logger.output_level || logger.ignore.contains(&level) {
return Ok(());
}
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}", mod_path)
.replace("{message}", &message);
if logger.file_output && !logger.file_ignore.contains(&level) {
if let Some(mut path) = logger.path {
if path.as_os_str().is_empty() {
path = std::env::current_dir().map_err(LogfatherError::from)?;
path.push(".logger");
}
if let Some(parent) = PathBuf::from(&path).parent() {
std::fs::create_dir_all(parent).map_err(LogfatherError::from)?;
}
let file = std::fs::OpenOptions::new()
.create(true)
.read(true)
.append(true)
.open(&path)
.map_err(LogfatherError::from)?;
let format = log_format.replace("{level}", &s!(level));
let file_mutex = std::sync::Mutex::new(file);
{
let mut file = file_mutex.lock().map_err(LogfatherError::from)?;
writeln!(file, "{}", format).map_err(LogfatherError::from)?;
}
}
}
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);
}
return Ok(());
}
#[macro_export]
macro_rules! trace {
($($arg:tt)*) => {{
$crate::log($crate::Level::Trace, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! debug {
($($arg:tt)*) => {
#[cfg(debug_assertions)]
{
$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)*))
}};
}
#[macro_export]
macro_rules! diagnostic {
($($arg:tt)*) => {
#[cfg(debug_assertions)]
{
$crate::log($crate::Level::Diagnostic, module_path!(), format_args!($($arg)*))
}
};
}
#[macro_export]
macro_rules! diag {
($($arg:tt)*) => {
#[cfg(debug_assertions)]
{
$crate::log($crate::Level::Diagnostic, module_path!(), format_args!($($arg)*))
}
};
}
#[macro_export]
macro_rules! r_trace {
($($arg:tt)*) => {{
$crate::result_log($crate::Level::Trace, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! r_debug {
($($arg:tt)*) => {{
#[cfg(debug_assertions)]
{
$crate::result_log($crate::Level::Debug, module_path!(), format_args!($($arg)*))
}
#[cfg(not(debug_assertions))]
{
Ok::<(), LogfatherError>(())
}
}};
}
#[macro_export]
macro_rules! r_info {
($($arg:tt)*) => {{
$crate::result_log($crate::Level::Info, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! r_warning {
($($arg:tt)*) => {{
$crate::result_log($crate::Level::Warning, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! r_warn {
($($arg:tt)*) => {{
$crate::result_log($crate::Level::Warning, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! r_error {
($($arg:tt)*) => {{
$crate::result_log($crate::Level::Error, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! r_critical {
($($arg:tt)*) => {{
$crate::result_log($crate::Level::Critical, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! r_crit {
($($arg:tt)*) => {{
$crate::result_log($crate::Level::Critical, module_path!(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! r_diagnostic {
($($arg:tt)*) => {{
#[cfg(debug_assertions)]
{
$crate::result_log($crate::Level::Diagnostic, module_path!(), format_args!($($arg)*))
}
#[cfg(not(debug_assertions))]
{
Ok::<(), LogfatherError>(())
}
}};
}
#[macro_export]
macro_rules! r_diag {
($($arg:tt)*) => {{
#[cfg(debug_assertions)]
{
$crate::result_log($crate::Level::Diagnostic, module_path!(), format_args!($($arg)*))
}
#[cfg(not(debug_assertions))]
{
Ok::<(), LogfatherError>(())
}
}};
}
#[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"
);
}
}