#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
use chrono::{DateTime, Utc};
use lazy_static::lazy_static;
pub use loggr_config::LoggrConfig;
use owo_colors::OwoColorize;
use std::{collections::HashMap, fmt, sync::Mutex};
pub use log_level::LogLevel;
use types::{ArgHookCallback, LogHooks, PostHookCallback, PreHookCallback};
use crate::types::PostHookCallbackParams;
pub mod log_level;
pub mod loggr_config;
pub mod types;
pub struct CatLoggr {
pub level_map: HashMap<String, LogLevel>,
max_length: usize,
timestamp_format: String,
shard: Option<String>,
shard_length: Option<usize>,
levels: Vec<LogLevel>,
hooks: LogHooks,
level_name: Option<String>,
color_enabled: bool,
}
impl Default for CatLoggr {
fn default() -> Self {
Self {
level_map: Default::default(),
max_length: Default::default(),
levels: Default::default(),
timestamp_format: "%d/%m %H:%M:%S".to_string(),
shard: None,
shard_length: None,
hooks: LogHooks::new(),
level_name: None,
color_enabled: true,
}
}
}
fn top<T: Clone>(vec: &mut [T]) -> Option<T> {
vec.last().cloned()
}
impl CatLoggr {
fn get_default_levels() -> Vec<LogLevel> {
#[rustfmt::skip]
let default_levels: Vec<LogLevel> = vec![
LogLevel { name: "fatal".to_string(), style: owo_colors::Style::new().red().on_black(), position: None },
LogLevel { name: "error".to_string(), style: owo_colors::Style::new().black().on_red(), position: None },
LogLevel { name: "warn".to_string(), style: owo_colors::Style::new().black().on_yellow(), position: None },
LogLevel { name: "trace".to_string(), style: owo_colors::Style::new().green().on_black(), position: None },
LogLevel { name: "init".to_string(), style: owo_colors::Style::new().black().on_blue(), position: None },
LogLevel { name: "info".to_string(), style: owo_colors::Style::new().black().on_green(), position: None},
LogLevel { name: "verbose".to_string(), style: owo_colors::Style::new().black().on_cyan(), position: None },
LogLevel { name: "debug".to_string(), style: owo_colors::Style::new().magenta().on_black(), position: None }
];
default_levels
}
#[doc(hidden)]
pub fn add_pre_hook(&mut self, func: PreHookCallback) -> &mut Self {
self.hooks.pre.push(func);
self
}
#[doc(hidden)]
pub fn add_arg_hook(&mut self, func: ArgHookCallback) -> &mut Self {
self.hooks.arg.push(func);
self
}
pub fn add_post_hook(&mut self, func: PostHookCallback) -> &mut Self {
self.hooks.post.push(func);
self
}
pub fn config(&mut self, options: Option<LoggrConfig>) -> &mut Self {
let options = options.unwrap_or_default();
if options.timestamp_format.is_some() {
self.timestamp_format = options.timestamp_format.unwrap();
}
if options.shard.is_some() {
self.shard = options.shard;
}
if options.shard_length.is_some() {
self.shard_length = options.shard_length;
}
if options.levels.is_some() {
self.set_levels(options.levels.unwrap());
} else {
self.set_levels(Self::get_default_levels());
}
if options.level.is_some() {
self.level_name = options.level;
} else {
self.level_name = Some(top::<LogLevel>(&mut self.levels).unwrap().name);
}
self.color_enabled = options.color_enabled;
self
}
pub fn new(options: Option<LoggrConfig>) -> Self {
let mut logger = Self::default();
logger.config(options);
logger
}
#[doc(hidden)]
fn get_timestamp(&self, time: Option<DateTime<Utc>>) -> String {
let now: DateTime<Utc> = time.unwrap_or_else(Utc::now);
let format_string = &self.timestamp_format;
let formatted = now.format(format_string);
formatted.to_string()
}
pub fn set_levels(&mut self, levels: Vec<LogLevel>) -> &mut Self {
self.level_map.clear();
self.levels = levels;
let mut max = 0;
for (position, level) in self.levels.iter_mut().enumerate() {
level.position = Some(position);
max = if level.name.len() > max {
level.name.len()
} else {
max
};
if !self.level_map.contains_key(&level.name) {
self.level_map.insert(level.name.clone(), level.clone());
}
}
self.max_length = max + 2;
self
}
pub fn set_level(&mut self, level: &str) -> &mut Self {
if !self.level_map.contains_key(level) {
panic!("The level `{}` doesn't exist.", level);
}
self.level_name = Some(level.to_string());
self
}
fn centre_pad(text: &String, length: usize) -> String {
if text.len() < length {
let before_count = ((length - text.len()) as f32 / 2_f32).floor() as usize;
let after_count = ((length - text.len()) as f32 / 2_f32).ceil() as usize;
format!(
"{}{}{}",
" ".repeat(before_count),
text,
" ".repeat(after_count)
)
} else {
text.to_string()
}
}
#[doc(hidden)]
pub fn __write(&self, args: fmt::Arguments, level: &str) {
self.log(format!("{}", args).as_str(), level);
}
#[doc(hidden)]
fn get_level(&self) -> &LogLevel {
self.level_map
.get(&self.level_name.clone().unwrap())
.unwrap()
}
pub fn log(&self, text: &str, level: &str) -> &Self {
if !self.level_map.contains_key(level) {
panic!("The level `{}` level doesn't exist.", level);
}
let current_log_level = self.get_level();
let log_level = self.level_map.get(level).unwrap();
if log_level.position.unwrap() > current_log_level.position.unwrap() {
return self;
}
let shard_text = if self.shard.is_some() {
CatLoggr::centre_pad(&self.shard.clone().unwrap(), self.shard_length.unwrap())
} else {
"".to_string()
};
let formatted_shard_text = if self.color_enabled {
shard_text.black().on_yellow().to_string()
} else {
shard_text
};
let centered_str = CatLoggr::centre_pad(&log_level.name, self.max_length);
let level_str = if self.color_enabled {
centered_str.style(log_level.style).to_string()
} else {
centered_str
};
let now = Utc::now();
let timestamp = self.get_timestamp(Some(now));
let formatted_timestamp = if self.color_enabled {
timestamp.black().on_white().to_string()
} else {
timestamp.clone()
};
let mut final_text: String = text.to_string();
for hook in self.hooks.post.iter() {
let res = hook(PostHookCallbackParams {
text: text.to_string(),
date: now,
timestamp: timestamp.clone(),
level: level.to_string(),
shard: self.shard.clone(),
});
if let Some(response) = res {
final_text = response
}
}
let final_string = format!(
"{}{}{} {}",
formatted_shard_text, formatted_timestamp, level_str, final_text
);
println!("{}", final_string);
self
}
}
#[cfg(feature = "macros")]
lazy_static! {
#[cfg(feature = "macros")]
pub static ref CAT_LOGGR: Mutex<CatLoggr> = Mutex::new(CatLoggr::new(None));
}
#[cfg(feature = "macros")]
mod macros {
#[macro_export]
#[cfg(feature = "macros")]
macro_rules! log {
(target: $target:expr, $lvl:expr, $($key:tt = $value:expr),+; $($arg:tt)+) => ({
cat_loggr::CAT_LOGGR.write(
format_args!($($args)*),
$lvl,
)
});
(target: $target:expr, $lvl:expr, $($arg:tt)+) => ({
cat_loggr::CAT_LOGGR.lock().unwrap().__write(
format_args!($($arg)*),
$lvl,
);
});
($lvl:expr, $($arg:tt)+) => ($crate::log!(target: "", $lvl, $($arg)+));
}
#[macro_export]
macro_rules! log_fatal {
(target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "fatal", $($arg)+));
($($arg:tt)+) => ($crate::log!("fatal", $($arg)+))
}
#[macro_export]
macro_rules! log_error {
(target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "error", $($arg)+));
($($arg:tt)+) => ($crate::log!("error", $($arg)+))
}
#[macro_export]
macro_rules! log_warn {
(target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "warn", $($arg)+));
($($arg:tt)+) => ($crate::log!("warn", $($arg)+))
}
#[macro_export]
macro_rules! log_trace {
(target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "trace", $($arg)+));
($($arg:tt)+) => ($crate::log!("trace", $($arg)+))
}
#[macro_export]
macro_rules! log_init {
(target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "init", $($arg)+));
($($arg:tt)+) => ($crate::log!("init", $($arg)+))
}
#[macro_export]
macro_rules! log_info {
(target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "info", $($arg)+));
($($arg:tt)+) => ($crate::log!("info", $($arg)+))
}
#[macro_export]
macro_rules! log_verbose {
(target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "verbose", $($arg)+));
($($arg:tt)+) => ($crate::log!("verbose", $($arg)+))
}
#[macro_export]
macro_rules! log_debug {
(target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "debug", $($arg)+));
($($arg:tt)+) => ($crate::log!("debug", $($arg)+))
}
}
#[cfg(test)]
mod test {
use crate::CatLoggr;
use crate::LogLevel;
mod should_instantiate {
use crate::CatLoggr;
use crate::LogLevel;
use crate::LoggrConfig;
#[test]
fn should_instantiate_with_none_opts() {
let loggr = CatLoggr::new(None);
assert_ne!(loggr.level_map.len(), 0, "Loggr not made")
}
#[test]
fn should_instantiate_with_shard_id() {
let loggr = CatLoggr::new(Some(LoggrConfig {
shard: Some("shard-id".to_string()),
..Default::default()
}));
assert_eq!(loggr.shard, Some("shard-id".to_string()))
}
#[test]
fn should_instantiate_with_default_level() {
let loggr = CatLoggr::new(Some(LoggrConfig {
level: Some("fatal".to_string()),
..Default::default()
}));
assert_eq!(loggr.level_name, Some("fatal".to_string()))
}
#[test]
fn should_instantiate_with_default_level_definitions() {
let loggr = CatLoggr::new(Some(LoggrConfig {
levels: Some(vec![
LogLevel {
name: "catnip".to_string(),
style: owo_colors::Style::new().red().on_black(),
position: None,
},
LogLevel {
name: "fish".to_string(),
style: owo_colors::Style::new().black().on_red(),
position: None,
},
]),
..Default::default()
}));
assert_eq!(loggr.levels.len(), 2);
assert_eq!(loggr.level_name, Some("fish".to_string()));
}
}
mod set_level {
use crate::CatLoggr;
#[test]
#[should_panic(expected = "The level `catnip` doesn't exist.")]
fn should_panic_if_level_doesnt_exist() {
let mut loggr = CatLoggr::new(None);
loggr.set_level("catnip");
assert_eq!(loggr.levels.len(), 2);
assert_eq!(loggr.level_name, Some("fish".to_string()));
}
}
#[test]
fn should_chain_properly() {
let mut loggr = CatLoggr::new(None);
loggr
.set_levels(vec![
LogLevel {
name: "catnip".to_string(),
style: owo_colors::Style::new().red().on_black(),
position: None,
},
LogLevel {
name: "fish".to_string(),
style: owo_colors::Style::new().black().on_red(),
position: None,
},
])
.set_level("catnip");
assert_eq!(&loggr.level_name.unwrap(), "catnip");
}
}