Logging #26

Open
leon wants to merge 33 commits from Logging into main
2 changed files with 175 additions and 163 deletions
Showing only changes of commit f3b061a637 - Show all commits

View File

@ -16,92 +16,105 @@ use getset::Setters;
* Represents a valid `WANessa` configuration. Intended as a read-only singleton.
* See [`DEFAULTS`]
*/
#[derive(Clone,PartialEq, Eq)]
#[derive(Getters, Setters)]
#[getset(get = "pub")]
#[derive(Clone, PartialEq, Eq, Getters, Setters)]
#[getset(get = "pub", set = "pub")]
#[allow(clippy::struct_excessive_bools)] // False positive, since it is a config struct.
pub struct Config {
/// See [`LogVerbosity`].<br>
Outdated
Review

-> Maybe just remove the setters and use an initializer to ensure immutability

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

> -> Maybe just remove the setters and use an initializer to ensure immutability 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

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
Outdated
Review

Seems sensible. It is also probably a good idea to split the config struct into multiple parts.

Seems sensible. It is also probably a good idea to split the config struct into multiple parts.
Outdated
Review
See a787dd93e5
/// Default: [`Warning`]
pub log_verbosity: LogVerbosity,
/// Default: [`Warning`]
log_verbosity: LogVerbosity,
/// Logs UTC time and date of message, if true.<br>
/// Default: `false`
pub log_time: bool,
/// Time and date format.<br>
/// Defaults to `%F/%T:%f`.<br>
/// See [chrono](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).<br>
/// Default example : `2001-12-31/23:59:33:26490000`
/// Default: `false`
log_time: bool,
/// Time and date format.<br>
/// Defaults to `%F/%T:%f`.<br>
/// See [chrono](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).<br>
/// Default example : `2001-12-31/23:59:33:26490000`
#[getset(skip)]
pub log_time_format: Option<String>,
log_time_format: Option<String>,
/// Logs location in code, where the message was logged, if true.<br>
/// Default: `false`
pub log_location: bool,
/// If `Some(path)` tries to also write the log to `path` in addition to stderr/stderr.<br>
/// Default: [None]
pub log_path: Option<PathBuf>,
/// Logs to standard out, if true.<br>
/// Default: `true`
pub log_stdout: bool,
/// Logs to standard err, if true.<br>
/// Default: `true`
pub log_stderr: bool,
/// Default: `false`
log_location: bool,
/// If `Some(path)` tries to also write the log to `path` in addition to stderr/stderr.<br>
/// Default: [None]
log_path: Option<PathBuf>,
/// Logs to standard out, if true.<br>
/// Default: `true`
log_stdout: bool,
/// Logs to standard err, if true.<br>
/// Default: `true`
log_stderr: bool,
/// Database connection address.
/// Is an option to allow constructing a default config during compile time.<br>
/// Default: `localhost`.
/// Is an option to allow constructing a default config during compile time.<br>
/// Default: `localhost`.
#[getset(skip)]
pub db_addr: Option<String>,
db_addr: Option<String>,
/// Database connection port.<br>
/// Default: `6969` (nice).
pub db_port: u16,
/// Default: `6969` (nice).
db_port: u16,
}
impl Config {
/// Getter for [`Self::db_addr`].
/// Getter for [`Self::db_addr`].
#[must_use]
pub fn db_addr(&self) -> &str {
self.db_addr.as_ref().map_or("localhost", |addr| addr)
self.db_addr.as_ref().map_or("localhost", |addr| addr)
}
/// Getter for the [`Self::log_time_format`].
pub fn log_time_format(&self) -> &str
{
self.log_time_format.as_ref().map_or("%F-%T:%f", |fmt| fmt)
}
/// Setter for [`Self::db_addr`].
pub fn set_db_addr(&mut self, db_addr: impl Into<String>) {
self.db_addr = Some(db_addr.into());
}
/// Getter for [`Self::log_time_format`].
#[must_use]
pub fn log_time_format(&self) -> &str {
todo!();
// self.log_time_format.as_ref().map_or("%F-%T:%f", |fmt| fmt)
}
/// Setter for [`Self::log_time_format`].
pub fn set_log_time_format(&mut self, format: impl Into<String>) {
self.log_time_format = Some(format.into());
}
}
/// See [`DEFAULTS`].
impl Default for Config {
/// See [`DEFAULTS`].
/// See [`DEFAULTS`].
fn default() -> Self {
DEFAULTS
}
}
/// Default configuration.
/// ```rust
/// # use config::Config;
/// # use config::LogVerbosity::Warning;
/// let DEFAULTS = Config
/// {
/// log_verbosity: Warning,
/// log_time: false,
/// log_time_format: None,
/// log_location: false,
/// log_stdout: true,
/// log_stderr: true,
/// log_path: None,
/// db_addr: None,
/// db_port: 6969,
/// };
/// # assert!(DEFAULTS == config::DEFAULTS)
/// ```
/**
* Default configuration.
* ```rust
* # use config::Config;
* # use config::LogVerbosity::Warning;
* let DEFAULTS = Config
* {
* log_verbosity: Warning,
* log_time: false,
* log_time_format: None,
* log_location: false,
* log_stdout: true,
* log_stderr: true,
* log_path: None,
* db_addr: None,
* db_port: 6969,
* };
* # assert!(DEFAULTS == config::DEFAULTS)
* ```
*/
pub const DEFAULTS: Config = Config {
log_verbosity: Warning,
log_time: false,
log_time_format: None,
log_time_format: None,
log_location: false,
log_stdout: true,
log_stderr: true,
log_path: None,
log_stdout: true,
log_stderr: true,
log_path: None,
db_addr: None,
db_port: 6969,
};
@ -113,36 +126,31 @@ pub static CONFIG: RwLock<Config> = RwLock::new(DEFAULTS);
* 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,
/// Very minor and recovered errors, such as invalid configs.
Warning,
/// Very verbose and detailed. Basically gives a step-by-step instruction on what is currently done.
Information,
/// Very technical and even more verbose.
/// May contain secrets and private information.
/// **Do not use in production environments!**
Debugging,
pub enum LogVerbosity {
/// Critical Errors, may lead to crashes or deactivating certain features.
Error,
/// Very minor and recovered errors, such as invalid configs.
Warning,
/// Very verbose and detailed. Basically gives a step-by-step instruction on what is currently done.
Information,
/// Very technical and even more verbose.
/// May contain secrets and private information.
/// **Do not use in production environments!**
hendrik marked this conversation as resolved Outdated
Outdated
Review

LogVerbosity doesn't specify values, but logging/src/lib.rs:75 assumes order
-> Accidental reordering of the enum would lead to errors

It is mentioned here, that each level includes the previous ones, but including explicit ordinals is still a good idea.

> LogVerbosity doesn't specify values, but logging/src/lib.rs:75 assumes order > -> Accidental reordering of the enum would lead to errors It is mentioned here, that each level includes the previous ones, but including explicit ordinals is still a good idea.
Outdated
Review
@hendrik
Outdated
Review
See 6e73c2a7b7
Debugging,
}
impl PartialOrd for LogVerbosity
{
/// Some operator overloading of comparison symbols (==, <,>=, etc.) as syntactic sugar.
/// See [`PartialOrd`].
fn partial_cmp(&self, other: &Self) -> Option<Ordering>
{
(*self as usize).partial_cmp(&(*other as usize))
}
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))
}
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))
}
}

