use std::io;
use std::path::Path;
fn base_level_for(verbosity: u8) -> log::LevelFilter {
match verbosity {
0 => log::LevelFilter::Warn,
1 => log::LevelFilter::Info,
2 => log::LevelFilter::Debug,
_3_or_more => log::LevelFilter::Trace,
}
}
fn file_level_for(base_level: log::LevelFilter) -> log::LevelFilter {
base_level.max(log::LevelFilter::Info)
}
pub fn setup(verbosity: u8, log_file: Option<&str>) -> Result<(), String> {
use colored::Colorize;
let base_level = base_level_for(verbosity);
let stderr_config = fern::Dispatch::new()
.level(base_level)
.format(
move |out: fern::FormatCallback,
message: &std::fmt::Arguments,
record: &log::Record| {
let level = match record.level() {
log::Level::Error => "E".bright_red(),
log::Level::Warn => "W".bright_yellow(),
log::Level::Info => "I".bright_green(),
log::Level::Debug => "D".blue(),
log::Level::Trace => "T".black(),
};
let target = record.target().replace("::", ".").yellow();
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
out.finish(format_args!("{} {} {}: {}", now, level, target, message));
},
)
.chain(io::stderr());
let mut base_config = fern::Dispatch::new().chain(stderr_config);
if let Some(path) = log_file {
let file_level = file_level_for(base_level);
let file = open_log_file(Path::new(path))?;
let file_config = fern::Dispatch::new()
.level(file_level)
.format(
|out: fern::FormatCallback, message: &std::fmt::Arguments, record: &log::Record| {
let level = match record.level() {
log::Level::Error => "E",
log::Level::Warn => "W",
log::Level::Info => "I",
log::Level::Debug => "D",
log::Level::Trace => "T",
};
let target = record.target().replace("::", ".");
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
out.finish(format_args!("{} {} {}: {}", now, level, target, message));
},
)
.chain(file);
base_config = base_config.chain(file_config);
}
base_config.apply().map_err(|e| e.to_string())
}
const MAX_LOG_SIZE: u64 = 2 * 1024 * 1024;
fn open_log_file(path: &Path) -> Result<std::fs::File, String> {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("cannot create log directory: {}", e))?;
}
}
if let Ok(meta) = std::fs::metadata(path) {
if meta.len() > MAX_LOG_SIZE {
let _ = std::fs::remove_file(path);
}
}
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|e| format!("cannot open log file {}: {}", path.display(), e))
}
#[cfg(test)]
mod tests {
use super::*;
use log::LevelFilter;
#[test]
fn test_base_level_for_verbosity() {
assert_eq!(base_level_for(0), LevelFilter::Warn);
assert_eq!(base_level_for(1), LevelFilter::Info);
assert_eq!(base_level_for(2), LevelFilter::Debug);
assert_eq!(base_level_for(3), LevelFilter::Trace);
assert_eq!(base_level_for(4), LevelFilter::Trace);
assert_eq!(base_level_for(255), LevelFilter::Trace);
}
#[test]
fn test_file_level_floors_at_info() {
assert_eq!(file_level_for(LevelFilter::Warn), LevelFilter::Info);
assert_eq!(file_level_for(LevelFilter::Info), LevelFilter::Info);
}
#[test]
fn test_file_level_follows_more_verbose_request() {
assert_eq!(file_level_for(LevelFilter::Debug), LevelFilter::Debug);
assert_eq!(file_level_for(LevelFilter::Trace), LevelFilter::Trace);
}
#[test]
fn test_vvv_routes_trace_to_file() {
let base = base_level_for(3);
assert_eq!(file_level_for(base), LevelFilter::Trace);
}
}