Logging #26
@ -1,3 +1,5 @@
|
||||
workspace = { members = ["config", "logging"] }
|
||||
|
||||
[package]
|
||||
name = "wanessa"
|
||||
version = "0.1.0"
|
||||
@ -9,12 +11,13 @@ edition = "2021"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
missing_docs = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
enum_glob_use = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/enum_glob_use
|
||||
pedantic = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=pedantic
|
||||
nursery = "deny" # wip lints: https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=nursery
|
||||
unwrap_used = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/unwrap_used
|
||||
unwrap_used = "forbid" # https://rust-lang.github.io/rust-clippy/master/index.html#/unwrap_used
|
||||
missing_const_for_fn = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_const_for_fn
|
||||
missing_assert_message = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message
|
||||
missing_errors_doc = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message
|
||||
missing_assert_message = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message
|
||||
missing_errors_doc = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message
|
||||
|
23
config/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "config"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
getset = { version = "0.1.2", default-features = false }
|
||||
once_cell = { version = "1.19.0", default-features = false, features = ["std"] }
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
missing_docs = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
enum_glob_use = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/enum_glob_use
|
||||
pedantic = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=pedantic
|
||||
nursery = "deny" # wip lints: https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=nursery
|
||||
unwrap_used = "forbid" # https://rust-lang.github.io/rust-clippy/master/index.html#/unwrap_used
|
||||
missing_const_for_fn = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_const_for_fn
|
||||
missing_assert_message = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message
|
||||
missing_errors_doc = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message
|
57
config/src/db.rs
Normal file
@ -0,0 +1,57 @@
|
||||
/*!
|
||||
* This module contains everything related to [`DbConfig`].
|
||||
*/
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
use getset::Getters;
|
||||
|
||||
/**
|
||||
* A immutable record of all the information needed to connect to the SQL database.
|
||||
*/
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
#[derive(Clone, PartialEq, Eq, Getters, Debug)]
|
||||
#[getset(get = "pub")]
|
||||
pub struct DbConfig {
|
||||
/// Database connection address.
|
||||
/// Is an option to allow constructing a default config during compile time.<br>
|
||||
addr: String,
|
||||
/// Database connection port.<br>
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl Default for DbConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
addr: String::from("localhost"),
|
||||
port: 6969,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for [`DbConfig`].
|
||||
*/
|
||||
#[derive(Default, Debug)]
|
||||
pub struct DbConfigBuilder(DbConfig);
|
||||
|
||||
impl DbConfigBuilder {
|
||||
/// Get a new [`DbConfigBuilder`]
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the address to the location of the database.
|
||||
#[must_use]
|
||||
pub fn set_addr(mut self, addr: impl Into<String>) -> Self {
|
||||
self.0.addr = addr.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the port to the port the database uses.
|
||||
#[must_use]
|
||||
pub fn set_port(mut self, port: impl Into<u16>) -> Self {
|
||||
self.0.port = port.into();
|
||||
self
|
||||
}
|
||||
}
|
31
config/src/lib.rs
Normal file
@ -0,0 +1,31 @@
|
||||
/*!
|
||||
* Containing all singleton and thread-safe structs related to configuring `WANessa`.
|
||||
*
|
||||
* This crate differentiates between `Configs` and `Settings`:
|
||||
* Configs:
|
||||
* - Configs are immutable after they have been initialized. Example [`DbConfig`].
|
||||
* - Changing the config requires a restart of `WANessa`.
|
||||
*
|
||||
* Settings:
|
||||
* - Settings are mutable after they have been initialized. Example [`LogSettings`].
|
||||
* - `WANessa` always uses the current setting and does not need a restart to apply new settings.
|
||||
*/
|
||||
|
||||
pub mod db;
|
||||
pub mod log;
|
||||
|
||||
use crate::db::DbConfig;
|
||||
use crate::log::LogSettings;
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
|
||||
// TODO: replace default with parsed settings from files+flags
|
||||
|
||||
/// Singelton [`DbConfig`].
|
||||
pub static DB_CONFIG: Lazy<Arc<DbConfig>> = Lazy::new(|| Arc::new(DbConfig::default()));
|
||||
|
||||
/// Singelton [`LogSettings`].
|
||||
pub static LOG_SETTINGS: Lazy<RwLock<LogSettings>> =
|
||||
Lazy::new(|| RwLock::new(LogSettings::default()));
|
98
config/src/log.rs
Normal file
@ -0,0 +1,98 @@
|
||||
/*!
|
||||
* This module contains everything related to [`LogSettings`].
|
||||
*/
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
use std::cmp::Ordering;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::log::LogVerbosity::Warning;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use getset::Getters;
|
||||
use getset::Setters;
|
||||
|
||||
/**
|
||||
* All settings relating to how the project logs information.
|
||||
*/
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Clone, PartialEq, Eq, Getters, Setters, Debug)]
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub struct LogSettings {
|
||||
/// See [`LogVerbosity`].<br>
|
||||
verbosity: LogVerbosity,
|
||||
/// Logs UTC time and date of message, if true.<br>
|
||||
time: bool,
|
||||
/// Time and date format.<br>
|
||||
/// See [chrono](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).<br>
|
||||
time_format: String,
|
||||
/// Logs location in code, where the message was logged, if true.<br>
|
||||
location: bool,
|
||||
/// If `Some(path)` tries to also write the log to `path` in addition to stderr/stderr.<br>
|
||||
#[getset(skip)]
|
||||
path: Option<PathBuf>,
|
||||
/// Logs to standard out, if true.<br>
|
||||
stdout: bool,
|
||||
/// Logs to standard err, if true.<br>
|
||||
stderr: bool,
|
||||
}
|
||||
|
||||
impl LogSettings {
|
||||
/// Setter for log path, including syntactic sugar for the [Option] enum.
|
||||
pub fn set_path(&mut self, path: impl Into<Option<PathBuf>>) {
|
||||
self.path = path.into();
|
||||
}
|
||||
|
||||
/// Getter for the log path.
|
||||
#[must_use]
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.path.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LogSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
verbosity: Warning,
|
||||
time: false,
|
||||
time_format: String::from("%F-%T:%f"),
|
||||
location: false,
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Each level includes the previous ones.
|
||||
*/
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LogVerbosity {
|
||||
/// Critical Errors, may lead to crashes or deactivating certain features.
|
||||
Error = 10,
|
||||
/// Very minor and recovered errors, such as invalid configs.
|
||||
Warning = 20,
|
||||
/// Very verbose and detailed. Basically gives a step-by-step instruction on what is currently done.
|
||||
Information = 30,
|
||||
/// Very technical and even more verbose.
|
||||
/// May contain secrets and private information.
|
||||
/// **Do not use in production environments!**
|
||||
Debugging = 40,
|
||||
}
|
||||
|
||||
impl PartialOrd for LogVerbosity {
|
||||
/// Some operator overloading of comparison symbols (==, <,>=, etc.) as syntactic sugar.
|
||||
/// See [`PartialOrd`].
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for LogVerbosity {
|
||||
/// Some operator overloading of comparison symbols (==, <,>=, etc.) as syntactic sugar.
|
||||
/// See [`Ord`].
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
(*self as usize).cmp(&(*other as usize))
|
||||
}
|
||||
}
|
28
logging/Cargo.toml
Normal file
@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "logging"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.34", default-features = false, features = ["now"] }
|
||||
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]
|
||||
unsafe_code = "forbid"
|
||||
missing_docs = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
enum_glob_use = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/enum_glob_use
|
||||
pedantic = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=pedantic
|
||||
nursery = "deny" # wip lints: https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=nursery
|
||||
unwrap_used = "forbid" # https://rust-lang.github.io/rust-clippy/master/index.html#/unwrap_used
|
||||
missing_const_for_fn = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_const_for_fn
|
||||
missing_assert_message = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message
|
||||
missing_errors_doc = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message
|
195
logging/src/lib.rs
Normal file
@ -0,0 +1,195 @@
|
||||
/*!
|
||||
* This module handles logging messages asynchronously and is thread-safe.
|
||||
* It should mostly be called statically using the [`log!`] macro.
|
||||
*/
|
||||
|
||||
mod test;
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
|
||||
use config::log::LogSettings;
|
||||
use config::log::LogVerbosity;
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use LogVerbosity::Information;
|
||||
use LogVerbosity::Warning;
|
||||
|
||||
use LogMessageType::GenericDebug;
|
||||
use LogMessageType::GenericErr;
|
||||
use LogMessageType::GenericInfo;
|
||||
use LogMessageType::GenericWarn;
|
||||
|
||||
/**
|
||||
* Logs the given message.
|
||||
*/
|
||||
pub fn log_message(
|
||||
msg: &LogMessage,
|
||||
conf: &LogSettings,
|
||||
file: &str,
|
||||
line: u32,
|
||||
column: u32,
|
||||
) -> Result<(), io::Error> {
|
||||
// Check if message may be logged according to config.
|
||||
let Some(log_line) = log_to_str(msg, conf, file, line, column) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Log to file
|
||||
match conf.path().as_ref() {
|
||||
None => { /* Do not log to file */ }
|
||||
Some(p) => {
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.append(true)
|
||||
.create(true)
|
||||
leon
commented
While logging is not a dependency of the program's main function, it is a crucial part of administrating it. If the program cannot log to stdout because the feature was disabled by the admin or due to some problem, it could lead to an edge case-where the program has no logging and cannot inform the sysadmin about errors. Although I see how panicking is not the best solution. A controlled shutdown is preferable and a config Option allowing the sysadmin to set a limit on how many log-messages may be dropped within x time, without the service shutting down. > logging/src/lib.rs:46 and logging/src/lib.rs:48 will panic if an IO error occurs. Maybe consider to not panic and print another error to the console, informing about the writing problems, rather than crashing the program as logging is not a crucial feature for the program's functioning
While logging is not a dependency of the program's main function, it is a crucial part of administrating it. If the program cannot log to stdout because the feature was disabled by the admin or due to some problem, it could lead to an edge case-where the program has no logging and cannot inform the sysadmin about errors.
Although I see how panicking is not *the best* solution. A controlled shutdown is preferable and a config Option allowing the sysadmin to set a limit on how many log-messages may be dropped within x time, without the service shutting down.
@hendrik
hendrik
commented
If the stdout was disabled, panicking wouldn't tell the admin about the error either. The whole program shutting down is not a good indicator for a failure, if not shutting down is a safe alternative. The program could also notify the admin via CLI, Internet, Email or many other ways that something went wrong while still keeping up its full functionality (as from the perspective of the user). If the stdout was disabled, panicking wouldn't tell the admin about the error either. The whole program shutting down is not a good indicator for a failure, if not shutting down is a safe alternative. The program could also notify the admin via CLI, Internet, Email or many other ways that something went wrong while still keeping up its full functionality (as from the perspective of the user).
leon
commented
The error in this case is the inability to report about warnings. This edge-case happens, when there is absolutely no way of informing the sysadmin of other errors, including CLI and E-Mail, etc. I would suggest the following:
But until then, I will remove the panic and spit something into stderr, ignoring the stderr setting. The error in this case is the inability to report about warnings. This edge-case happens, when there is absolutely no way of informing the sysadmin of other errors, including CLI and E-Mail, etc.
Let's assume the program detects something which could to serious data-loss in the future. The program should warn the sysadmin about it, but not yet shut down, unless the error is possibly imminent. But if all configured ways of logging and communicating with the sysadmin fail, the service should try to shut down to prevent data loss or other unintended behavior.
I would suggest the following:
- Introduce a serious-warning verbosity between error and warning for soon-to-be errors
- implementing more log channels, such as E-Mail
- implementing some sort of unwind&shutdown macro/function as a better alternative to panic!(), which would generally be useful
- introduce a config-attribute, which determines if the service may be shut down due to errors in logging:
- What verbosity must the log at least have?
- How many log failures may happen?
- In what amount of time?
But until then, I will remove the panic and spit something into stderr, ignoring the stderr setting.
leon
commented
See See e4baaa5f45
|
||||
.open(p);
|
||||
let mut file = match file {
|
||||
Ok(f) => f,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
match writeln!(file, "{log_line}") {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if msg.1 <= Warning && *conf.stderr() {
|
||||
let mut stdout = io::stdout().lock();
|
||||
return writeln!(stdout, "{log_line}");
|
||||
} else if msg.1 >= Information && *conf.stdout() {
|
||||
let mut stderr = io::stderr().lock();
|
||||
return writeln!(stderr, "{log_line}");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return log line, if message may be logged according to [`config::log::LogSettings`].
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn log_to_str(
|
||||
msg: &LogMessage,
|
||||
conf: &LogSettings,
|
||||
file: &str,
|
||||
line: u32,
|
||||
column: u32,
|
||||
) -> Option<String> {
|
||||
if conf.verbosity() < &msg.1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut log_line = String::new();
|
||||
|
||||
// add time substring
|
||||
if *conf.time() {
|
||||
log_line += &format!("{} ", Utc::now().format(conf.time_format()));
|
||||
}
|
||||
|
||||
// add code location substring
|
||||
if *conf.location() {
|
||||
log_line += &format!("{file}:{line},{column} ");
|
||||
}
|
||||
|
||||
Some(log_line + &msg.to_string())
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand version for [`log_message`], which does not require information about where in the code
|
||||
leon
commented
Fair, although getting a mut ref on an Arc would require some while-looping everytime. The same question about read-only config applies here: When this function is stable, it might be used to clear the poison: > logging/src/lib.rs:102 will panic if any code requests write-lock on config::CONFIG and then panics. Consider using Arc to ensure immutability and accessibility to the configuration from any thread at any time
Fair, although getting a mut ref on an Arc would require some while-looping everytime.
The same question about read-only config applies here:
https://git.libre.moe/KomuSolutions/WANessa/pulls/26/files#issuecomment-1519
When this function is stable, it might be used to clear the poison:
https://doc.rust-lang.org/std/sync/struct.RwLock.html#method.clear_poison
leon
commented
@hendrik
hendrik
commented
Assuming the config is read-only, we could also use an Arc without while-looping everytime by using the Arc::clone() function. Also using Arcs would defeat the whole problem of poisoning due to no code being able to request a write-lock. Assuming the config is read-only, we could also use an Arc without while-looping everytime by using the Arc::clone() function. Also using Arcs would defeat the whole problem of poisoning due to no code being able to request a write-lock.
@leon
leon
commented
While not a read lock, the Arc Type has this, which seems to be at odds with the whole immutability thing. To replace the compile-time default config with the runtime sysadmin config, we need to get a *mut reference for While not a read lock, the Arc Type has this, which seems to be at odds with the whole immutability thing.
https://doc.rust-lang.org/std/sync/struct.Arc.html#method.get_mut
The docs don't seem to define what happens after you get the &mut and then panic before dropping the reference, which seems sketchy to me
To replace the compile-time default config with the runtime sysadmin config, we need to get a *mut reference for [`mem::swap`](https://doc.rust-lang.org/std/mem/fn.swap.html) to work. Otherwise, we can only have the default values, which defeats the point of a config.
leon
commented
Or alternatively, we could use this crate I once used in a test: It would also make DEFAULT more sensible, since it can now use strings and other types of variable length. Or alternatively, we could use this crate I once used in a test:
https://docs.rs/once_cell/latest/once_cell/sync/struct.Lazy.html
It would also make DEFAULT more sensible, since it can now use strings and other types of variable length.
leon
commented
See TODO: Poison checking. See a787dd93e5
TODO: Poison checking.
|
||||
* the command was called from.
|
||||
* Tries to aqcuire a read lock, if a reference to an instance of [`config::log::LogSettings`] is
|
||||
* not given.
|
||||
*/
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($msg:expr) => {
|
||||
let conf = config::LOG_SETTINGS
|
||||
.read()
|
||||
.unwrap_or_else(|_| panic!("Failed aqcuire read lock on config!"));
|
||||
let res = log_message($msg, &*conf, file!(), line!(), column!());
|
||||
drop(conf);
|
||||
res
|
||||
};
|
||||
($msg:expr, $config:expr) => {
|
||||
log_message($msg, $config, file!(), line!(), column!())
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A named typle assigning a log [`LogVerbosity`] to a [`LogMessageType`].
|
||||
*/
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct LogMessage(LogMessageType, LogVerbosity);
|
||||
|
||||
impl Display for LogMessage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
self.0.fmt(f) // just display LogMessageType
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Evaluate replacing the following with a trait, to decouple this component from the rest.
|
||||
|
||||
/**
|
||||
* Every possible Message, which may be logged. Grouped the following:
|
||||
*
|
||||
* These groups correspond to the four levels of logging verbosity. See [`LogVerbosity`].
|
||||
*/
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LogMessageType {
|
||||
/// Errors
|
||||
/// An error of any type; please avoid using this.
|
||||
GenericErr(String),
|
||||
/// Warnings
|
||||
/// A warning of any type; please avoid using this.
|
||||
GenericWarn(String),
|
||||
/// Info
|
||||
/// Some information of any type; please avoid using this.
|
||||
GenericInfo(String),
|
||||
/// Debug
|
||||
/// a debug message of any type; please avoid using this.
|
||||
GenericDebug(String),
|
||||
}
|
||||
|
||||
/*
|
||||
* Please don't put anything other except new match arms below this comment, since it is expected that
|
||||
* the following code will have a lot of lines, due to exhausting all possible enum values
|
||||
* respectively and (hopefully) detailed messages.
|
||||
*/
|
||||
|
||||
impl LogMessageType {
|
||||
/// Returns a new [`LogMessage`] based on the default verbosity of the given [`LogMessageType`].
|
||||
#[must_use]
|
||||
pub fn log_message(&self) -> LogMessage {
|
||||
let verbosity = match self {
|
||||
GenericErr(_) => LogVerbosity::Error,
|
||||
GenericWarn(_) => LogVerbosity::Warning,
|
||||
GenericInfo(_) => LogVerbosity::Information,
|
||||
GenericDebug(_) => LogVerbosity::Debugging,
|
||||
};
|
||||
|
||||
LogMessage(self.clone(), verbosity)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LogMessageType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
GenericErr(err) => {
|
||||
write!(f, "Generic Error: {err}")
|
||||
}
|
||||
GenericWarn(warn) => {
|
||||
write!(f, "Generic Warning: {warn}")
|
||||
}
|
||||
GenericInfo(info) => {
|
||||
write!(f, "Generic Information: {info}")
|
||||
}
|
||||
GenericDebug(debug) => {
|
||||
write!(f, "Generic Debug Message: {debug}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
303
logging/src/test.rs
Normal file
@ -0,0 +1,303 @@
|
||||
#![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 the macro logs properly with a given config.
|
||||
#[test]
|
||||
pub fn log_macro_given_config() {
|
||||
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::log::LogSettings::default();
|
||||
config.set_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, &config);
|
||||
|
||||
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`] 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::log::LogSettings::default();
|
||||
config.set_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, &config);
|
||||
|
||||
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::log::LogSettings::default();
|
||||
config.set_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, &config);
|
||||
|
||||
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::log::LogSettings::default();
|
||||
config.set_path(PathBuf::from(log_path));
|
||||
config.set_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!(&msg, &config);
|
||||
}
|
||||
|
||||
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::log::LogSettings::default();
|
||||
config.set_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!(&msg, &config);
|
||||
}
|
||||
|
||||
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_shared_config();
|
||||
log_concurrently_any_order();
|
||||
log_concurrently_correct_order();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if log macro logs to file correctly.
|
||||
* [`config::CONFIG`]
|
||||
*/
|
||||
fn log_macro_shared_config() {
|
||||
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::LOG_SETTINGS
|
||||
.write()
|
||||
.expect("Could not acquire write lock on config!");
|
||||
take(&mut *config);
|
||||
config.set_path(PathBuf::from(log_path));
|
||||
config.set_stdout(false);
|
||||
config.set_stderr(false);
|
||||
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::LOG_SETTINGS
|
||||
.write()
|
||||
.expect("Could not acquire write lock on config!");
|
||||
take(&mut *config);
|
||||
let mut messages = Vec::with_capacity(CONCURRENT_MESSAGE_COUNT);
|
||||
config.set_path(PathBuf::from(log_path));
|
||||
config.set_stdout(false);
|
||||
config.set_stderr(false);
|
||||
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 = num_str
|
||||
.parse()
|
||||
.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::LOG_SETTINGS
|
||||
.write()
|
||||
.expect("Could not acquire write lock on config!");
|
||||
take(&mut *config);
|
||||
let mut messages = Vec::with_capacity(CONCURRENT_MESSAGE_COUNT);
|
||||
config.set_path(PathBuf::from(log_path));
|
||||
config.set_stdout(false);
|
||||
config.set_stderr(false);
|
||||
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 = num_str
|
||||
.parse()
|
||||
.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);
|
||||
}
|
I considered this.
I just thought that setters are more convenient than using
mem::swap
or*config = Config::new(...)
. I also was not sure, if the config is truly read-only since we never specified if hot-reloading the config is a desired feature. Depending on how the CLI is implemented, hot-applying config-changes might also be a necessity to disable stdout logging, which would pollute the CLIs visuals.@hendrik
I assumed the config would be read only, because some attribute changes after initialization could easily cause unexpected problems. For example, changing the database server at runtime is not something the current code is designed for, nor would it be a sensible feature to implement. If the log-settings should be mutable after the initialization phase, we should add a mutable reference to some "LogPreferences" structure to our Config instead of generally supporting hot-reload for the whole Config
@leon
Seems sensible. It is also probably a good idea to split the config struct into multiple parts.
See
a787dd93e5