use chrono::Local;
use core::fmt;
use lazy_static::lazy_static;
use std::{fs::OpenOptions, io::Write, sync::Mutex};
lazy_static! {
static ref LOG_LEVEL: Mutex<LogLevel> = Mutex::new(LogLevel::Default);
static ref LOG_PATH: Mutex<String> = Mutex::new(String::new());
}
#[derive(Debug, PartialEq, Clone)]
pub enum LogLevel {
Verbose,
Debug,
Default,
ErrorsOnly,
}
#[derive(Debug, PartialEq)]
pub enum LogType {
Info,
Debug,
Warning,
Error,
}
impl fmt::Display for LogType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Info => write!(f, "INFO"),
Self::Debug => write!(f, "DEBUG"),
Self::Warning => write!(f, "WARNING"),
Self::Error => write!(f, "ERROR"),
}
}
}
pub fn set_level(level: LogLevel) {
*LOG_LEVEL.lock().unwrap() = level;
}
pub fn set_path(path: String) {
*LOG_PATH.lock().unwrap() = path;
}
pub fn log(message: &str, t: LogType) -> Result<String, std::io::Error> {
let timestamp = Local::now();
let formatted_timestamp = timestamp.format("%Y-%m-%d %H:%M:%S");
let formatted_message = format!(
"[{log_type}] {time} -> {message}",
log_type = t,
time = formatted_timestamp
);
let level = LOG_LEVEL.lock().unwrap().clone();
match level {
LogLevel::ErrorsOnly => {
if t != LogType::Error {
return Ok("".to_string());
}
}
LogLevel::Default => {
if t == LogType::Debug || t == LogType::Info {
return Ok("".to_string());
}
}
LogLevel::Debug => {
if t == LogType::Info {
return Ok("".to_string());
}
}
_ => {}
}
match t {
LogType::Error => eprintln!("{}", formatted_message),
_ => println!("{}", formatted_message),
}
let path = LOG_PATH.lock().unwrap().clone();
if path != "" {
let mut file = OpenOptions::new().append(true).create(true).open(&path)?;
writeln!(file, "{}", &formatted_message)?;
}
Ok(formatted_message)
}
#[cfg(test)]
mod tests {
use all_asserts::assert_false;
use super::*;
use std::fs;
fn check_string_in_file(path: &str, string_to_find: &str) -> bool {
let lines = fs::read_to_string(path).unwrap();
for line in lines.lines() {
println!("Line read: {}", line);
if line == string_to_find {
return true;
}
}
false
}
#[test]
fn calling_set_level_sets_level() {
set_level(LogLevel::Debug);
assert_eq!(*LOG_LEVEL.lock().unwrap(), LogLevel::Debug);
}
#[test]
fn calling_set_path_sets_path() {
set_path(String::from(
"/home/jordan/projects/rust/logger/target/debug/test/log.txt",
));
assert_eq!(
*LOG_PATH.lock().unwrap(),
"/home/jordan/projects/rust/logger/target/debug/test/log.txt"
);
}
#[test]
fn log_writes_to_file() {
set_path(String::from(
"/home/jordan/projects/rust/logger/target/debug/test/log.txt",
));
let path = LOG_PATH.lock().unwrap().clone();
let logged_message = log("This is a test", LogType::Error).unwrap();
assert!(
check_string_in_file(&path, &logged_message),
"Did not find test string in log file."
);
}
#[test]
fn log_level_debug_does_not_log_info() {
set_path(String::from(
"/home/jordan/projects/rust/logger/target/debug/test/log.txt",
));
let path = LOG_PATH.lock().unwrap().clone();
assert_eq!(
&path,
"/home/jordan/projects/rust/logger/target/debug/test/log.txt"
);
set_level(LogLevel::Debug);
let log_level = LOG_LEVEL.lock().unwrap().clone();
assert_eq!(log_level, LogLevel::Debug);
let logged_message = log("This shouldn't get logged", LogType::Info).unwrap();
assert_false!(
check_string_in_file(&path, &logged_message),
"Found test string in log file."
);
}
#[test]
fn log_level_error_only_prints_errors() {
set_path(String::from(
"/home/jordan/projects/rust/logger/target/debug/test/log.txt",
));
let path = LOG_PATH.lock().unwrap().clone();
assert_eq!(
&path,
"/home/jordan/projects/rust/logger/target/debug/test/log.txt"
);
set_level(LogLevel::ErrorsOnly);
let log_level = LOG_LEVEL.lock().unwrap().clone();
assert_eq!(log_level, LogLevel::ErrorsOnly);
let path = LOG_PATH.lock().unwrap().clone();
let mut logged_message = log("This shouldn't get logged (errors_only).", LogType::Info).unwrap();
assert_false!(
check_string_in_file(&path, &logged_message),
"Found INFO test string in log file."
);
logged_message = log("This shouldn't get logged (errors_only).", LogType::Debug).unwrap();
assert_false!(
check_string_in_file(&path, &logged_message),
"Found DEBUG test string in log file."
);
logged_message = log("This shouldn't get logged (errors_only).", LogType::Warning).unwrap();
assert_false!(
check_string_in_file(&path, &logged_message),
"Found WARNING test string in log file."
);
logged_message = log("This should get logged (errors_only).", LogType::Error).unwrap();
assert!(
check_string_in_file(&path, &logged_message),
"Did not find ERROR test string in log file."
);
}
#[test]
fn log_level_verbose_prints_everything() {
set_path(String::from(
"/home/jordan/projects/rust/logger/target/debug/test/log.txt",
));
let path = LOG_PATH.lock().unwrap().clone();
assert_eq!(
&path,
"/home/jordan/projects/rust/logger/target/debug/test/log.txt"
);
set_level(LogLevel::Verbose);
let log_level = LOG_LEVEL.lock().unwrap().clone();
assert_eq!(log_level, LogLevel::Verbose);
let path = LOG_PATH.lock().unwrap().clone();
let mut logged_message = log("This should get logged (verbose).", LogType::Info).unwrap();
assert!(
check_string_in_file(&path, &logged_message),
"Did not find INFO test string in log file."
);
logged_message = log("This should get logged (verbose).", LogType::Debug).unwrap();
assert!(
check_string_in_file(&path, &logged_message),
"Did not find DEBUG test string in log file."
);
logged_message = log("This should get logged (verbose).", LogType::Warning).unwrap();
assert!(
check_string_in_file(&path, &logged_message),
"Did not find WARNING test string in log file."
);
logged_message = log("This should get logged (verbose).", LogType::Error).unwrap();
assert!(
check_string_in_file(&path, &logged_message),
"Did not find ERROR test string in log file."
);
}
#[test]
fn log_level_default_does_not_log_info_debug() {
set_path(String::from(
"/home/jordan/projects/rust/logger/target/debug/test/log.txt",
));
let path = LOG_PATH.lock().unwrap().clone();
assert_eq!(
&path,
"/home/jordan/projects/rust/logger/target/debug/test/log.txt"
);
set_level(LogLevel::Default);
let log_level = LOG_LEVEL.lock().unwrap().clone();
assert_eq!(log_level, LogLevel::Default);
let path = LOG_PATH.lock().unwrap().clone();
let mut logged_message = log("This shouldn't get logged (default).", LogType::Info).unwrap();
assert_false!(
check_string_in_file(&path, &logged_message),
"Found INFO test string in log file."
);
logged_message = log("This shouldn't get logged (default).", LogType::Debug).unwrap();
println!("Logged message: \n{}", logged_message);
assert_false!(
check_string_in_file(&path, &logged_message),
"Found DEBUG test string in log file."
);
logged_message = log("This should get logged (default).", LogType::Warning).unwrap();
assert!(
check_string_in_file(&path, &logged_message),
"Did not find WARNING test string in log file."
);
logged_message = log("This should get logged (default).", LogType::Error).unwrap();
assert!(
check_string_in_file(&path, &logged_message),
"Did not find ERROR test string in log file."
);
}
}