1use lazy_static::lazy_static;
6use std::{
7 env,
8 fs::{File, OpenOptions},
9 io::Write,
10 sync::{Arc, Mutex},
11};
12
13#[cfg(test)]
14use std::hash::{Hash, Hasher};
15
16const DEFAULT_LOG_FILE: &str = "woody.log";
17
18lazy_static! {
19 static ref INSTANCE: Arc<Mutex<Option<Logger>>> = Arc::new(Mutex::new(None));
20 static ref FILENAME: Arc<Mutex<String>> = Arc::new(Mutex::new(DEFAULT_LOG_FILE.to_string()));
21}
22
23#[allow(dead_code)]
25#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
26pub enum LogLevel {
27 Error = 5,
29 Warning = 4,
31 Debug = 3,
33 Info = 2,
35 Trace = 1,
37 Off = 0,
39 ALL = -1,
41}
42
43impl std::fmt::Display for LogLevel {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 LogLevel::Debug => write!(f, "DEBUG"),
47 LogLevel::Info => write!(f, "INFO"),
48 LogLevel::Warning => write!(f, "WARNING"),
49 LogLevel::Error => write!(f, "ERROR"),
50 LogLevel::Trace => write!(f, "TRACE"),
51 LogLevel::Off => write!(f, "OFF"),
52 LogLevel::ALL => write!(f, ""),
53 }
54 }
55}
56
57#[derive(Clone, Debug)]
59#[allow(dead_code)]
60pub struct Logger {
61 file: Arc<Mutex<File>>,
62 level: LogLevel,
63 filename: String,
64}
65
66#[cfg(test)]
71fn generate_temp_file_name() -> String {
72 let mut hasher = std::collections::hash_map::DefaultHasher::new();
73 let now = chrono::Local::now();
74 let now_string = now.format("%Y-%m-%d %H:%M:%S%.3f %Z").to_string();
75 now_string.hash(&mut hasher);
76 let hash = hasher.finish();
77 let prefix = "temp-";
78 let suffix = ".log";
79 let len = 32 - prefix.len() - suffix.len();
81 let hash = format!("{hash:0>len$}");
82
83 format!("temp-{hash}.log")
84}
85
86#[cfg(not(test))]
87fn get_file_and_filename() -> (Arc<Mutex<File>>, String) {
88 let mut filename: String;
89 let file: Arc<Mutex<File>>;
90 filename = FILENAME.lock().unwrap().clone();
91 let env_filename = env::var("WOODY_FILE");
92 if let Ok(env_filename) = env_filename {
93 filename = env_filename;
94 }
95 let f = OpenOptions::new().create(true).append(true).open(&filename);
96 file = Arc::new(Mutex::new(f.unwrap()));
97 return (file, filename);
98}
99
100#[cfg(test)]
102fn get_file_and_filename() -> (Arc<Mutex<File>>, String) {
103 let filename: String;
104 let file: Arc<Mutex<File>>;
105 let temp_dir_base = env::temp_dir();
106 let temp_dir = temp_dir_base.join("logger");
109 if temp_dir.exists() {
111 std::fs::remove_dir_all(&temp_dir).unwrap();
112 }
113 std::fs::create_dir(&temp_dir).unwrap();
114 let temp_file_name = generate_temp_file_name();
115 let temp_file_path = temp_dir.join(temp_file_name);
116 filename = temp_file_path.to_str().unwrap().to_string();
117
118 let f = OpenOptions::new()
119 .create(true)
120 .append(true)
121 .open(temp_file_path);
122 file = Arc::new(Mutex::new(f.unwrap()));
123
124 (file, filename)
125}
126
127impl Logger {
128 fn new() -> Self {
130 let env_level = env::var("WOODY_LEVEL");
131 let level = match env_level {
132 Ok(x) => match x.to_lowercase().as_str() {
133 "error" | "5" => LogLevel::Error,
134 "warning" | "warn" | "4" => LogLevel::Warning,
135 "debug" | "3" => LogLevel::Debug,
136 "info" | "2" => LogLevel::Info,
137 "trace" | "1" => LogLevel::Trace,
138 "off" | "0" => LogLevel::Off,
139 _ => LogLevel::ALL,
140 },
141 Err(_) => LogLevel::ALL,
142 };
143
144 let (file, filename) = get_file_and_filename();
145
146 Self {
147 file,
148 level,
149 filename,
150 }
151 }
152
153 pub fn set_level(&mut self, level: LogLevel) {
155 self.level = level;
156 }
157
158 pub fn log<W: Write>(&self, info: &LogInfo, writer: Option<&mut W>) {
160 if self.level > info.level || self.level == LogLevel::Off {
161 return;
166 }
167
168 let now = chrono::Local::now();
169 let thread = info.thread.clone().unwrap_or_else(|| {
170 let thread = std::thread::current();
171 let name = thread.name().unwrap_or("unnamed");
172 name.to_string()
173 });
174 let location = format!("{}:{}", info.filepath, info.line_number);
175 let level = info.level;
176 let message = info.message.clone();
177 let now_string = now.format("%Y-%m-%d %H:%M:%S%.3f %Z");
178 let output = format!("[{now_string}] [{level}] [{thread}] [{location}] {message}\n");
179
180 if let Some(writer) = writer {
181 writer.write_all(output.as_bytes()).unwrap();
182 return;
183 }
184
185 let mut file = self.file.lock().unwrap();
186 file.write_all(output.as_bytes()).unwrap();
187 }
188
189 pub fn get_instance() -> Logger {
191 let current_global_instance = INSTANCE.clone();
193 let mut current_global_instance_lock = current_global_instance.lock().unwrap();
194 if current_global_instance_lock.is_none() {
195 let logger = Logger::new();
197 *current_global_instance_lock = Some(logger.clone());
198 logger
199 } else {
200 current_global_instance_lock.clone().unwrap()
202 }
203 }
204}
205
206#[derive(Clone)]
208pub struct LogInfo {
209 pub level: LogLevel,
211 pub message: String,
213 pub filepath: &'static str,
215 pub line_number: u32,
217 pub thread: Option<String>,
219}
220
221#[macro_export]
231macro_rules! log {
232 ($message:expr) => {
233 let message = $message.to_string();
234 let logger = $crate::Logger::get_instance();
235 let info = $crate::LogInfo {
236 level: $crate::LogLevel::Info,
237 message,
238 filepath: file!(),
239 line_number: line!(),
240 thread: None,
241 };
242 let writer: Option<&mut Vec<u8>> = None;
243 logger.log(&info, writer);
244 };
245 ($level:expr, $message:expr) => {
246 let message = $message.to_string();
247 let logger = $crate::Logger::get_instance();
248 let info = $crate::LogInfo {
249 level: $level,
250 message,
251 filepath: file!(),
252 line_number: line!(),
253 thread: None,
254 };
255 let writer: Option<&mut Vec<u8>> = None;
256 logger.log(&info, writer);
257 };
258}
259
260#[macro_export]
268macro_rules! log_debug {
269 ($message:expr) => {
270 $crate::log!($crate::LogLevel::Debug, $message);
271 };
272
273 ($message:expr, $($arg:tt)*) => {
274 let message = format!($message, $($arg)*).to_string();
275 $crate::log!($crate::LogLevel::Debug, message);
276 };
277}
278
279#[macro_export]
286macro_rules! log_info {
287 ($message:expr) => {
288 $crate::log!($crate::LogLevel::Info, $message);
289 };
290
291 ($message:expr, $($arg:tt)*) => {
292 let message = format!($message, $($arg)*).to_string();
293 $crate::log!($crate::LogLevel::Info, message);
294 };
295}
296
297#[macro_export]
304macro_rules! log_warning {
305 ($message:expr) => {
306 $crate::log!($crate::LogLevel::Warning, $message);
307 };
308
309 ($message:expr, $($arg:tt)*) => {
310 let message = format!($message, $($arg)*).to_string();
311 $crate::log!($crate::LogLevel::Warning, message);
312 };
313}
314
315#[macro_export]
322macro_rules! log_error {
323 ($message:expr) => {
324 $crate::log!($crate::LogLevel::Error, $message);
325 };
326
327 ($message:expr, $($arg:tt)*) => {
328 let message = format!($message, $($arg)*).to_string();
329 $crate::log!($crate::LogLevel::Error, message);
330 };
331}
332
333#[macro_export]
340macro_rules! log_trace {
341 ($message:expr) => {
342 $crate::log!($crate::LogLevel::Trace, $message);
343 };
344
345 ($message:expr, $($arg:tt)*) => {
346 let message = format!($message, $($arg)*).to_string();
347 $crate::log!($crate::LogLevel::Trace, message);
348 };
349}
350
351#[macro_export]
358macro_rules! log_text {
359 ($message:expr) => {
360 $crate::log!($crate::LogLevel::Off, $message);
361 };
362
363 ($message:expr, $($arg:tt)*) => {
364 let message = format!($message, $($arg)*).to_string();
365 $crate::log!($crate::LogLevel::Off, message);
366 };
367}
368
369#[allow(unused_macros)]
373macro_rules! function {
374 () => {{
375 fn f() {}
376 fn type_name_of<T>(_: T) -> &'static str {
377 std::any::type_name::<T>()
378 }
379 let name = type_name_of(f);
380 &name[..name.len() - 3]
381 }};
382}
383
384#[cfg(test)]
385mod tests {
386 use serial_test::serial;
387 use std::io::Read;
388 use tokio::runtime::Runtime;
389
390 use super::*;
391
392 async fn write_to_logger(id: Option<u8>) {
393 let logger = Logger::get_instance();
394 let thread = std::thread::current();
395 let thread = thread.name();
396 let thread = match id {
397 Some(id) => format!("{}-{}", thread.unwrap(), id),
398 None => thread.unwrap().to_string(),
399 };
400 let id = id.unwrap_or(0);
401 let message = format!("Hello, world! {id}");
402 let info = LogInfo {
403 level: LogLevel::Info,
404 message,
405 filepath: file!(),
406 line_number: line!(),
407 thread: Some(thread),
408 };
409
410 let writer: Option<&mut Vec<u8>> = None;
411 logger.log(&info, writer);
412 }
413
414 fn get_global_instance() -> Option<Logger> {
416 let current_global_instance = INSTANCE.clone();
417 let current_global_instance_lock = current_global_instance.lock().unwrap();
418 current_global_instance_lock.clone()
419 }
420
421 #[test]
424 #[serial]
425 fn test_global_instance_value() {
426 let current_global_instance = get_global_instance();
427 assert!(current_global_instance.is_none() || current_global_instance.is_some());
428
429 let logger = Logger::get_instance();
430 let current_global_instance = get_global_instance();
431 assert!(current_global_instance.is_some());
432 assert_eq!(logger.level, LogLevel::ALL);
433 }
434
435 #[test]
437 fn test_writing_to_logger() {
438 let logger = Logger::get_instance();
439 let info = LogInfo {
440 level: LogLevel::Info,
441 message: "Hello, world!".to_string(),
442 filepath: file!(),
443 line_number: line!(),
444 thread: None,
445 };
446
447 let mut writer = Vec::new();
448 logger.log(&info, Some(&mut writer));
449
450 let mut contents = String::new();
451 contents.push_str(&String::from_utf8(writer).unwrap());
452
453 assert!(
454 contents.contains(info.message.as_str()),
455 "Contents of log does not contain 'Hello, world!'\nContents: {contents}"
456 );
457 }
458
459 fn check_log_file_contains(s: String) {
460 let logger = Logger::get_instance();
462 let filename = &logger.filename;
463 let file = OpenOptions::new().read(true).open(filename);
464 if file.is_err() {
465 panic!("Could not open {}: {:?}", filename, file.unwrap_err());
466 }
467 let mut contents = String::new();
468 file.unwrap().read_to_string(&mut contents).unwrap();
469 assert!(
470 contents.contains(s.as_str()),
471 "Contents of log does not contain '{s}'\nContents: {contents}\nLogger: {logger:?}"
472 );
473 }
474
475 #[test]
477 fn test_writing_to_logger_across_threads() {
478 async fn spawn_logs() {
479 let mut handles = Vec::new();
480 for i in 0..10 {
481 let task = tokio::spawn(write_to_logger(Some(i)));
482 handles.push(task);
483 }
484
485 for handle in handles {
486 handle.await.unwrap();
487 }
488 }
489
490 let rt = Runtime::new().unwrap();
491 rt.block_on(spawn_logs());
492
493 let filename = Logger::get_instance().filename;
494 let mut file = OpenOptions::new().read(true).open(&filename).unwrap();
495 let mut contents = String::new();
496 file.read_to_string(&mut contents).unwrap();
497
498 for i in 0..10 {
499 let message = format!("Hello, world! {i}");
500 check_log_file_contains(message);
501 }
502 }
503
504 #[test]
505 fn test_log_info() {
506 let f = function!();
507 let s = format!("Hello, {f}!");
508 log_info!(s);
509 check_log_file_contains(s);
510 }
511
512 #[test]
513 fn test_log_debug() {
514 let f = function!();
515 let s = format!("Hello, {f}!");
516 log_debug!(s);
517 check_log_file_contains(s);
518 }
519
520 #[test]
521 fn test_log_warning() {
522 let f = function!();
523 let s = format!("Hello, {f}!");
524 log_warning!(s);
525 check_log_file_contains(s);
526 }
527
528 #[test]
529 fn test_log_error() {
530 let f = function!();
531 let s = format!("Hello, {f}!");
532 log_error!(s);
533 check_log_file_contains(s);
534 }
535
536 #[test]
537 fn test_log_trace() {
538 let f = function!();
539 let s = format!("Hello, {f}!");
540 log_trace!(s);
541 check_log_file_contains(s);
542 }
543
544 #[test]
545 fn test_log_text() {
546 let f = function!();
547 let s = format!("Hello, {f}!");
548 log_text!(s);
549 check_log_file_contains(s);
550 }
551
552 #[test]
553 fn test_random_file_name() {
554 let filename = generate_temp_file_name();
555
556 assert_eq!(
558 filename.len(),
559 32,
560 "Filename is not 32 characters long: {}",
561 filename.len()
562 );
563
564 assert!(
566 filename.starts_with("temp-"),
567 "Filename does not start with 'temp-': {filename}"
568 );
569 }
570}