use std::path::PathBuf;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
#[derive(Clone, Debug, Default)]
pub enum LogRotation {
#[default]
Daily,
Hourly,
Minutely,
Never,
}
impl LogRotation {
fn to_rotation(&self) -> Rotation {
match self {
LogRotation::Daily => Rotation::DAILY,
LogRotation::Hourly => Rotation::HOURLY,
LogRotation::Minutely => Rotation::MINUTELY,
LogRotation::Never => Rotation::NEVER,
}
}
}
#[derive(Clone, Debug)]
pub struct LogConfig {
pub log_dir: PathBuf,
pub log_file_prefix: String,
pub rotation: LogRotation,
pub max_files: Option<usize>,
pub level: String,
pub stdout: bool,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
log_dir: PathBuf::from("logs"),
log_file_prefix: "batata-client".to_string(),
rotation: LogRotation::Daily,
max_files: Some(7),
level: "info".to_string(),
stdout: true,
}
}
}
impl LogConfig {
pub fn new(log_dir: impl Into<PathBuf>) -> Self {
Self {
log_dir: log_dir.into(),
..Default::default()
}
}
pub fn with_level(mut self, level: &str) -> Self {
self.level = level.to_string();
self
}
pub fn with_rotation(mut self, rotation: LogRotation) -> Self {
self.rotation = rotation;
self
}
pub fn with_max_files(mut self, max_files: usize) -> Self {
self.max_files = Some(max_files);
self
}
pub fn with_prefix(mut self, prefix: &str) -> Self {
self.log_file_prefix = prefix.to_string();
self
}
pub fn with_stdout(mut self, enabled: bool) -> Self {
self.stdout = enabled;
self
}
pub fn init(&self) -> Result<LogGuard, Box<dyn std::error::Error + Send + Sync>> {
std::fs::create_dir_all(&self.log_dir)?;
let mut builder = RollingFileAppender::builder()
.rotation(self.rotation.to_rotation())
.filename_prefix(&self.log_file_prefix)
.filename_suffix("log");
if let Some(max_files) = self.max_files {
builder = builder.max_log_files(max_files);
}
let file_appender = builder.build(&self.log_dir)?;
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(&self.level));
let file_layer = tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false)
.with_target(true)
.with_thread_ids(true);
let subscriber = tracing_subscriber::registry()
.with(filter)
.with(file_layer);
if self.stdout {
let stdout_layer = tracing_subscriber::fmt::layer()
.with_target(true)
.with_thread_ids(false);
subscriber.with(stdout_layer).init();
} else {
subscriber.init();
}
Ok(LogGuard { _guard: guard })
}
pub fn init_file_only(&self) -> Result<LogGuard, Box<dyn std::error::Error + Send + Sync>> {
let mut config = self.clone();
config.stdout = false;
config.init()
}
}
pub struct LogGuard {
_guard: WorkerGuard,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_config_defaults() {
let config = LogConfig::default();
assert_eq!(config.log_dir, PathBuf::from("logs"));
assert_eq!(config.log_file_prefix, "batata-client");
assert_eq!(config.level, "info");
assert!(config.stdout);
assert_eq!(config.max_files, Some(7));
}
#[test]
fn test_log_config_builder() {
let config = LogConfig::new("/var/log/test")
.with_level("debug")
.with_rotation(LogRotation::Hourly)
.with_max_files(10)
.with_prefix("my-app")
.with_stdout(false);
assert_eq!(config.log_dir, PathBuf::from("/var/log/test"));
assert_eq!(config.level, "debug");
assert_eq!(config.log_file_prefix, "my-app");
assert!(!config.stdout);
assert_eq!(config.max_files, Some(10));
}
#[test]
fn test_log_rotation_conversion() {
assert!(matches!(LogRotation::Daily.to_rotation(), Rotation::DAILY));
assert!(matches!(LogRotation::Hourly.to_rotation(), Rotation::HOURLY));
assert!(matches!(
LogRotation::Minutely.to_rotation(),
Rotation::MINUTELY
));
assert!(matches!(LogRotation::Never.to_rotation(), Rotation::NEVER));
}
}