1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![doc = include_str!("../README.md")]
3
4use chrono::{DateTime, Utc};
5use lazy_static::lazy_static;
6pub use loggr_config::LoggrConfig;
7use owo_colors::OwoColorize;
8use std::{collections::HashMap, fmt, sync::Mutex};
9
10pub use log_level::LogLevel;
11use types::{ArgHookCallback, LogHooks, PostHookCallback, PreHookCallback};
12
13use crate::types::PostHookCallbackParams;
14
15pub mod log_level;
16pub mod loggr_config;
17pub mod types;
18
19pub struct CatLoggr {
20 pub level_map: HashMap<String, LogLevel>,
21
22 max_length: usize,
23
24 timestamp_format: String,
25 shard: Option<String>,
26 shard_length: Option<usize>,
27
28 levels: Vec<LogLevel>,
29
30 hooks: LogHooks,
31
32 level_name: Option<String>,
33
34 color_enabled: bool,
35}
36
37impl Default for CatLoggr {
38 fn default() -> Self {
39 Self {
40 level_map: Default::default(),
41 max_length: Default::default(),
42 levels: Default::default(),
43 timestamp_format: "%d/%m %H:%M:%S".to_string(),
44 shard: None,
45 shard_length: None,
46 hooks: LogHooks::new(),
47 level_name: None,
48 color_enabled: true,
49 }
50 }
51}
52
53fn top<T: Clone>(vec: &mut [T]) -> Option<T> {
54 vec.last().cloned()
55}
56
57impl CatLoggr {
58 fn get_default_levels() -> Vec<LogLevel> {
59 #[rustfmt::skip]
60 let default_levels: Vec<LogLevel> = vec![
61 LogLevel { name: "fatal".to_string(), style: owo_colors::Style::new().red().on_black(), position: None },
62 LogLevel { name: "error".to_string(), style: owo_colors::Style::new().black().on_red(), position: None },
63 LogLevel { name: "warn".to_string(), style: owo_colors::Style::new().black().on_yellow(), position: None },
64 LogLevel { name: "trace".to_string(), style: owo_colors::Style::new().green().on_black(), position: None },
65 LogLevel { name: "init".to_string(), style: owo_colors::Style::new().black().on_blue(), position: None },
66 LogLevel { name: "info".to_string(), style: owo_colors::Style::new().black().on_green(), position: None},
67 LogLevel { name: "verbose".to_string(), style: owo_colors::Style::new().black().on_cyan(), position: None },
68 LogLevel { name: "debug".to_string(), style: owo_colors::Style::new().magenta().on_black(), position: None }
69 ];
70
71 default_levels
72 }
73
74 #[doc(hidden)]
75 pub fn add_pre_hook(&mut self, func: PreHookCallback) -> &mut Self {
76 self.hooks.pre.push(func);
77
78 self
79 }
80
81 #[doc(hidden)]
82 pub fn add_arg_hook(&mut self, func: ArgHookCallback) -> &mut Self {
83 self.hooks.arg.push(func);
84
85 self
86 }
87
88 pub fn add_post_hook(&mut self, func: PostHookCallback) -> &mut Self {
106 self.hooks.post.push(func);
107
108 self
109 }
110
111 pub fn config(&mut self, options: Option<LoggrConfig>) -> &mut Self {
134 let options = options.unwrap_or_default();
135
136 if options.timestamp_format.is_some() {
137 self.timestamp_format = options.timestamp_format.unwrap();
138 }
139
140 if options.shard.is_some() {
141 self.shard = options.shard;
142 }
143
144 if options.shard_length.is_some() {
145 self.shard_length = options.shard_length;
146 }
147
148 if options.levels.is_some() {
149 self.set_levels(options.levels.unwrap());
150 } else {
151 self.set_levels(Self::get_default_levels());
152 }
153
154 if options.level.is_some() {
155 self.level_name = options.level;
156 } else {
157 self.level_name = Some(top::<LogLevel>(&mut self.levels).unwrap().name);
158 }
159
160 self.color_enabled = options.color_enabled;
161
162 self
163 }
164
165 pub fn new(options: Option<LoggrConfig>) -> Self {
187 let mut logger = Self::default();
188 logger.config(options);
189
190 logger
191 }
192
193 #[doc(hidden)]
194 fn get_timestamp(&self, time: Option<DateTime<Utc>>) -> String {
195 let now: DateTime<Utc> = time.unwrap_or_else(Utc::now);
196
197 let format_string = &self.timestamp_format;
198
199 let formatted = now.format(format_string);
200
201 formatted.to_string()
202 }
203
204 pub fn set_levels(&mut self, levels: Vec<LogLevel>) -> &mut Self {
223 self.level_map.clear();
224 self.levels = levels;
225
226 let mut max = 0;
227
228 for (position, level) in self.levels.iter_mut().enumerate() {
229 level.position = Some(position);
230
231 max = if level.name.len() > max {
232 level.name.len()
233 } else {
234 max
235 };
236
237 if !self.level_map.contains_key(&level.name) {
238 self.level_map.insert(level.name.clone(), level.clone());
239 }
240 }
241
242 self.max_length = max + 2;
243
244 self
245 }
246
247 pub fn set_level(&mut self, level: &str) -> &mut Self {
252 if !self.level_map.contains_key(level) {
253 panic!("The level `{}` doesn't exist.", level);
254 }
255
256 self.level_name = Some(level.to_string());
257
258 self
259 }
260
261 fn centre_pad(text: &String, length: usize) -> String {
268 if text.len() < length {
269 let before_count = ((length - text.len()) as f32 / 2_f32).floor() as usize;
270 let after_count = ((length - text.len()) as f32 / 2_f32).ceil() as usize;
271
272 format!(
273 "{}{}{}",
274 " ".repeat(before_count),
275 text,
276 " ".repeat(after_count)
277 )
278 } else {
279 text.to_string()
280 }
281 }
282
283 #[doc(hidden)]
284 pub fn __write(&self, args: fmt::Arguments, level: &str) {
290 self.log(format!("{}", args).as_str(), level);
291 }
292
293 #[doc(hidden)]
294 fn get_level(&self) -> &LogLevel {
295 self.level_map
296 .get(&self.level_name.clone().unwrap())
297 .unwrap()
298 }
299
300 pub fn log(&self, text: &str, level: &str) -> &Self {
317 if !self.level_map.contains_key(level) {
318 panic!("The level `{}` level doesn't exist.", level);
319 }
320
321 let current_log_level = self.get_level();
322 let log_level = self.level_map.get(level).unwrap();
323
324 if log_level.position.unwrap() > current_log_level.position.unwrap() {
325 return self;
326 }
327
328 let shard_text = if self.shard.is_some() {
329 CatLoggr::centre_pad(&self.shard.clone().unwrap(), self.shard_length.unwrap())
330 } else {
331 "".to_string()
332 };
333
334 let formatted_shard_text = if self.color_enabled {
335 shard_text.black().on_yellow().to_string()
336 } else {
337 shard_text
338 };
339 let centered_str = CatLoggr::centre_pad(&log_level.name, self.max_length);
340 let level_str = if self.color_enabled {
341 centered_str.style(log_level.style).to_string()
342 } else {
343 centered_str
344 };
345
346 let now = Utc::now();
347
348 let timestamp = self.get_timestamp(Some(now));
349 let formatted_timestamp = if self.color_enabled {
350 timestamp.black().on_white().to_string()
351 } else {
352 timestamp.clone()
353 };
354
355 let mut final_text: String = text.to_string();
356
357 for hook in self.hooks.post.iter() {
358 let res = hook(PostHookCallbackParams {
359 text: text.to_string(),
360 date: now,
361 timestamp: timestamp.clone(),
362 level: level.to_string(),
363 shard: self.shard.clone(),
364 });
365
366 if let Some(response) = res {
367 final_text = response
368 }
369 }
370
371 let final_string = format!(
372 "{}{}{} {}",
373 formatted_shard_text, formatted_timestamp, level_str, final_text
374 );
375
376 println!("{}", final_string);
377
378 self
379 }
380}
381
382#[cfg(feature = "macros")]
383lazy_static! {
384 #[cfg(feature = "macros")]
385 pub static ref CAT_LOGGR: Mutex<CatLoggr> = Mutex::new(CatLoggr::new(None));
386}
387
388#[cfg(feature = "macros")]
389mod macros {
390 #[macro_export]
407 #[cfg(feature = "macros")]
408 macro_rules! log {
409 (target: $target:expr, $lvl:expr, $($key:tt = $value:expr),+; $($arg:tt)+) => ({
411 cat_loggr::CAT_LOGGR.write(
412 format_args!($($args)*),
413 $lvl,
414 )
415 });
416
417 (target: $target:expr, $lvl:expr, $($arg:tt)+) => ({
419 cat_loggr::CAT_LOGGR.lock().unwrap().__write(
420 format_args!($($arg)*),
421 $lvl,
422 );
423 });
424
425 ($lvl:expr, $($arg:tt)+) => ($crate::log!(target: "", $lvl, $($arg)+));
426
427 }
428
429 #[macro_export]
443 macro_rules! log_fatal {
444 (target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "fatal", $($arg)+));
445 ($($arg:tt)+) => ($crate::log!("fatal", $($arg)+))
446 }
447
448 #[macro_export]
462 macro_rules! log_error {
463 (target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "error", $($arg)+));
464 ($($arg:tt)+) => ($crate::log!("error", $($arg)+))
465 }
466
467 #[macro_export]
481 macro_rules! log_warn {
482 (target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "warn", $($arg)+));
483 ($($arg:tt)+) => ($crate::log!("warn", $($arg)+))
484 }
485
486 #[macro_export]
500 macro_rules! log_trace {
501 (target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "trace", $($arg)+));
502 ($($arg:tt)+) => ($crate::log!("trace", $($arg)+))
503 }
504
505 #[macro_export]
519 macro_rules! log_init {
520 (target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "init", $($arg)+));
521 ($($arg:tt)+) => ($crate::log!("init", $($arg)+))
522 }
523
524 #[macro_export]
538 macro_rules! log_info {
539 (target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "info", $($arg)+));
540 ($($arg:tt)+) => ($crate::log!("info", $($arg)+))
541 }
542
543 #[macro_export]
557 macro_rules! log_verbose {
558 (target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "verbose", $($arg)+));
559 ($($arg:tt)+) => ($crate::log!("verbose", $($arg)+))
560 }
561
562 #[macro_export]
576 macro_rules! log_debug {
577 (target: $target:expr, $($arg:tt)+) => ($crate::log!(target: $target, "debug", $($arg)+));
578 ($($arg:tt)+) => ($crate::log!("debug", $($arg)+))
579 }
580}
581
582#[cfg(test)]
583mod test {
584
585 use crate::CatLoggr;
586 use crate::LogLevel;
587
588 mod should_instantiate {
589 use crate::CatLoggr;
590 use crate::LogLevel;
591 use crate::LoggrConfig;
592
593 #[test]
594 fn should_instantiate_with_none_opts() {
595 let loggr = CatLoggr::new(None);
596
597 assert_ne!(loggr.level_map.len(), 0, "Loggr not made")
598 }
599
600 #[test]
601 fn should_instantiate_with_shard_id() {
602 let loggr = CatLoggr::new(Some(LoggrConfig {
603 shard: Some("shard-id".to_string()),
604 ..Default::default()
605 }));
606
607 assert_eq!(loggr.shard, Some("shard-id".to_string()))
608 }
609
610 #[test]
611 fn should_instantiate_with_default_level() {
612 let loggr = CatLoggr::new(Some(LoggrConfig {
613 level: Some("fatal".to_string()),
614 ..Default::default()
615 }));
616
617 assert_eq!(loggr.level_name, Some("fatal".to_string()))
618 }
619
620 #[test]
621 fn should_instantiate_with_default_level_definitions() {
622 let loggr = CatLoggr::new(Some(LoggrConfig {
623 levels: Some(vec![
624 LogLevel {
625 name: "catnip".to_string(),
626 style: owo_colors::Style::new().red().on_black(),
627 position: None,
628 },
629 LogLevel {
630 name: "fish".to_string(),
631 style: owo_colors::Style::new().black().on_red(),
632 position: None,
633 },
634 ]),
635 ..Default::default()
636 }));
637
638 assert_eq!(loggr.levels.len(), 2);
639 assert_eq!(loggr.level_name, Some("fish".to_string()));
640 }
641 }
642
643 mod set_level {
644 use crate::CatLoggr;
645
646 #[test]
647 #[should_panic(expected = "The level `catnip` doesn't exist.")]
648 fn should_panic_if_level_doesnt_exist() {
649 let mut loggr = CatLoggr::new(None);
650
651 loggr.set_level("catnip");
652
653 assert_eq!(loggr.levels.len(), 2);
654 assert_eq!(loggr.level_name, Some("fish".to_string()));
655 }
656 }
657
658 #[test]
659 fn should_chain_properly() {
660 let mut loggr = CatLoggr::new(None);
661
662 loggr
663 .set_levels(vec![
664 LogLevel {
665 name: "catnip".to_string(),
666 style: owo_colors::Style::new().red().on_black(),
667 position: None,
668 },
669 LogLevel {
670 name: "fish".to_string(),
671 style: owo_colors::Style::new().black().on_red(),
672 position: None,
673 },
674 ])
675 .set_level("catnip");
676
677 assert_eq!(&loggr.level_name.unwrap(), "catnip");
678 }
679
680 }