#[allow(warnings)]
#[cfg(feature = "logger")]
pub mod logger {
use chrono::{Local, NaiveDate, TimeDelta, TimeZone};
use std::fs;
use std::path::{Path, PathBuf};
use time::{format_description::parse, UtcOffset};
use tracing_appender::{non_blocking, rolling};
use tracing_error::ErrorLayer;
use tracing_subscriber::{
filter::EnvFilter,
fmt,
fmt::time::OffsetTime,
layer::SubscriberExt,
registry::Registry,
util::SubscriberInitExt,
};
pub use tracing::*;
pub use tracing_appender::*;
pub use tracing_appender;
pub use tracing_subscriber::*;
pub use tracing_subscriber;
pub use tracing_error::*;
pub use tracing_error;
const DEFAULT_LOG_RETENTION_DAYS: u64 = 14;
pub fn init_debug() {
if let Err(e) = clean_old_logs("log", "app", "log", rolling::Rotation::DAILY, DEFAULT_LOG_RETENTION_DAYS) {
eprintln!("Warning: Failed to clean old logs: {}", e);
}
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("debug"));
let timer = create_local_timer();
let formatting_layer = fmt::layer()
.with_timer(timer.clone())
.pretty()
.with_writer(std::io::stdout);
let file_appender = rolling::RollingFileAppender::builder()
.rotation(rolling::Rotation::DAILY)
.filename_prefix("app")
.filename_suffix("log")
.build("log")
.expect("Failed to create rolling file appender");
let file_layer = fmt::layer()
.with_ansi(false)
.with_timer(timer)
.with_writer(file_appender);
if let Err(e) = Registry::default()
.with(env_filter)
.with(formatting_layer)
.with(file_layer)
.try_init()
{
eprintln!("Failed to initialize debug logger: {}", e);
}
}
pub fn init_info() {
if let Err(e) = clean_old_logs("log", "app", "log", rolling::Rotation::DAILY, DEFAULT_LOG_RETENTION_DAYS) {
eprintln!("Warning: Failed to clean old logs: {}", e);
}
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
let timer = create_local_timer();
let file_appender = rolling::RollingFileAppender::builder()
.rotation(rolling::Rotation::DAILY)
.filename_prefix("app")
.filename_suffix("log")
.build("log")
.expect("Failed to create rolling file appender");
let file_layer = fmt::layer()
.with_ansi(false)
.with_timer(timer.clone())
.with_writer(file_appender);
let formatting_layer = fmt::layer()
.with_timer(timer)
.pretty()
.with_writer(std::io::stdout);
if let Err(e) = Registry::default()
.with(env_filter)
.with(formatting_layer)
.with(file_layer)
.try_init()
{
eprintln!("Failed to initialize info logger: {}", e);
}
}
pub struct LoggerBuilder {
log_level: String,
log_dir: String,
filename_prefix: String,
filename_suffix: String,
rotation: Option<rolling::Rotation>,
console_enabled: bool,
file_enabled: bool,
retention_days: u64,
}
impl Default for LoggerBuilder {
fn default() -> Self {
Self {
log_level: "info".to_string(),
log_dir: "log".to_string(),
filename_prefix: "app".to_string(),
filename_suffix: "log".to_string(),
rotation: Some(rolling::Rotation::DAILY),
console_enabled: true,
file_enabled: true,
retention_days: DEFAULT_LOG_RETENTION_DAYS,
}
}
}
impl LoggerBuilder {
pub fn init() -> Self {
Self::default()
}
pub fn with(mut self, level: &str) -> Self {
self.log_level = level.to_string();
self
}
pub fn log_dir(mut self, dir: &str) -> Self {
self.log_dir = dir.to_string();
self
}
pub fn filename_prefix(mut self, prefix: &str) -> Self {
self.filename_prefix = prefix.to_string();
self
}
pub fn filename_suffix(mut self, suffix: &str) -> Self {
self.filename_suffix = suffix.to_string();
self
}
pub fn file_appender_rotation(mut self, rotation: rolling::Rotation) -> Self {
self.rotation = Some(rotation);
self
}
pub fn with_console(mut self, enabled: bool) -> Self {
self.console_enabled = enabled;
self
}
pub fn with_file(mut self, enabled: bool) -> Self {
self.file_enabled = enabled;
self
}
pub fn retention_days(mut self, days: u64) -> Self {
self.retention_days = days;
self
}
pub fn build(self) {
if self.file_enabled {
let rotation = self.rotation.as_ref().unwrap_or(&rolling::Rotation::NEVER);
if let Err(e) = clean_old_logs(
&self.log_dir,
&self.filename_prefix,
&self.filename_suffix,
rotation.clone(),
self.retention_days
) {
eprintln!("Warning: Failed to clean old logs: {}", e);
}
}
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(&self.log_level));
let timer = create_local_timer();
match (self.console_enabled, self.file_enabled) {
(true, true) => {
let file_appender = if let Some(rotation) = self.rotation {
rolling::RollingFileAppender::builder()
.rotation(rotation)
.filename_prefix(&self.filename_prefix)
.filename_suffix(&self.filename_suffix)
.build(&self.log_dir)
.expect("Failed to create rolling file appender")
} else {
let file_path = format!("{}/{}.{}", self.log_dir, self.filename_prefix, self.filename_suffix);
rolling::never(&self.log_dir, &file_path)
};
let console_layer = fmt::layer()
.with_timer(timer.clone())
.pretty()
.with_writer(std::io::stdout);
let file_layer = fmt::layer()
.with_ansi(false)
.with_timer(timer)
.with_writer(file_appender);
if let Err(e) = Registry::default()
.with(env_filter)
.with(console_layer)
.with(file_layer)
.try_init()
{
eprintln!("Failed to initialize logger: {}", e);
}
}
(true, false) => {
let console_layer = fmt::layer()
.with_timer(timer)
.pretty()
.with_writer(std::io::stdout);
if let Err(e) = Registry::default()
.with(env_filter)
.with(console_layer)
.try_init()
{
eprintln!("Failed to initialize logger: {}", e);
}
}
(false, true) => {
let file_appender = if let Some(rotation) = self.rotation {
rolling::RollingFileAppender::builder()
.rotation(rotation)
.filename_prefix(&self.filename_prefix)
.filename_suffix(&self.filename_suffix)
.build(&self.log_dir)
.expect("Failed to create rolling file appender")
} else {
let file_path = format!("{}/{}.{}", self.log_dir, self.filename_prefix, self.filename_suffix);
rolling::never(&self.log_dir, &file_path)
};
let file_layer = fmt::layer()
.with_ansi(false)
.with_timer(timer)
.with_writer(file_appender);
if let Err(e) = Registry::default()
.with(env_filter)
.with(file_layer)
.try_init()
{
eprintln!("Failed to initialize logger: {}", e);
}
}
(false, false) => {
eprintln!("Warning: Both console and file logging are disabled");
}
}
}
}
fn create_local_timer() -> OffsetTime<time::format_description::OwnedFormatItem> {
let offset = UtcOffset::current_local_offset()
.expect("Failed to get local time offset");
let time_format = parse("[year]-[month]-[day] [hour]:[minute]:[second]")
.expect("Invalid time format string");
let time_format = time::format_description::OwnedFormatItem::from(time_format);
OffsetTime::new(offset, time_format)
}
pub fn clean_old_logs(
log_dir: &str,
filename_prefix: &str,
filename_suffix: &str,
file_appender_rotation: rolling::Rotation,
retention_days: u64,
) -> Result<(), std::io::Error> {
let log_dir_path = Path::new(log_dir);
if !log_dir_path.exists() {
return Ok(());
}
let cutoff_datetime = Local::now() - TimeDelta::days(retention_days as i64);
for entry in fs::read_dir(log_dir_path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
let has_correct_prefix = file_name.starts_with(&format!("{}.", filename_prefix));
let has_correct_suffix = file_name.ends_with(&format!(".{}", filename_suffix));
if !has_correct_prefix || !has_correct_suffix {
continue;
}
let is_expired = match file_appender_rotation {
rolling::Rotation::NEVER => {
if let Ok(metadata) = fs::metadata(&path) {
if let Ok(modified) = metadata.modified() {
let modified_datetime = chrono::DateTime::<chrono::Local>::from(modified);
modified_datetime < cutoff_datetime
} else {
false
}
} else {
false
}
}
rolling::Rotation::HOURLY => {
match extract_datetime_from_filename(file_name, &format!("{}.", filename_prefix), &format!(".{}", filename_suffix), "%Y-%m-%d-%H") {
Some(file_datetime) => file_datetime < cutoff_datetime,
None => false,
}
}
rolling::Rotation::DAILY => {
match extract_datetime_from_filename(file_name, &format!("{}.", filename_prefix), &format!(".{}", filename_suffix), "%Y-%m-%d") {
Some(file_datetime) => file_datetime < cutoff_datetime,
None => false,
}
}
rolling::Rotation::MINUTELY => {
match extract_datetime_from_filename(file_name, &format!("{}.", filename_prefix), &format!(".{}", filename_suffix), "%Y-%m-%d-%H-%M") {
Some(file_datetime) => file_datetime < cutoff_datetime,
None => false,
}
}
rolling::Rotation::WEEKLY => {
if let Ok(metadata) = fs::metadata(&path) {
if let Ok(modified) = metadata.modified() {
let modified_datetime = chrono::DateTime::<chrono::Local>::from(modified);
modified_datetime < cutoff_datetime
} else {
false
}
} else {
false
}
}
};
if is_expired {
println!("Deleting old log file: {}", path.display());
fs::remove_file(&path)?;
}
}
}
}
Ok(())
}
fn extract_datetime_from_filename(
file_name: &str,
prefix: &str,
suffix: &str,
format_str: &str,
) -> Option<chrono::DateTime<Local>> {
let datetime_str = file_name.strip_prefix(prefix)?.strip_suffix(suffix)?;
match NaiveDate::parse_from_str(datetime_str, format_str) {
Ok(date) => Some(date.and_hms_opt(0, 0, 0)?.and_local_timezone(Local).unwrap()),
Err(_) => None,
}
}
pub fn init() {
init_info();
}
}
#[cfg(feature = "logger")]
pub use logger::*;