Logging #26
@ -9,6 +9,11 @@ edition = "2021"
|
|||||||
chrono = { version = "0.4.34", default-features = false, features = ["now"] }
|
chrono = { version = "0.4.34", default-features = false, features = ["now"] }
|
||||||
config = { path = "../config"}
|
config = { path = "../config"}
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
uuid = { version = "1.7.0", default-features = false, features = ["v4", "fast-rng"] }
|
||||||
|
once_cell = { version = "1.19.0", default-features = false, features = ["std"] }
|
||||||
|
futures = { version = "0.3.30", default-features = false, features = ["alloc", "executor"] }
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
unsafe_code = "forbid"
|
unsafe_code = "forbid"
|
||||||
missing_docs = "warn"
|
missing_docs = "warn"
|
||||||
|
@ -3,11 +3,14 @@
|
|||||||
* It should mostly be called statically.
|
* It should mostly be called statically.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
mod test;
|
||||||
|
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use config::LogVerbosity;
|
use config::LogVerbosity;
|
||||||
|
use config::Config;
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
@ -26,29 +29,9 @@ use LogMessageType::GenericWarn;
|
|||||||
* Panics if readlock on [`config::CONFIG`] could not be acquired
|
* Panics if readlock on [`config::CONFIG`] could not be acquired
|
||||||
* or if another error occurs, such as a full disk.
|
* or if another error occurs, such as a full disk.
|
||||||
*/
|
*/
|
||||||
// TODO: Create macro to make last three parameters optional.
|
pub fn log_message(msg: &LogMessage, conf: &Config, file: &str, line: u32, column: u32) {
|
||||||
pub fn log_message(msg: &LogMessage, file: &str, line: &str, column: &str) {
|
|
||||||
let conf = config::CONFIG.read().unwrap_or_else(|_| {
|
|
||||||
panic!("Failed aqcuire read lock on config\nTried to log message: {msg}")
|
|
||||||
});
|
|
||||||
|
|
||||||
if conf.log_verbosity() < &msg.1 {
|
let Some(log_line) = log_to_str(msg, conf, file, line, column) else { return };
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut log_line = String::new();
|
|
||||||
|
|
||||||
// add time substring
|
|
||||||
if *conf.log_time() {
|
|
||||||
log_line += &format!("{} ", Utc::now().format(conf.log_time_format()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// add code location substring
|
|
||||||
if *conf.log_location() {
|
|
||||||
log_line += &format!("{file}:{line},{column} ");
|
|
||||||
}
|
|
||||||
|
|
||||||
log_line += &msg.to_string();
|
|
||||||
|
|
||||||
// May panic if file cannot be opened or written to.
|
// May panic if file cannot be opened or written to.
|
||||||
conf.log_path().as_ref().map_or_else(
|
conf.log_path().as_ref().map_or_else(
|
||||||
@ -57,11 +40,11 @@ pub fn log_message(msg: &LogMessage, file: &str, line: &str, column: &str) {
|
|||||||
let mut file = OpenOptions::new()
|
let mut file = OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
|
.create(true)
|
||||||
.open(path)
|
.open(path)
|
||||||
.unwrap_or_else(|_| {
|
.unwrap_or_else(|_| {
|
||||||
panic!(
|
panic!(
|
||||||
"Could not open log fil
|
"Could not open log file: {path:#?}"
|
||||||
e: {path:#?}"
|
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
writeln!(file, "{log_line}")
|
writeln!(file, "{log_line}")
|
||||||
@ -76,13 +59,54 @@ pub fn log_message(msg: &LogMessage, file: &str, line: &str, column: &str) {
|
|||||||
// May panic if writing to stdout fails.
|
// May panic if writing to stdout fails.
|
||||||
println!("{log_line}");
|
println!("{log_line}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
drop(conf); // remove read lock
|
/**
|
||||||
|
* Return log line, if message may be logged according to [`config::Config`].
|
||||||
|
* # Panics
|
||||||
|
* Panics if readlock on [`config::CONFIG`] could not be acquired
|
||||||
|
* or if another error occurs, such as a full disk.
|
||||||
|
*/
|
||||||
|
#[must_use]
|
||||||
|
pub fn log_to_str(msg: &LogMessage, conf: &Config, file: &str, line: u32, column: u32) -> Option<String>
|
||||||
|
{
|
||||||
|
if conf.log_verbosity() < &msg.1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut log_line = String::new();
|
||||||
|
|
||||||
|
// add time substring
|
||||||
|
if *conf.log_time() {
|
||||||
|
log_line += &format!("{} ", Utc::now().format(conf.log_time_format()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// add code location substring
|
||||||
|
if *conf.log_location() {
|
||||||
|
log_line += &format!("{file}:{line},{column} ");
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(log_line + &msg.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand version for [`log_message`], which does not require information about the [`config::Config::log_location`].
|
||||||
|
*/
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log{
|
||||||
|
($msg:expr) => {
|
||||||
|
let conf = config::CONFIG.read().unwrap_or_else(|_| {
|
||||||
|
panic!("Failed aqcuire read lock on config!")
|
||||||
|
});
|
||||||
|
log_message($msg, &*conf, file!(), line!(), column!());
|
||||||
|
drop(conf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A named typle assigning a log [`LogVerbosity`] to a [`LogMessageType`]
|
* A named typle assigning a log [`LogVerbosity`] to a [`LogMessageType`]
|
||||||
*/
|
*/
|
||||||
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
pub struct LogMessage(LogMessageType, LogVerbosity);
|
pub struct LogMessage(LogMessageType, LogVerbosity);
|
||||||
|
|
||||||
impl Display for LogMessage {
|
impl Display for LogMessage {
|
||||||
@ -96,7 +120,7 @@ impl Display for LogMessage {
|
|||||||
*
|
*
|
||||||
* These groups correspond to the four levels of logging verbosity. See [`LogVerbosity`].
|
* These groups correspond to the four levels of logging verbosity. See [`LogVerbosity`].
|
||||||
*/
|
*/
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum LogMessageType {
|
pub enum LogMessageType {
|
||||||
/// Errors
|
/// Errors
|
||||||
/// An error of any type; please avoid using this.
|
/// An error of any type; please avoid using this.
|
||||||
|
262
logging/src/test.rs
Normal file
262
logging/src/test.rs
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
#![cfg(test)]
|
||||||
|
/*!
|
||||||
|
* This test suite uses uuid to easily avoid race conditions when writing to the same log file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::{collections::HashSet, env, fs::{create_dir_all, read_to_string}, mem::take, path::PathBuf};
|
||||||
|
|
||||||
|
use futures::{executor::block_on, future::join_all};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const CONCURRENT_MESSAGE_COUNT: usize = 99999;
|
||||||
|
|
||||||
|
static LOG_DIR: Lazy<String> = Lazy::new(||
|
||||||
|
if cfg!(unix)
|
||||||
|
{
|
||||||
|
String::from("/tmp/WANessa/unit-tests/logging")
|
||||||
|
}
|
||||||
|
else if cfg !(windows) {
|
||||||
|
let tmp_path = env::var("TMP").unwrap_or_else(|_| env::var("TEMP").expect("Windows should have both TMP and TEMP, but you have neither, what did you do?"));
|
||||||
|
format!("{tmp_path}/WANessa/unit-tests/logging")
|
||||||
|
}
|
||||||
|
else { String::new() }
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Tests if [`log_message`] to file correctly.
|
||||||
|
#[test]
|
||||||
|
pub fn log_msg_file()
|
||||||
|
{
|
||||||
|
let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4());
|
||||||
|
println!("Log Path: {log_path}");
|
||||||
|
let message = LogMessageType::GenericWarn(String::from("Test Log")).log_message();
|
||||||
|
let mut config = config::DEFAULTS;
|
||||||
|
config.set_log_path(PathBuf::from(log_path));
|
||||||
|
|
||||||
|
create_dir_all(PathBuf::from(LOG_DIR.as_str()))
|
||||||
|
.unwrap_or_else(|_| panic!("Could not create directory: {}", *LOG_DIR));
|
||||||
|
|
||||||
|
log_message(&message, &config, file!(), line!(), column!());
|
||||||
|
|
||||||
|
let log_file = read_to_string(PathBuf::from(log_path))
|
||||||
|
.unwrap_or_else(|_| panic!("Could not read file: {log_path}"));
|
||||||
|
|
||||||
|
assert_eq!(message.to_string() + "\n", log_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests if [`log_message`] does not modify output from [`log_to_str`], when logging to file.
|
||||||
|
#[test]
|
||||||
|
pub fn log_str()
|
||||||
|
{
|
||||||
|
let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4());
|
||||||
|
println!("Log Path: {log_path}");
|
||||||
|
let message = LogMessageType::GenericWarn(String::from("Test Log")).log_message();
|
||||||
|
let mut config = config::DEFAULTS;
|
||||||
|
config.set_log_path(PathBuf::from(log_path));
|
||||||
|
|
||||||
|
create_dir_all(PathBuf::from(LOG_DIR.as_str()))
|
||||||
|
.unwrap_or_else(|_| panic!("Could not create directory: {}", *LOG_DIR));
|
||||||
|
|
||||||
|
let log_line = log_to_str(&message, &config, file!(), line!(), column!()).expect("There should be a log line.") + "\n";
|
||||||
|
|
||||||
|
log_message(&message, &config, file!(), line!(), column!());
|
||||||
|
|
||||||
|
let log_file = read_to_string(PathBuf::from(log_path))
|
||||||
|
.unwrap_or_else(|_| panic!("Could not read file: {log_path}"));
|
||||||
|
|
||||||
|
assert_eq!(log_line, log_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests if no messages are unintentionally filtered due to their verboisity.
|
||||||
|
#[test]
|
||||||
|
pub fn verbosity_no_filter()
|
||||||
|
{
|
||||||
|
let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4());
|
||||||
|
println!("Log Path: {log_path}");
|
||||||
|
let messages = vec![
|
||||||
|
LogMessageType::GenericErr(String::from("Test Err")).log_message(),
|
||||||
|
LogMessageType::GenericWarn(String::from("Test Warn")).log_message(),
|
||||||
|
LogMessageType::GenericInfo(String::from("Test Info")).log_message(),
|
||||||
|
LogMessageType::GenericDebug(String::from("Test Debug")).log_message(),
|
||||||
|
];
|
||||||
|
let mut config = config::DEFAULTS;
|
||||||
|
config.set_log_path(PathBuf::from(log_path));
|
||||||
|
config.set_log_verbosity(LogVerbosity::Error);
|
||||||
|
|
||||||
|
create_dir_all(PathBuf::from(LOG_DIR.as_str()))
|
||||||
|
.unwrap_or_else(|_| panic!("Could not create directory: {}", *LOG_DIR));
|
||||||
|
|
||||||
|
let log_line = log_to_str(&messages[0], &config, file!(), line!(), column!()).expect("There should be a log line.") + "\n";
|
||||||
|
|
||||||
|
for msg in messages
|
||||||
|
{
|
||||||
|
log_message(&msg, &config, file!(), line!(), column!());
|
||||||
|
}
|
||||||
|
|
||||||
|
let log_file = read_to_string(PathBuf::from(log_path))
|
||||||
|
.unwrap_or_else(|_| panic!("Could not read file: {log_path}"));
|
||||||
|
|
||||||
|
assert_eq!(log_file, log_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests if messages are properly filtered according to their verbosity.
|
||||||
|
#[test]
|
||||||
|
pub fn verbosity_filter()
|
||||||
|
{
|
||||||
|
let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4());
|
||||||
|
println!("Log Path: {log_path}");
|
||||||
|
let messages = vec![
|
||||||
|
LogMessageType::GenericErr(String::from("Test Err")).log_message(),
|
||||||
|
LogMessageType::GenericWarn(String::from("Test Warn")).log_message(),
|
||||||
|
LogMessageType::GenericInfo(String::from("Test Info")).log_message(),
|
||||||
|
LogMessageType::GenericDebug(String::from("Test Debug")).log_message(),
|
||||||
|
];
|
||||||
|
let mut config = config::DEFAULTS;
|
||||||
|
config.set_log_path(PathBuf::from(log_path));
|
||||||
|
|
||||||
|
create_dir_all(PathBuf::from(LOG_DIR.as_str()))
|
||||||
|
.unwrap_or_else(|_| panic!("Could not create directory: {}", *LOG_DIR));
|
||||||
|
|
||||||
|
let mut log_line = log_to_str(&messages[0], &config, file!(), line!(), column!()).expect("There should be a log line.") + "\n";
|
||||||
|
log_line += &(log_to_str(&messages[1], &config, file!(), line!(), column!()).expect("There should be a log line.") + "\n");
|
||||||
|
|
||||||
|
for msg in messages
|
||||||
|
{
|
||||||
|
log_message(&msg, &config, file!(), line!(), column!());
|
||||||
|
}
|
||||||
|
|
||||||
|
let log_file = read_to_string(PathBuf::from(log_path))
|
||||||
|
.unwrap_or_else(|_| panic!("Could not read file: {log_path}"));
|
||||||
|
|
||||||
|
assert_eq!(log_file, log_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All testing concurrency in a controlled manner.
|
||||||
|
*
|
||||||
|
* When a test modifies the config according to it's requirements, another test might panic, because
|
||||||
|
* it might read the result from the changed config.
|
||||||
|
*/
|
||||||
|
#[test]
|
||||||
|
pub fn concurrency_tests()
|
||||||
|
{
|
||||||
|
log_macro_file();
|
||||||
|
log_concurrently_any_order();
|
||||||
|
log_concurrently_correct_order();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if log macro logs to file correctly.
|
||||||
|
* [`config::CONFIG`]
|
||||||
|
*/
|
||||||
|
fn log_macro_file()
|
||||||
|
{
|
||||||
|
let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4());
|
||||||
|
println!("Log Path: {log_path}");
|
||||||
|
let message = LogMessageType::GenericWarn(String::from("Test Log")).log_message();
|
||||||
|
let mut config = config::CONFIG.write().expect("Could not acquire write lock on config!");
|
||||||
|
take(&mut *config);
|
||||||
|
config.set_log_path(PathBuf::from(log_path));
|
||||||
|
drop(config);
|
||||||
|
|
||||||
|
create_dir_all(PathBuf::from(LOG_DIR.as_str()))
|
||||||
|
.unwrap_or_else(|_| panic!("Could not create directory: {}", *LOG_DIR));
|
||||||
|
|
||||||
|
log!(&message);
|
||||||
|
|
||||||
|
let log_file = read_to_string(PathBuf::from(log_path))
|
||||||
|
.unwrap_or_else(|_| panic!("Could not read file: {log_path}"));
|
||||||
|
|
||||||
|
assert_eq!(message.to_string()+"\n", log_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests concurrent logging. Log lines may be in any order.
|
||||||
|
*/
|
||||||
|
fn log_concurrently_any_order()
|
||||||
|
{
|
||||||
|
let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4());
|
||||||
|
println!("Log Path: {log_path}");
|
||||||
|
let mut config = config::CONFIG.write().expect("Could not acquire write lock on config!");
|
||||||
|
take(&mut *config);
|
||||||
|
let mut messages = Vec::with_capacity(CONCURRENT_MESSAGE_COUNT);
|
||||||
|
config.set_log_path(PathBuf::from(log_path));
|
||||||
|
drop(config);
|
||||||
|
|
||||||
|
for i in 0..CONCURRENT_MESSAGE_COUNT
|
||||||
|
{
|
||||||
|
let msg = i.to_string();
|
||||||
|
messages.push(
|
||||||
|
async {
|
||||||
|
log!(&LogMessageType::GenericWarn(msg).log_message());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
block_on(join_all(messages));
|
||||||
|
|
||||||
|
let mut num_set = HashSet::with_capacity(CONCURRENT_MESSAGE_COUNT);
|
||||||
|
for i in 0..CONCURRENT_MESSAGE_COUNT
|
||||||
|
{
|
||||||
|
num_set.insert(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for line in read_to_string(PathBuf::from(log_path)).unwrap_or_else(|_| panic!("Could not read file: {log_path}")).lines()
|
||||||
|
{
|
||||||
|
let num_str = line.split_whitespace().last().unwrap_or_else(|| panic!("Could not get message number from line: {line}"));
|
||||||
|
let num = usize::from_str_radix(num_str, 10).unwrap_or_else(|_| panic!("Could not parse number: {num_str}"));
|
||||||
|
assert!(num_set.remove(&num));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(num_set.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests concurrent logging. Log lines must be in order.
|
||||||
|
*/
|
||||||
|
fn log_concurrently_correct_order()
|
||||||
|
{
|
||||||
|
let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4());
|
||||||
|
println!("Log Path: {log_path}");
|
||||||
|
let mut config = config::CONFIG.write().expect("Could not acquire write lock on config!");
|
||||||
|
take(&mut *config);
|
||||||
|
let mut messages = Vec::with_capacity(CONCURRENT_MESSAGE_COUNT);
|
||||||
|
config.set_log_path(PathBuf::from(log_path));
|
||||||
|
drop(config);
|
||||||
|
|
||||||
|
for i in 0..CONCURRENT_MESSAGE_COUNT
|
||||||
|
{
|
||||||
|
let msg = i.to_string();
|
||||||
|
messages.push(
|
||||||
|
async {
|
||||||
|
log!(&LogMessageType::GenericWarn(msg).log_message());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
block_on(join_all(messages));
|
||||||
|
|
||||||
|
let mut num_set = HashSet::with_capacity(CONCURRENT_MESSAGE_COUNT);
|
||||||
|
for i in 0..CONCURRENT_MESSAGE_COUNT
|
||||||
|
{
|
||||||
|
num_set.insert(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, line) in read_to_string(PathBuf::from(log_path)).unwrap_or_else(|_| panic!("Could not read file: {log_path}")).lines().enumerate()
|
||||||
|
{
|
||||||
|
let num_str = line.split_whitespace().last().unwrap_or_else(|| panic!("Could not get message number from line: {line}"));
|
||||||
|
let num = usize::from_str_radix(num_str, 10).unwrap_or_else(|_| panic!("Could not parse number: {num_str}"));
|
||||||
|
assert!(num_set.remove(&num));
|
||||||
|
assert_eq!(i, num);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(num_set.len(), 0);
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user