cat_loggr/
lib.rs

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	/// Adds a post hook, ran after formatting, but before final output
89	/// The string returned by the hook will be the text after the level name.
90	///
91	/// # Arguments
92	/// * `func` - The function to run
93	///
94	/// # Examples
95	///
96	/// ```rust
97	/// use cat_loggr::CatLoggr;
98	/// let mut logger = CatLoggr::new(None);
99	///
100	/// logger.add_post_hook(|params| -> Option<String> {
101	///     let string: String = "New log".to_string();
102	///     Some(string)
103	/// });
104	/// ```
105	pub fn add_post_hook(&mut self, func: PostHookCallback) -> &mut Self {
106		self.hooks.post.push(func);
107
108		self
109	}
110
111	/// Configures the logger
112	///
113	/// # Arguments
114	/// * `options` - The options to configure with
115	///
116	/// # Examples
117	///
118	/// ```rust
119	/// // Configuring the levels
120	///
121	/// use cat_loggr::{CatLoggr, LogLevel, LoggrConfig};
122	/// let mut logger = CatLoggr::new(None);
123	///
124	/// logger.config(Some(LoggrConfig {
125	///     levels: Some(vec![
126	///         LogLevel   { name: "fatal".to_string(), style: owo_colors::Style::new().red().on_black(), position: None },
127	///         LogLevel   { name: "info".to_string(), style: owo_colors::Style::new().red().on_black(), position: None }
128	///     ]),
129	///     color_enabled: false,
130	///     ..LoggrConfig::default()
131	/// }));
132	/// ```
133	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	/// Creates an instance of the logger
166	///
167	/// # Arguments
168	/// * `options` - The options to instantiate with
169	///
170	/// # Examples
171	///
172	/// ```rust
173	/// // Create with levels and shard ID
174	///
175	/// use cat_loggr::{CatLoggr, LoggrConfig, log_level::LogLevel};
176	/// let logger = CatLoggr::new(Some(LoggrConfig {
177	///     levels: Some(vec![
178	///         LogLevel   { name: "fatal".to_string(), style: owo_colors::Style::new().red().on_black(), position: None },
179	///         LogLevel   { name: "info".to_string(), style: owo_colors::Style::new().red().on_black(), position: None }
180	///     ]),
181	///     shard: Some("123".to_string()),
182	///     shard_length: Some(4),
183	///     ..LoggrConfig::default()
184	/// }));
185	/// ```
186	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	/// Overwrites the currently set levels with a custom set
205	///
206	/// # Arguments
207	///
208	/// * `levels` - New levels to override with, in order from high to low priority
209	///
210	/// # Examples
211	///
212	/// ```
213	/// use cat_loggr::{CatLoggr, LogLevel};
214	///
215	/// let mut logger = CatLoggr::new(None);
216	///
217	/// logger.set_levels(vec![
218	///     LogLevel   { name: "fatal".to_string(), style: owo_colors::Style::new().red().on_black(), position: None },
219	///     LogLevel   { name: "info".to_string(), style: owo_colors::Style::new().red().on_black(), position: None }
220	/// ]);
221	/// ```
222	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	/// Sets the level threshold. Only logs on and above the threshold will be output
248	///
249	/// # Arguments
250	/// * `level` - The name of the level threshold
251	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	/// Center aligns text
262	///
263	/// # Arguments
264	///
265	/// * `text` - The text to align
266	/// * `length` - The length that it should be padded to
267	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	/// Internal function that writes the log by taking [`fmt::Arguments`]
285	///
286	/// # Arguments
287	/// * `args` - The text to write
288	/// * `level` - The level to write at
289	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	/// Writes the log
301	///
302	/// # Arguments
303	/// * `text` - The text to write
304	/// * `level` - The level to write at
305	///
306	/// # Examples
307	///
308	/// ```rust
309	/// // Log to the `info` level
310	///
311	/// use cat_loggr::CatLoggr;
312	/// let logger = CatLoggr::new(None);
313	///
314	/// logger.log("This is a log", "info");
315	/// ```
316	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	/// Logs something to the console with a specified level, using the default logger.
391	///
392	///
393	/// # Example
394	///
395	/// ```rust
396	/// use cat_loggr::log;
397	///
398	/// log!("info", "Default log");
399	///
400	/// let data = vec!["a", "b", "c"];
401	///
402	/// log!("info", "Default log {:#?}", data);
403	/// ```
404	///
405	///
406	#[macro_export]
407	#[cfg(feature = "macros")]
408	macro_rules! log {
409		// log!(target: "my_target", Level::Info; key1 = 42, key2 = true; "a {} event", "log");
410		(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		// log!(target: "my_target", Level::Info; "a {} event", "log");
418		(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	/// Logs something to the console with the default fatal level, using the default logger.
430	///
431	/// # Example
432	///
433	/// ```rust
434	/// use cat_loggr::log_fatal;
435	///
436	/// log_fatal!("Default log");
437	///
438	/// let data = vec!["a", "b", "c"];
439	///
440	/// log_fatal!("{:#?}", data);
441	/// ```
442	#[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	/// Logs something to the console with the default error level, using the default logger.
449	///
450	/// # Example
451	///
452	/// ```rust
453	/// use cat_loggr::log_error;
454	///
455	/// log_error!("Default log");
456	///
457	/// let data = vec!["a", "b", "c"];
458	///
459	/// log_error!("{:#?}", data);
460	/// ```
461	#[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	/// Logs something to the console with the default warn level, using the default logger.
468	///
469	/// # Example
470	///
471	/// ```rust
472	/// use cat_loggr::log_warn;
473	///
474	/// log_warn!("Default log");
475	///
476	/// let data = vec!["a", "b", "c"];
477	///
478	/// log_warn!("{:#?}", data);
479	/// ```
480	#[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	/// Logs something to the console with the default trace level, using the default logger.
487	///
488	/// # Example
489	///
490	/// ```rust
491	/// use cat_loggr::log_trace;
492	///
493	/// log_trace!("Default log");
494	///
495	/// let data = vec!["a", "b", "c"];
496	///
497	/// log_trace!("{:#?}", data);
498	/// ```
499	#[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	/// Logs something to the console with the default init level, using the default logger.
506	///
507	/// # Example
508	///
509	/// ```rust
510	/// use cat_loggr::log_init;
511	///
512	/// log_init!("Default log");
513	///
514	/// let data = vec!["a", "b", "c"];
515	///
516	/// log_init!("{:#?}", data);
517	/// ```
518	#[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	/// Logs something to the console with the default info level, using the default logger.
525	///
526	/// # Example
527	///
528	/// ```rust
529	/// use cat_loggr::log_info;
530	///
531	/// log_info!("Default log");
532	///
533	/// let data = vec!["a", "b", "c"];
534	///
535	/// log_info!("{:#?}", data);
536	/// ```
537	#[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	/// Logs something to the console with the default verbose level, using the default logger.
544	///
545	/// # Example
546	///
547	/// ```rust
548	/// use cat_loggr::log_verbose;
549	///
550	/// log_verbose!("Default log");
551	///
552	/// let data = vec!["a", "b", "c"];
553	///
554	/// log_verbose!("{:#?}", data);
555	/// ```
556	#[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	/// Logs something to the console with the default debug level, using the default logger.
563	///
564	/// # Example
565	///
566	/// ```rust
567	/// use cat_loggr::log_debug;
568	///
569	/// log_debug!("Default log");
570	///
571	/// let data = vec!["a", "b", "c"];
572	///
573	/// log_debug!("{:#?}", data);
574	/// ```
575	#[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	// TODO: This test. Currently disabled because waiting for support for changing stdout
681	// #[test]
682	// fn should_execute_post_hooks() {
683	// 	let mut loggr = CatLoggr::new(None);
684
685	// 	loggr.add_post_hook(|params| {
686	// 		Some("new text".to_string())
687	// 	});
688
689	// 	loggr.log("a b c", "info");
690
691	// }
692}