colourful_logger/lib.rs
1#![crate_type = "lib"]
2
3use colored::Colorize;
4use pad::{PadStr, Alignment};
5use chrono::prelude::*;
6use serde::Serialize;
7use serde_json::to_string;
8use std::io::Write;
9use std::fs::OpenOptions;
10use backtrace::Backtrace;
11use regex::Regex;
12use std::env;
13
14#[derive(Clone, Copy)]
15pub enum LogLevel {
16 Fatal = 0,
17 Error = 1,
18 Warn = 2,
19 Info = 3,
20 Debug = 4,
21 Silly = 5
22}
23
24pub struct Logger {
25 log_level: LogLevel,
26 log_file: String,
27}
28
29struct Connectors {
30 single_line: &'static str,
31 start_line: &'static str,
32 line: &'static str,
33 end_line: &'static str,
34}
35
36impl Default for Connectors {
37 fn default() -> Self {
38 Connectors {
39 single_line: "▪",
40 start_line: "┏",
41 line: "┃",
42 end_line: "┗",
43 }
44 }
45}
46
47impl Default for Logger {
48 fn default() -> Self {
49 dotenvy::dotenv().unwrap_or_default();
50
51 let log_level = env::var("LOG_LEVEL")
52 .unwrap_or_else(|_| "info".to_string())
53 .to_lowercase();
54
55 let log_level = match log_level.as_str() {
56 "silly" | "5" => LogLevel::Silly,
57 "debug" | "4" => LogLevel::Debug,
58 "info" | "3" => LogLevel::Info,
59 "warn" | "2" => LogLevel::Warn,
60 "error" | "1" => LogLevel::Error,
61 "fatal" | "0" => LogLevel::Fatal,
62 _ => LogLevel::Info,
63 };
64
65 Self { log_level: log_level, log_file: String::from("") }
66 }
67}
68
69impl Logger {
70 pub fn new(log_level: LogLevel, log_file: Option<&str>) -> Self {
71 Logger {
72 log_level: log_level,
73 log_file: log_file.unwrap_or("").to_string()
74 }
75 }
76
77 /*
78 @brief Grabs the correlating tag.
79
80 Bring back a padded tag, depending on the logLevel provided
81 by the user.
82
83 @param LogLevel to get tag from.
84
85 @return String
86 */
87 fn get_tag(&self, level: &LogLevel) -> String {
88 match level {
89 LogLevel::Silly => format!("{}", "silly:".pad_to_width_with_alignment(6, Alignment::Left).bright_magenta()),
90 LogLevel::Debug => format!("{}", "debug:".pad_to_width_with_alignment(6, Alignment::Left).bright_blue()),
91 LogLevel::Info => format!("{}", "info:".pad_to_width_with_alignment(6, Alignment::Left).bright_green()),
92 LogLevel::Warn => format!("{}", "warn:".pad_to_width_with_alignment(6, Alignment::Left).bright_yellow()),
93 LogLevel::Error => format!("{}", "error:".pad_to_width_with_alignment(6, Alignment::Left).bright_red()),
94 LogLevel::Fatal => format!("{}", "fatal:".pad_to_width_with_alignment(6, Alignment::Left).red()),
95 }
96 }
97
98 /*
99 @brief Captures the current timestamp and returns it.
100
101 Returns a timestamp of when the log was called.
102 Formatted for better use.
103
104 @return String
105 */
106 fn timestamp(&self) -> String {
107 let now: DateTime<Local> = Local::now();
108 let time_format = now.format("[%Y-%m-%d %H:%M:%S]").to_string();
109 return time_format.dimmed().to_string()
110 }
111
112 /*
113 @brief Select colour based on LogLevel
114
115 Returns the correct colour based on the LogLevel set by the user.
116
117 @param LogLevel to get colour from.
118
119 @return ColouredString
120 */
121 fn get_colour(&self, level: &LogLevel) -> colored::Color {
122 match level {
123 LogLevel::Silly => colored::Color::BrightMagenta,
124 LogLevel::Debug => colored::Color::BrightBlue,
125 LogLevel::Info => colored::Color::BrightGreen,
126 LogLevel::Warn => colored::Color::BrightYellow,
127 LogLevel::Error => colored::Color::BrightRed,
128 LogLevel::Fatal => colored::Color::Red,
129 }
130 }
131
132
133 /*
134 @brief Captures where the logger was called from.
135
136 Captures where the logger was called from. The file name, line and column
137 Returns exact path name, which gets truncated alongside everything else
138 Will return "unknown" if unable to find file, line and column.
139
140 @return String
141 */
142 fn get_callee(&self) -> String {
143 let backtrace = Backtrace::new();
144
145 if let Some(frame) = backtrace.frames().get(3) {
146 if let Some(symbol) = frame.symbols().get(0) {
147
148 let file_name = symbol.filename()
149 .and_then(|f| f.file_name())
150 .and_then(|f| f.to_str())
151 .map(|f| f.strip_prefix("/").unwrap_or(f))
152 .unwrap_or("unknown");
153
154 let line_number = symbol.lineno().unwrap_or(0);
155 let column_number = symbol.colno().unwrap_or(0);
156 let function_name = symbol.name().map(|n| format!("{}", n)).unwrap_or("top level".to_string());
157
158 return format!(
159 "{}",
160 format!("at {}:{}:{} [{}]", file_name, line_number, column_number, function_name).italic()
161 );
162 }
163 }
164
165 "unknown".to_string()
166 }
167
168 /*
169 @brief Seralize data appended as object
170
171 Seralise all data that gets appended within the object
172 Allowing for ease of printing to the console, or file
173
174 @param object to seralize.
175
176 @return String
177 */
178 fn serialize<T: Serialize>(&self, obj: &T) -> String {
179 to_string(obj).unwrap_or_else(|_| "Serialization error".to_string())
180 }
181
182
183 /*
184 @brief Remove any ansi, only used for file logging.
185
186 Will remove all ansi (the colour to terminal), for file logging
187 Making it much easier to read.
188
189 @param message to remove ansi from
190
191 @return String
192 */
193 fn remove_ansi(&self, message: &str) -> String {
194 let ansi_regex = Regex::new(r"\x1B[@-_][0-?]*[ -/]*[@-~]").unwrap();
195 ansi_regex.replace_all(message, "").to_string()
196 }
197
198
199 /*
200 @brief Writes the data to the file or terminal.
201
202 Serializes all data that gets appended within the object, allowing for ease of
203 printing to the console or file. The method checks the log level and formats the
204 message accordingly, including the timestamp, log level, and other metadata.
205
206 @param message The message to log.
207 @param tag A tag for categorizing the log entry.
208 @param at Whether to include caller information.
209 @param level The log level of the message.
210 @param object Optional object to serialize and log.
211
212 @return void
213 */
214 fn write<T: Serialize + 'static>(&self, message: &str, tag: &str, at: bool, level: LogLevel, object: Option<&T>) {
215 if (level as i32) > (self.log_level as i32) {
216 return;
217 }
218
219 let message = message.to_string();
220 let tag = tag.to_string();
221 let connectors = &Connectors::default();
222 let color = self.get_colour(&level);
223 let timestamp = self.timestamp();
224 let timestamp_padding = " ".pad_to_width_with_alignment(21, Alignment::Middle);
225 let dim_level_tag = " ".pad_to_width_with_alignment(6, Alignment::Middle);
226 let level_tag = self.get_tag(&level).pad_to_width_with_alignment(6, Alignment::Middle);
227 let domain_tag = format!("[{}]", tag.color(color));
228 let main_message = message.color(color);
229 let mut log = format!(
230 "{} {} {} {} {}",
231 timestamp, level_tag, connectors.start_line, domain_tag, main_message
232 );
233
234 let meta_lines: Vec<String> = if let Some(obj) = object {
235 vec![self.serialize(obj)]
236 } else {
237 vec![]
238 };
239
240 if at {
241 let callee = self.get_callee().dimmed();
242 log.push_str(&format!(
243 "\n{} {} {} {}",
244 timestamp_padding,
245 dim_level_tag,
246 if !meta_lines.is_empty() { connectors.line } else { connectors.end_line },
247 callee
248 ));
249 }
250
251 for (i, line) in meta_lines.iter().enumerate() {
252 let line_content = if i > 2 { line.dimmed() } else { line.dimmed().clone() };
253 let connector = if i == meta_lines.len() - 1 { connectors.end_line } else { connectors.line };
254 let line_number = &format!("[{}]", i + 1).dimmed();
255 log.push_str(&format!(
256 "\n{} {} {} {} {}",
257 timestamp_padding, dim_level_tag, connector, line_number, line_content
258 ));
259 }
260
261 if !self.log_file.is_empty() {
262 let log = self.remove_ansi(&log);
263 let file = OpenOptions::new()
264 .write(true)
265 .append(true)
266 .create(true)
267 .open(&self.log_file);
268
269 match file {
270 Ok(mut file) => {
271 writeln!(file, "{}", log).unwrap();
272 }
273 Err(error) => {
274 eprint!("Failed to write to log file: {}", error);
275 }
276 }
277 return;
278 }
279
280 let stdout = std::io::stdout();
281 let mut handle = stdout.lock();
282 writeln!(handle, "{}", log).unwrap();
283 }
284
285 /*
286 @brief Writes the data to the file or terminal.
287
288 The method checks the log level and formats the message accordingly,
289 including the timestamp, log level, and other metadata.
290
291 @param message The message to log.
292 @param tag A tag for categorizing the log entry.
293 @param level The log level of the message.
294
295 @return void
296 */
297 fn write_single(&self, message: &str, tag: &str, level: LogLevel) {
298 if (level as i32) > (self.log_level as i32) {
299 return;
300 }
301
302 let message = message.to_string();
303 let tag = tag.to_string();
304 let connectors = &Connectors::default();
305 let color = self.get_colour(&level);
306 let timestamp = self.timestamp();
307 let level_tag = self.get_tag(&level).pad_to_width_with_alignment(6, Alignment::Middle);
308 let domain_tag = format!("[{}]", tag.color(color));
309 let main_message = message.color(color);
310 let log = format!(
311 "{} {} {} {} {}",
312 timestamp, level_tag, connectors.single_line, domain_tag, main_message
313 );
314
315 if !self.log_file.is_empty() {
316 let log = self.remove_ansi(&log);
317 let file = OpenOptions::new()
318 .write(true)
319 .append(true)
320 .create(true)
321 .open(&self.log_file);
322
323 match file {
324 Ok(mut file) => {
325 writeln!(file, "{}", log).unwrap();
326 }
327 Err(error) => {
328 eprint!("Failed to write to log file: {}", error);
329 }
330 }
331 return;
332 }
333
334 let stdout = std::io::stdout();
335 let mut handle = stdout.lock();
336 writeln!(handle, "{}", log).unwrap();
337 }
338
339 /*
340 @brief Updates the log_file name
341
342 Set the log file name, which will start printing out to that file,
343 instead of the terminal.
344
345 @param The name for the file you want to set.
346
347 @return void
348 */
349 pub fn set_file(&mut self, file_name: &str) {
350 self.log_file = file_name.to_string();
351 }
352
353 /*
354 @brief Removes the log_file name
355
356 Returns the log file name back to "", essentially
357 making it useless and returning logging back to terminal.
358
359 @return void
360 */
361 pub fn remove_file(&mut self) {
362 self.log_file = String::from("");
363 }
364
365 /*
366 @brief Update the log level
367
368 Set the logLevel, to print more or less
369 logging structures.
370
371 @param LogLevel you wish to set it to.
372
373 @return void
374 */
375 pub fn set_log_level(&mut self, log_level: LogLevel) {
376 self.log_level = log_level;
377 }
378
379 /*
380 @brief Logs to the terminal, or file using the tag fatal.
381
382 Log a message, using the silly tag.
383
384 @param message The message to log.
385 @param tag A tag for categorizing the log entry.
386 @param at Whether to include caller information.
387 @param object Optional object to serialize and log.
388
389 @return void
390
391 @return void
392 */
393 pub fn silly<T: Serialize + 'static>(&self, message: &str, tag: &str, at: bool, object: T) {
394 self.write(message, tag, at, LogLevel::Silly, Some(&object));
395 }
396
397 /*
398 @brief Logs to the terminal, or file using the tag debug.
399
400 Log a message, using the debug tag.
401
402 @param message The message to log.
403 @param tag A tag for categorizing the log entry.
404 @param at Whether to include caller information.
405 @param object Optional object to serialize and log.
406
407 @return void
408
409 @return void
410 */
411 pub fn debug<T: Serialize + 'static>(&self, message: &str, tag: &str, at: bool, object: T) {
412 self.write(message, tag, at, LogLevel::Debug, Some(&object))
413 }
414
415 /*
416 @brief Logs to the terminal, or file using the tag info.
417
418 Log a message, using the silly info.
419
420 @param message The message to log.
421 @param tag A tag for categorizing the log entry.
422 @param at Whether to include caller information.
423 @param object Optional object to serialize and log.
424
425 @return void
426
427 @return void
428 */
429 pub fn info<T: Serialize + 'static>(&self, message: &str, tag: &str, at: bool, object: T) {
430 self.write( message, tag, at, LogLevel::Info, Some(&object))
431 }
432
433 /*
434 @brief Logs to the terminal, or file using the tag warn.
435 Log a message, using the warn tag.
436
437 @param message The message to log.
438 @param tag A tag for categorizing the log entry.
439 @param at Whether to include caller information.
440 @param object Optional object to serialize and log.
441
442 @return void
443
444 @return void
445 */
446 pub fn warn<T: Serialize + 'static>(&self, message: &str, tag: &str, at: bool, object: T) {
447 self.write( message, tag, at, LogLevel::Warn, Some(&object))
448 }
449
450 /*
451 @brief Logs to the terminal, or file using the tag error.
452
453 Log a message, using the error tag.
454
455 @param message The message to log.
456 @param tag A tag for categorizing the log entry.
457 @param at Whether to include caller information.
458 @param object Optional object to serialize and log.
459
460 @return void
461
462 @return void
463 */
464 pub fn error<T: Serialize + 'static>(&self, message: &str, tag: &str, at: bool, object: T) {
465 self.write( message, tag, at, LogLevel::Error, Some(&object))
466 }
467
468 /*
469 @brief Logs to the terminal, or file using the tag fatal.
470
471 Log a message, using the fatal tag.
472
473 @param message The message to log.
474 @param tag A tag for categorizing the log entry.
475 @param at Whether to include caller information.
476 @param object Optional object to serialize and log.
477
478 @return void
479
480 @return void
481 */
482 pub fn fatal<T: Serialize + 'static>(&self, message: &str, tag: &str, at: bool, object: T) {
483 self.write(message, tag, at, LogLevel::Fatal, Some(&object))
484 }
485
486
487 /*
488 @brief Writes the data to the file or terminal.
489
490 Log a message, using the silly tag.
491
492 @param message The message to log.
493 @param tag A tag for categorizing the log entry.
494
495 @return void
496 */
497 pub fn silly_single(&self, message: &str, tag: &str) {
498 self.write_single(message, tag, LogLevel::Silly);
499 }
500
501
502 /*
503 @brief Writes the data to the file or terminal.
504
505 Log a message, using the debug tag.
506
507 @param message The message to log.
508 @param tag A tag for categorizing the log entry.
509
510 @return void
511 */
512 pub fn debug_single(&self, message: &str, tag: &str) {
513 self.write_single(message, tag, LogLevel::Debug)
514 }
515
516
517 /*
518 @brief Writes the data to the file or terminal.
519
520 Log a message, using the info tag.
521
522 @param message The message to log.
523 @param tag A tag for categorizing the log entry.
524
525 @return void
526 */
527 pub fn info_single(&self, message: &str, tag: &str) {
528 self.write_single(message, tag, LogLevel::Info)
529 }
530
531
532 /*
533 @brief Writes the data to the file or terminal.
534
535 Log a message, using the warn tag.
536
537 @param message The message to log.
538 @param tag A tag for categorizing the log entry.
539
540 @return void
541 */
542 pub fn warn_single(&self, message: &str, tag: &str) {
543 self.write_single(message, tag, LogLevel::Warn)
544 }
545
546
547 /*
548 @brief Writes the data to the file or terminal.
549
550 Log a message, using the error tag.
551
552 @param message The message to log.
553 @param tag A tag for categorizing the log entry.
554
555 @return void
556 */
557 pub fn error_single(&self, message: &str, tag: &str) {
558 self.write_single(message, tag, LogLevel::Error)
559 }
560
561
562 /*
563 @brief Writes the data to the file or terminal.
564
565 Log a message, using the fatal tag.
566
567 @param message The message to log.
568 @param tag A tag for categorizing the log entry.
569
570 @return void
571 */
572 pub fn fatal_single(&self, message: &str, tag: &str) {
573 self.write_single(message, tag, LogLevel::Fatal)
574 }
575}