use anyhow::Result;
use flexi_logger::{
Age, Cleanup, Criterion, DeferredNow, Duplicate, FileSpec, LogSpecification, Logger, Naming,
Record,
};
use log::LevelFilter;
use serde::{Deserialize, Serialize};
const TIME_FORMAT: &str = "%Y.%m.%d %H:%M:%S%.3f";
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum LogStyle {
Default,
Line,
Module,
Full,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LogConfig {
#[serde(default = "default_level")]
pub level: u8,
#[serde(default = "default_usize")]
pub size_mb: u64,
#[serde(default = "default_console")]
pub console: bool,
#[serde(default = "default_log_dir")]
pub dir: String,
pub keep_day: Option<usize>,
pub keep_file_count: Option<usize>,
pub filters: Option<Vec<String>>,
pub log_name: Option<String>,
pub style: LogStyle,
pub cleanup_sync: Option<bool>,
}
fn default_usize() -> u64 {
3
}
fn default_console() -> bool {
true
}
fn default_level() -> u8 {
4
}
fn default_log_dir() -> String {
"./logs".to_string()
}
impl LogConfig {
pub fn to_level(&self) -> LevelFilter {
let level = match self.level {
1 => LevelFilter::Error,
2 => LevelFilter::Warn,
3 => LevelFilter::Info,
4 => LevelFilter::Debug,
5 => LevelFilter::Trace,
_ => LevelFilter::Debug,
};
level
}
}
impl Default for LogConfig {
fn default() -> Self {
Self {
level: default_level(),
size_mb: default_usize(),
console: default_console(),
dir: default_log_dir(),
keep_day: None,
keep_file_count: Some(3),
filters: None,
log_name: None,
style: LogStyle::Default,
cleanup_sync: Some(false),
}
}
}
pub fn string_to_level(level: &str) -> u8 {
let v = match level {
"trace" | "TRACE" => 5,
"debug" | "DEBUG" => 4,
"info" | "INFO" => 3,
"warn" | "WARN" => 2,
"error" | "ERROR" => 1,
_ => 4,
};
v
}
pub fn _default(
w: &mut dyn std::io::Write,
now: &mut DeferredNow,
record: &Record,
) -> Result<(), std::io::Error> {
if record.level() == LevelFilter::Error || record.level() == LevelFilter::Warn {
write!(
w,
"{} [{}] {}:{}: ",
now.format(TIME_FORMAT),
record.level(),
record.file().unwrap_or("<unnamed>"),
record.line().unwrap_or(0),
)?;
} else {
write!(w, "{} [{}]: ", now.format(TIME_FORMAT), record.level(),)?;
}
write!(w, "{}", &record.args())
}
pub fn _line(
w: &mut dyn std::io::Write,
now: &mut DeferredNow,
record: &Record,
) -> Result<(), std::io::Error> {
write!(
w,
"{} [{}] {}:{}: ",
now.format(TIME_FORMAT),
record.level(),
record.file().unwrap_or("<unnamed>"),
record.line().unwrap_or(0),
)?;
write!(w, "{}", &record.args())
}
pub fn _module(
w: &mut dyn std::io::Write,
now: &mut DeferredNow,
record: &Record,
) -> Result<(), std::io::Error> {
write!(
w,
"{} [{}] [{}]: ",
now.format(TIME_FORMAT),
record.level(),
record.module_path().unwrap_or("<unnamed>"),
)?;
write!(w, "{}", &record.args())
}
pub fn _full(
w: &mut dyn std::io::Write,
now: &mut DeferredNow,
record: &Record,
) -> Result<(), std::io::Error> {
write!(
w,
"{} [{}] [{}] {}:{}: ",
now.format(TIME_FORMAT),
record.level(),
record.module_path().unwrap_or("<unnamed>"),
record.file().unwrap_or("<unnamed>"),
record.line().unwrap_or(0),
)?;
write!(w, "{}", &record.args())
}
pub fn setup(cfg: LogConfig) -> Result<()> {
let level = match cfg.level {
1 => LevelFilter::Error,
2 => LevelFilter::Warn,
3 => LevelFilter::Info,
4 => LevelFilter::Debug,
5 => LevelFilter::Trace,
_ => LevelFilter::Debug,
};
let mut log_spec = LogSpecification::builder();
log_spec.default(level);
if let Some(items) = &cfg.filters {
for m in items {
log_spec.module(m, LevelFilter::Warn);
}
}
let hand = Logger::with(log_spec.build());
let hand = match cfg.style {
LogStyle::Line => hand.format(_line),
LogStyle::Module => hand.format(_module),
LogStyle::Full => hand.format(_full),
_ => hand.format(_default),
};
let basename = match &cfg.log_name {
Some(name) => name,
None => "",
};
let filespec = FileSpec::default().directory(cfg.dir).basename(basename);
let mut crit = Criterion::Size(cfg.size_mb * 1024 * 1024);
let mut cleanup = Cleanup::KeepLogFiles(cfg.keep_file_count.unwrap_or(3));
let mut naming = Naming::Numbers;
if let Some(v) = cfg.keep_day {
crit = Criterion::AgeOrSize(Age::Day, cfg.size_mb * 1024 * 1024);
cleanup = Cleanup::KeepForDays(v);
naming = Naming::TimestampsCustomFormat {
current_infix: Some("rCURRENT"),
format: "r%Y-%m-%d_%H-%M-%S",
};
}
let mut hand = hand
.rotate(crit, naming, cleanup)
.cleanup_in_background_thread(cfg.cleanup_sync.unwrap_or(false))
.log_to_file(filespec);
if cfg.console {
let d = match cfg.level {
1 => Duplicate::Error,
2 => Duplicate::Warn,
3 => Duplicate::Info,
4 => Duplicate::Debug,
5 => Duplicate::Trace,
_ => Duplicate::Debug,
};
hand = hand.duplicate_to_stdout(d)
}
hand.start()?;
Ok(())
}