View File

@ -11,13 +11,13 @@ use config::LogVerbosity;
use chrono::Utc;
use LogVerbosity::Warning;
use LogVerbosity::Information;
use LogVerbosity::Warning;
use LogMessageType::GenericErr;
use LogMessageType::GenericWarn;
use LogMessageType::GenericInfo;
use LogMessageType::GenericDebug;
use LogMessageType::GenericErr;
use LogMessageType::GenericInfo;
use LogMessageType::GenericWarn;
/**
* Logs the given message.
@ -27,73 +27,73 @@ use LogMessageType::GenericDebug;
* 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, file: &str, line: &str, column: &str)
{
let conf = config::CONFIG.read().expect(&format!("Failed aqcuire read lock on config\nTried to log message: {msg}"));
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 { return; }
if conf.log_verbosity() < &msg.1 {
return;
}
let mut log_line = String::new();
let mut log_line = String::new();
// add time substring
if *conf.log_time()
{
log_line += &format!("{} ", Utc::now().format(conf.log_time_format()));
}
// 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} ");
}
// add code location substring
if *conf.log_location() {
log_line += &format!("{file}:{line},{column} ");
Outdated
Review

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

> 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

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).
Outdated
Review

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.

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.
Outdated
Review
See e4baaa5f45
}
log_line += &msg.to_string();
log_line += &msg.to_string();
// May panic if file cannot be opened or written to.
conf.log_path().as_ref().map_or_else(
|| {}, |path|
{
let mut file = OpenOptions::new()
.write(true)
.append(true)
.open(path)
.expect(
&format!("Could not open log file: {path:#?}")
);
writeln!(file, "{log_line}").expect(&format!("Could not write log to file: {path:#?}"));
}
);
// May panic if file cannot be opened or written to.
conf.log_path().as_ref().map_or_else(
|| {},
|path| {
let mut file = OpenOptions::new()
.write(true)
.append(true)
.open(path)
.unwrap_or_else(|_| {
panic!(
"Could not open log fil
e: {path:#?}"
)
});
writeln!(file, "{log_line}")
.unwrap_or_else(|_| panic!("Could not write log to file: {path:#?}"));
},
);
if msg.1 <= Warning && *conf.log_stderr()
{
// May panic if writing to stderr fails.
eprintln!("{log_line}");
}
else if msg.1 >= Information && *conf.log_stdout()
{
// May panic if writing to stdout fails.
println!("{log_line}");
}
if msg.1 <= Warning && *conf.log_stderr() {
// May panic if writing to stderr fails.
eprintln!("{log_line}");
} else if msg.1 >= Information && *conf.log_stdout() {
// May panic if writing to stdout fails.
println!("{log_line}");
}
drop(conf); // remove read lock
drop(conf); // remove read lock
}
/**
* A named typle assigning a log [`LogVerbosity`] to a [`LogMessageType`]
* A named typle assigning a log [`LogVerbosity`] to a [`LogMessageType`]
*/
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
}
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
}
}
/**
* 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)]
@ -118,32 +118,36 @@ pub enum LogMessageType {
* respectively and (hopefully) detailed messages.
*/
impl LogMessageType
{
/// Returns a new [`LogMessage`] based on the default verbosity of the given [`LogMessageType`].
pub fn log_message(&self) -> LogMessage
{
let verbosity = match self
{
GenericErr(_) => LogVerbosity::Error,
GenericWarn(_) => LogVerbosity::Warning,
GenericInfo(_) => LogVerbosity::Information,
GenericDebug(_) => LogVerbosity::Debugging,
};
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)
}
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}") },
}
}
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}")
}
}
}
}