use super::defaults::{
default_enable_file_logging, default_log_dir, default_log_filename, default_log_format,
default_rotation,
};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Serialize, Clone)]
pub struct LoggingConfig {
#[serde(default = "default_log_dir")]
pub dir: String,
#[serde(default = "default_log_filename")]
pub filename: String,
#[serde(default = "default_rotation")]
pub rotation: String,
#[serde(default)]
pub level: Option<LogLevel>,
#[serde(default = "default_enable_file_logging")]
pub enable_file_logging: bool,
#[serde(default = "default_log_format")]
pub format: LogFormat,
}
impl<'de> Deserialize<'de> for LoggingConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct LoggingConfigHelper {
#[serde(default = "default_log_dir")]
dir: String,
#[serde(default = "default_log_filename")]
filename: String,
#[serde(default = "default_rotation")]
rotation: String,
#[serde(default)]
level: Option<serde_json::Value>,
#[serde(default = "default_enable_file_logging")]
enable_file_logging: bool,
#[serde(default = "default_log_format")]
format: LogFormat,
}
let helper = LoggingConfigHelper::deserialize(deserializer)?;
let level = helper.level.and_then(|value| {
if value.is_string() {
let level_str = value.as_str().unwrap();
match level_str.trim().to_lowercase().as_str() {
"trace" => Some(LogLevel::Trace),
"debug" => Some(LogLevel::Debug),
"info" => Some(LogLevel::Info),
"warn" | "warning" => Some(LogLevel::Warn),
"error" | "err" => Some(LogLevel::Error),
_ => {
eprintln!("Invalid log level '{level_str}', using default");
None
}
}
} else if value.is_array() {
value
.as_array()
.and_then(|arr| arr.first())
.and_then(|first| first.as_str())
.and_then(|s| match s.trim().to_lowercase().as_str() {
"trace" => Some(LogLevel::Trace),
"debug" => Some(LogLevel::Debug),
"info" => Some(LogLevel::Info),
"warn" | "warning" => Some(LogLevel::Warn),
"error" | "err" => Some(LogLevel::Error),
_ => None,
})
} else {
None
}
});
Ok(Self {
dir: helper.dir,
filename: helper.filename,
rotation: helper.rotation,
level,
enable_file_logging: helper.enable_file_logging,
format: helper.format,
})
}
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
dir: default_log_dir(),
filename: default_log_filename(),
rotation: default_rotation(),
level: None,
enable_file_logging: default_enable_file_logging(),
format: default_log_format(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Trace => "trace",
Self::Debug => "debug",
Self::Info => "info",
Self::Warn => "warn",
Self::Error => "error",
}
}
}
impl<'de> Deserialize<'de> for LogLevel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let s = s.trim().to_lowercase();
match s.as_str() {
"trace" => Ok(Self::Trace),
"debug" => Ok(Self::Debug),
"info" => Ok(Self::Info),
"warn" | "warning" => Ok(Self::Warn),
"error" | "err" => Ok(Self::Error),
other => Err(serde::de::Error::custom(format!(
"invalid log level '{other}', expected one of: trace, debug, info, warn, error"
))),
}
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum LogFormat {
#[default]
Json,
Text,
}