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}