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#[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
43pub fn set_level(level: LogLevel) {
46 *LOG_LEVEL.lock().unwrap() = level;
47}
48
49pub fn set_path(path: String) {
52 *LOG_PATH.lock().unwrap() = path;
53}
54
55pub 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}