use std::path::PathBuf;
use directories::{BaseDirs, ProjectDirs};
use fern::colors::{Color, ColoredLevelConfig};
use crate::CrateSetup;
#[cfg(feature = "log_rotation")]
mod rotation;
pub struct LoggingSetupBuilder<'a> {
crate_setup: &'a CrateSetup,
verbosity: Option<i8>,
log_to_file: Option<bool>,
log_filters: Vec<Box<fern::Filter>>,
#[cfg(feature = "log_rotation")]
log_rotation: Option<bool>,
#[cfg(feature = "panic_handler")]
log_panics: Option<bool>,
}
impl<'a> LoggingSetupBuilder<'a> {
pub fn new(crate_setup: &'a CrateSetup) -> Self {
Self {
crate_setup,
verbosity: None,
log_to_file: None,
log_filters: Vec::new(),
log_rotation: None,
log_panics: None,
}
}
pub fn with_verbosity(mut self, verbosity: i8) -> Self {
self.verbosity = Some(verbosity);
self
}
pub fn with_log_to_file(mut self, log_to_file: bool) -> Self {
self.log_to_file = Some(log_to_file);
self
}
pub fn with_filter<F>(mut self, filter: F) -> Self
where
F: Fn(&log::Metadata) -> bool + Send + Sync + 'static,
{
self.log_filters.push(Box::new(filter));
self
}
#[cfg(feature = "log_rotation")]
pub fn with_log_rotation(mut self, log_rotation: bool) -> Self {
self.log_rotation = Some(log_rotation);
self
}
#[cfg(feature = "panic_handler")]
pub fn with_log_panics(mut self, log_panics: bool) -> Self {
self.log_panics = Some(log_panics);
self
}
pub fn build(self) -> Result<(), fern::InitError> {
let verbosity = self
.verbosity
.unwrap_or_else(|| if cfg!(debug_assertions) { 2 } else { 1 });
let log_to_file = self.log_to_file.unwrap_or(false);
#[cfg(feature = "log_rotation")]
let log_rotation = self.log_rotation.unwrap_or(false);
#[cfg(not(feature = "log_rotation"))]
let log_rotation = false;
#[cfg(feature = "panic_handler")]
let log_panics = self.log_panics.unwrap_or(false);
let colors = ColoredLevelConfig::new()
.error(Color::Red)
.warn(Color::Yellow)
.info(Color::Green)
.debug(Color::White)
.trace(Color::BrightBlack);
let mut logger = fern::Dispatch::new();
logger = match verbosity {
e if e < 0 => logger.level(log::LevelFilter::Error),
0 => logger.level(log::LevelFilter::Warn),
1 => logger.level(log::LevelFilter::Info),
2 => logger.level(log::LevelFilter::Debug),
_ => logger.level(log::LevelFilter::Trace),
};
for filter in self.log_filters {
logger = logger.filter(filter);
}
let stdout_logger = fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"{}[{}][{}][{}] {}\x1B[0m",
format_args!("\x1B[{}m", colors.get_color(&record.level()).to_fg_str()),
chrono::Local::now().format("%H:%M:%S"),
record.level(),
record.target(),
message
))
})
.chain(std::io::stdout());
#[allow(unused_mut, unused_variables)]
let mut old_logs = Vec::<PathBuf>::new();
if log_to_file {
let log_dir = get_log_path(
self.crate_setup.base_dirs(),
self.crate_setup.project_dirs(),
);
if log_rotation {
#[cfg(feature = "log_rotation")]
{
old_logs = rotation::get_old_log_files(&log_dir);
let log_file_latest = log_dir.join("latest.log");
let log_file_time = log_dir.join(
format_args!("{}.log", chrono::Local::now().format("%Y.%m.%d.%H.%M.%S"))
.to_string(),
);
logger = logger.chain(
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"[{}][{}][{}] {}",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
record.target(),
message
))
})
.chain(
std::fs::OpenOptions::new()
.truncate(true)
.write(true)
.create(true)
.open(log_file_latest)?,
)
.chain(fern::log_file(log_file_time)?),
);
}
} else {
let log_file = log_dir.join(format!("{}.log", self.crate_setup.application_name()));
logger = logger.chain(
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"[{}][{}][{}] {}",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
record.target(),
message
))
})
.chain(
std::fs::OpenOptions::new()
.append(true)
.create(true)
.open(log_file)?,
),
);
}
}
logger.chain(stdout_logger).apply()?;
#[cfg(feature = "log_rotation")]
{
if !old_logs.is_empty() {
rotation::clean_up_old_logs(old_logs);
}
}
#[cfg(feature = "panic_handler")]
{
if log_panics {
std::panic::set_hook(Box::new(logging_panic_handler));
}
}
Ok(())
}
}
fn get_log_path(base_dirs: &BaseDirs, project_dirs: &ProjectDirs) -> PathBuf {
let dir = if cfg!(target_os = "macos") {
base_dirs
.home_dir()
.join("Library")
.join("Logs")
.join(project_dirs.project_path())
} else {
project_dirs.data_local_dir().join("logs")
};
std::fs::create_dir_all(&dir).expect("Failed to create log dir");
dir
}
#[cfg(feature = "panic_handler")]
pub fn logging_panic_handler(info: &std::panic::PanicInfo) {
let location = info.location().unwrap(); let msg = match info.payload().downcast_ref::<&'static str>() {
Some(s) => *s,
None => match info.payload().downcast_ref::<String>() {
Some(s) => &s[..],
None => "Box<Any>",
},
};
let thread = std::thread::current();
let name = thread.name().unwrap_or("<unnamed>");
log::error!("thread '{}' panicked at '{}', {}", name, msg, location);
let mut frame_counter = 0;
backtrace::trace(|frame| {
let ip = frame.ip();
backtrace::resolve(ip, |symbol| {
let name = symbol
.name()
.map(|name| name.to_string())
.unwrap_or_else(|| "<unnamed>".into());
let addr = symbol.addr().unwrap_or_else(std::ptr::null_mut);
let filename = symbol.filename().map_or_else(
|| "<unknown source>".into(),
|path| path.to_string_lossy().into_owned(),
);
let line_number = symbol.lineno().unwrap_or(0);
log::error!(
"#{}: {:?}:{} at {}:{}",
frame_counter,
addr,
name,
filename,
line_number
);
});
frame_counter += 1;
true
});
}