Logging #26
10
logging/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[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"}
|
149
logging/src/lib.rs
Normal file
@ -0,0 +1,149 @@
|
||||
/*!
|
||||
* This module handles logging messages asynchronously and is thread-safe.
|
||||
* It should mostly be called statically.
|
||||
*/
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
|
||||
use config::LogVerbosity;
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use LogVerbosity::Warning;
|
||||
use LogVerbosity::Information;
|
||||
|
||||
use LogMessageType::GenericErr;
|
||||
use LogMessageType::GenericWarn;
|
||||
use LogMessageType::GenericInfo;
|
||||
use LogMessageType::GenericDebug;
|
||||
|
||||
/**
|
||||
* Logs the given message.
|
||||
*
|
||||
* # Panics
|
||||
* Panics if readlock on [`config::CONFIG`] could not be acquired
|
||||
* 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}"));
|
||||
|
||||
if conf.log_verbosity() < &msg.1 { 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.
|
||||
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:#?}"));
|
||||
}
|
||||
);
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)]
|
||||
pub enum LogMessageType {
|
||||
/// Errors
|
||||
/// An error of any type; please avoid using this.
|
||||
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.
|
||||
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`].
|
||||
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}") },
|
||||
}
|
||||
}
|
||||
}
|
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).
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:
But until then, I will remove the panic and spit something into stderr, ignoring the stderr setting.
See
e4baaa5f45