use tracing::info;
use tracing_subscriber::reload::Handle;
use tracing_subscriber::EnvFilter;
use crate::errors::AppError;
use crate::services::SettingsService;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"trace" => Some(LogLevel::Trace),
"debug" => Some(LogLevel::Debug),
"info" => Some(LogLevel::Info),
"warn" | "warning" => Some(LogLevel::Warn),
"error" => Some(LogLevel::Error),
_ => None,
}
}
pub fn as_tracing_level(&self) -> &'static str {
match self {
LogLevel::Trace => "trace",
LogLevel::Debug => "debug",
LogLevel::Info => "info",
LogLevel::Warn => "warn",
LogLevel::Error => "error",
}
}
}
pub struct LoggingService {
reload_handle: Handle<EnvFilter, tracing_subscriber::Registry>,
default_filter: String,
}
impl LoggingService {
pub fn new(
reload_handle: Handle<EnvFilter, tracing_subscriber::Registry>,
default_filter: String,
) -> Self {
Self {
reload_handle,
default_filter,
}
}
fn build_filter(&self, level: LogLevel) -> EnvFilter {
let level_str = level.as_tracing_level();
let filter_str = format!(
"cedros_login={},tower_http={},axum={}",
level_str, level_str, level_str
);
EnvFilter::try_new(&filter_str).unwrap_or_else(|_| {
EnvFilter::try_new(&self.default_filter)
.expect("Default filter should always be valid")
})
}
pub fn set_level(&self, level: LogLevel) -> Result<(), AppError> {
let filter = self.build_filter(level);
self.reload_handle.reload(filter).map_err(|e| {
AppError::Internal(anyhow::anyhow!("Failed to reload log filter: {}", e))
})?;
info!(level = ?level, "Log level updated");
Ok(())
}
pub async fn apply_from_settings(&self, settings: &SettingsService) -> Result<(), AppError> {
let level_str = settings.get("server_log_level").await?;
let level = level_str
.as_deref()
.and_then(LogLevel::parse)
.unwrap_or(LogLevel::Info);
self.set_level(level)?;
if let Some(format) = settings.get("server_log_format").await? {
if format != "json" {
info!(
format = %format,
"Note: Log format can only be changed at server restart"
);
}
}
Ok(())
}
pub fn default_filter(&self) -> &str {
&self.default_filter
}
}
pub fn init_logging(default_filter: &str) -> LoggingService {
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
let initial_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::try_new(default_filter).expect("Invalid default filter"));
let (filter, reload_handle) = tracing_subscriber::reload::Layer::new(initial_filter);
tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer())
.init();
LoggingService::new(reload_handle, default_filter.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_level_parsing() {
assert_eq!(LogLevel::parse("trace"), Some(LogLevel::Trace));
assert_eq!(LogLevel::parse("DEBUG"), Some(LogLevel::Debug));
assert_eq!(LogLevel::parse("Info"), Some(LogLevel::Info));
assert_eq!(LogLevel::parse("WARN"), Some(LogLevel::Warn));
assert_eq!(LogLevel::parse("warning"), Some(LogLevel::Warn));
assert_eq!(LogLevel::parse("error"), Some(LogLevel::Error));
assert_eq!(LogLevel::parse("invalid"), None);
}
#[test]
fn test_log_level_to_tracing() {
assert_eq!(LogLevel::Trace.as_tracing_level(), "trace");
assert_eq!(LogLevel::Debug.as_tracing_level(), "debug");
assert_eq!(LogLevel::Info.as_tracing_level(), "info");
assert_eq!(LogLevel::Warn.as_tracing_level(), "warn");
assert_eq!(LogLevel::Error.as_tracing_level(), "error");
}
}