sw_logger_rs/
lib.rs

1use chrono::Local;
2use core::fmt;
3use lazy_static::lazy_static;
4use std::{fs::OpenOptions, io::Write, sync::Mutex};
5
6lazy_static! {
7    static ref LOG_LEVEL: Mutex<LogLevel> = Mutex::new(LogLevel::Default);
8    static ref LOG_PATH: Mutex<String> = Mutex::new(String::new());
9}
10
11/// `Verbose`    -> Logs all messages, regardless of `LogType`.  
12/// `Debug`      -> Logs messages marked as `LogType::Error`, `LogType::Warning` and
13///                 `LogType::Debug`.  
14/// `Default`    -> Logs messages marked as `LogType::Error` and `LogType::Warning`.  
15/// `ErrorsOnly` -> Only logs messages marked as `LogType::Error`.
16#[derive(Debug, PartialEq, Clone)]
17pub enum LogLevel {
18    Verbose,
19    Debug,
20    Default,
21    ErrorsOnly,
22}
23
24#[derive(Debug, PartialEq)]
25pub enum LogType {
26    Info,
27    Debug,
28    Warning,
29    Error,
30}
31
32impl fmt::Display for LogType {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::Info => write!(f, "INFO"),
36            Self::Debug => write!(f, "DEBUG"),
37            Self::Warning => write!(f, "WARNING"),
38            Self::Error => write!(f, "ERROR"),
39        }
40    }
41}
42
43/// Sets the level of logging.
44/// See `LogLevel` for a description of what each level means.
45pub fn set_level(level: LogLevel) {
46    *LOG_LEVEL.lock().unwrap() = level;
47}
48
49/// Sets the default path the logger uses to write to.
50/// If left as an empty `String` (or set as one), the logger won't write to a file; only to the `stdout` and the `stderr`.
51pub fn set_path(path: String) {
52    *LOG_PATH.lock().unwrap() = path;
53}
54
55/// Log a message to stdout and, optionally, a file.
56/// This function will format the message with a type-signifier and the timestamp of the moment of
57/// logging.
58///
59/// Example:  
60/// ```
61/// [WARNING] 2024-02-21 12:05:51 -> This is a logged message!
62/// ```
63///
64/// `message` -> the message to print.
65/// `t`       -> the `LogType` to use. Defines both the type-signifier in the stdout/file and
66/// whether this message should be logged at all, depending on the global `LogLevel` set.  
67/// p         -> A custom path for the logger to write this message to. When `None` the logger will
68/// write to the default path set with `set_path`. A custom path can be specified like so:
69/// `Some("/the/path/here")`.
70pub fn log(message: &str, t: LogType, p: Option<&str>) -> Result<String, std::io::Error> {
71    let default_path = LOG_PATH.lock().unwrap().clone();
72    let path = p.unwrap_or(&default_path);
73
74    let timestamp = Local::now();
75    let formatted_timestamp = timestamp.format("%Y-%m-%d %H:%M:%S");
76
77    let formatted_message = format!(
78        "[{log_type}] {time} -> {message}",
79        log_type = t,
80        time = formatted_timestamp
81    );
82
83    let level = LOG_LEVEL.lock().unwrap().clone();
84
85    match level {
86        LogLevel::ErrorsOnly => {
87            if t != LogType::Error {
88                return Ok("".to_string());
89            }
90        }
91        LogLevel::Default => {
92            if t == LogType::Debug || t == LogType::Info {
93                return Ok("".to_string());
94            }
95        }
96        LogLevel::Debug => {
97            if t == LogType::Info {
98                return Ok("".to_string());
99            }
100        }
101        _ => {}
102    }
103
104    match t {
105        LogType::Error => eprintln!("{}", formatted_message),
106        _ => println!("{}", formatted_message),
107    }
108
109    if path != String::new() {
110        let mut file = OpenOptions::new().append(true).create(true).open(&path)?;
111        writeln!(file, "{}", &formatted_message)?;
112    }
113
114    Ok(formatted_message)
115}
116
117#[cfg(test)]
118mod tests {
119    use all_asserts::assert_false;
120
121    use super::*;
122    use std::fs;
123
124    fn check_string_in_file(path: &str, string_to_find: &str) -> bool {
125        let lines = fs::read_to_string(path).unwrap();
126
127        for line in lines.lines() {
128            println!("Line read: {}", line);
129            if line == string_to_find {
130                return true;
131            }
132        }
133        false
134    }
135
136    #[test]
137    fn calling_set_level_sets_level() {
138        set_level(LogLevel::Debug);
139        assert_eq!(*LOG_LEVEL.lock().unwrap(), LogLevel::Debug);
140    }
141
142    #[test]
143    fn calling_set_path_sets_path() {
144        set_path(String::from(
145            "/home/jordan/projects/rust/logger/target/debug/test/test.log",
146        ));
147        assert_eq!(
148            *LOG_PATH.lock().unwrap(),
149            "/home/jordan/projects/rust/logger/target/debug/test/test.log"
150        );
151    }
152
153    #[test]
154    fn log_writes_to_default_file() {
155        set_path(String::from(
156            "/home/jordan/projects/rust/logger/target/debug/test/test.log",
157        ));
158
159        let path = LOG_PATH.lock().unwrap().clone();
160
161        let logged_message = log("This is a test", LogType::Error, None).unwrap();
162        assert!(
163            check_string_in_file(&path, &logged_message),
164            "Did not find test string in log file."
165        );
166    }
167
168    #[test]
169    fn log_writes_to_custom_file() {
170        let path = "/home/jordan/projects/rust/logger/target/debug/test/custom.log";
171
172        let logged_message = log("This is a test", LogType::Error, Some(path)).unwrap();
173        assert!(
174            check_string_in_file(&path, &logged_message),
175            "Did not find test string in log file."
176        );
177    }
178
179    #[test]
180    fn log_level_debug_does_not_log_info() {
181        set_path(String::from(
182            "/home/jordan/projects/rust/logger/target/debug/test/test.log",
183        ));
184        let path = LOG_PATH.lock().unwrap().clone();
185        assert_eq!(
186            &path,
187            "/home/jordan/projects/rust/logger/target/debug/test/test.log"
188        );
189
190        set_level(LogLevel::Debug);
191        let log_level = LOG_LEVEL.lock().unwrap().clone();
192        assert_eq!(log_level, LogLevel::Debug);
193
194        let logged_message = log("This shouldn't get logged", LogType::Info, None).unwrap();
195        assert_false!(
196            check_string_in_file(&path, &logged_message),
197            "Found test string in log file."
198        );
199    }
200
201    #[test]
202    fn log_level_error_only_prints_errors() {
203        set_path(String::from(
204            "/home/jordan/projects/rust/logger/target/debug/test/test.log",
205        ));
206        let path = LOG_PATH.lock().unwrap().clone();
207        assert_eq!(
208            &path,
209            "/home/jordan/projects/rust/logger/target/debug/test/test.log"
210        );
211
212        set_level(LogLevel::ErrorsOnly);
213        let log_level = LOG_LEVEL.lock().unwrap().clone();
214        assert_eq!(log_level, LogLevel::ErrorsOnly);
215
216        let path = LOG_PATH.lock().unwrap().clone();
217
218        let mut logged_message = log("This shouldn't get logged (errors_only).", LogType::Info, None).unwrap();
219        assert_false!(
220            check_string_in_file(&path, &logged_message),
221            "Found INFO test string in log file."
222        );
223
224        logged_message = log("This shouldn't get logged (errors_only).", LogType::Debug, None).unwrap();
225        assert_false!(
226            check_string_in_file(&path, &logged_message),
227            "Found DEBUG test string in log file."
228        );
229
230        logged_message = log("This shouldn't get logged (errors_only).", LogType::Warning, None).unwrap();
231        assert_false!(
232            check_string_in_file(&path, &logged_message),
233            "Found WARNING test string in log file."
234        );
235
236        logged_message = log("This should get logged (errors_only).", LogType::Error, None).unwrap();
237        assert!(
238            check_string_in_file(&path, &logged_message),
239            "Did not find ERROR test string in log file."
240        );
241    }
242
243    #[test]
244    fn log_level_verbose_prints_everything() {
245        set_path(String::from(
246            "/home/jordan/projects/rust/logger/target/debug/test/test.log",
247        ));
248        let path = LOG_PATH.lock().unwrap().clone();
249        assert_eq!(
250            &path,
251            "/home/jordan/projects/rust/logger/target/debug/test/test.log"
252        );
253
254        set_level(LogLevel::Verbose);
255        let log_level = LOG_LEVEL.lock().unwrap().clone();
256        assert_eq!(log_level, LogLevel::Verbose);
257
258        let path = LOG_PATH.lock().unwrap().clone();
259
260        let mut logged_message = log("This should get logged (verbose).", LogType::Info, None).unwrap();
261        assert!(
262            check_string_in_file(&path, &logged_message),
263            "Did not find INFO test string in log file."
264        );
265
266        logged_message = log("This should get logged (verbose).", LogType::Debug, None).unwrap();
267        assert!(
268            check_string_in_file(&path, &logged_message),
269            "Did not find DEBUG test string in log file."
270        );
271
272        logged_message = log("This should get logged (verbose).", LogType::Warning, None).unwrap();
273        assert!(
274            check_string_in_file(&path, &logged_message),
275            "Did not find WARNING test string in log file."
276        );
277
278        logged_message = log("This should get logged (verbose).", LogType::Error, None).unwrap();
279        assert!(
280            check_string_in_file(&path, &logged_message),
281            "Did not find ERROR test string in log file."
282        );
283    }
284
285    #[test]
286    fn log_level_default_does_not_log_info_debug() {
287        set_path(String::from(
288            "/home/jordan/projects/rust/logger/target/debug/test/test.log",
289        ));
290        let path = LOG_PATH.lock().unwrap().clone();
291        assert_eq!(
292            &path,
293            "/home/jordan/projects/rust/logger/target/debug/test/test.log"
294        );
295
296        set_level(LogLevel::Default);
297        let log_level = LOG_LEVEL.lock().unwrap().clone();
298        assert_eq!(log_level, LogLevel::Default);
299
300        let path = LOG_PATH.lock().unwrap().clone();
301
302        let mut logged_message = log("This shouldn't get logged (default).", LogType::Info, None).unwrap();
303        assert_false!(
304            check_string_in_file(&path, &logged_message),
305            "Found INFO test string in log file."
306        );
307
308        logged_message = log("This shouldn't get logged (default).", LogType::Debug, None).unwrap();
309        println!("Logged message: \n{}", logged_message);
310        assert_false!(
311            check_string_in_file(&path, &logged_message),
312            "Found DEBUG test string in log file."
313        );
314
315        logged_message = log("This should get logged (default).", LogType::Warning, None).unwrap();
316        assert!(
317            check_string_in_file(&path, &logged_message),
318            "Did not find WARNING test string in log file."
319        );
320
321        logged_message = log("This should get logged (default).", LogType::Error, None).unwrap();
322        assert!(
323            check_string_in_file(&path, &logged_message),
324            "Did not find ERROR test string in log file."
325        );
326    }
327}