use flate2::Compression;
use log::{LevelFilter, Log, Metadata, Record};
use std::fs::{File, OpenOptions, create_dir_all};
use std::io::{self, Write};
use std::path::Path;
use std::sync::Mutex;
use thiserror::Error;
mod config;
pub mod examples;
pub mod formatter;
pub use config::{LoggerConfig, LoggerConfigBuilder, ModuleFilters};
pub use formatter::LogFormatter;
#[derive(Error, Debug)]
pub enum LogError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Failed to set logger")]
Logger,
}
pub struct FStdoutLogger {
log_file: Option<Mutex<File>>,
formatter: LogFormatter,
}
impl FStdoutLogger {
pub fn new<P: AsRef<Path>>(file_path: Option<P>) -> Result<Self, LogError> {
Self::with_config(file_path, LoggerConfig::default())
}
pub fn with_config<P: AsRef<Path>>(
file_path: Option<P>,
config: LoggerConfig,
) -> Result<Self, LogError> {
let log_file = match file_path {
Some(path) => {
let file = Path::new(path.as_ref()).to_path_buf();
if let Some(parent) = file.parent() {
create_dir_all(parent)?;
};
if file.exists() {
use flate2::write::GzEncoder;
use tar::Builder;
let file_basename = format!("{}", chrono::Local::now().format("%d%m%Y_%H%M%S"));
let archive_ref = format!("{}.tar.xz", file_basename);
let mut archive_path = Path::new(&archive_ref).to_path_buf();
if let Some(parent) = file.parent() {
archive_path = parent.join(archive_path.file_name().unwrap());
};
let archive_file = File::create(archive_path)?;
let encoder = GzEncoder::new(archive_file, Compression::default());
let mut archive = Builder::new(encoder);
archive.append_file(
Path::new(&format!("{}.log", file_basename)),
&mut File::open(file)?,
)?;
archive.into_inner().unwrap();
}
let file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(path)?;
Some(Mutex::new(file))
}
None => None,
};
Ok(Self {
log_file,
formatter: LogFormatter::new(config),
})
}
pub fn init(self) -> Result<(), LogError> {
if log::set_logger(Box::leak(Box::new(self))).is_err() {
return Err(LogError::Logger);
}
log::set_max_level(LevelFilter::Trace);
Ok(())
}
pub fn init_with_level(self, level: LevelFilter) -> Result<(), LogError> {
if log::set_logger(Box::leak(Box::new(self))).is_err() {
return Err(LogError::Logger);
}
log::set_max_level(level);
Ok(())
}
}
impl Log for FStdoutLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
if metadata.level() > log::max_level() {
return false;
}
let target = metadata.target();
let module_level = self.formatter.config().module_filters.level_for(target);
metadata.level() <= module_level
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let stdout_formatted = format!("{}\n", self.formatter.format_stdout(record));
print!("{stdout_formatted}");
if let Some(file) = &self.log_file
&& let Ok(mut file) = file.lock()
{
let file_formatted = self.formatter.format_file(record);
let _ = file.write_all(file_formatted.as_bytes());
}
}
fn flush(&self) {
let _ = io::stdout().flush();
if let Some(file) = &self.log_file
&& let Ok(mut file) = file.lock()
{
let _ = file.flush();
}
}
}
pub fn init_logger<P: AsRef<Path>>(file_path: Option<P>) -> Result<(), LogError> {
if std::env::var("RUST_LOG").is_ok() || std::env::var("LOG_LEVEL").is_ok() {
init_logger_from_env(file_path)
} else {
FStdoutLogger::new(file_path)?.init()
}
}
pub fn init_logger_with_level<P: AsRef<Path>>(
file_path: Option<P>,
level: LevelFilter,
) -> Result<(), LogError> {
FStdoutLogger::new(file_path)?.init_with_level(level)
}
pub fn init_logger_with_config<P: AsRef<Path>>(
file_path: Option<P>,
config: LoggerConfig,
) -> Result<(), LogError> {
let level = config.level;
FStdoutLogger::with_config(file_path, config)?.init_with_level(level)
}
pub fn init_production_logger<P: AsRef<Path>>(file_path: Option<P>) -> Result<(), LogError> {
init_logger_with_config(file_path, LoggerConfig::production())
}
pub fn init_development_logger<P: AsRef<Path>>(file_path: Option<P>) -> Result<(), LogError> {
init_logger_with_config(file_path, LoggerConfig::development())
}
pub fn init_stdout_logger(config: LoggerConfig) -> Result<(), LogError> {
init_logger_with_config(None::<String>, config)
}
pub fn init_simple_stdout_logger(level: LevelFilter) -> Result<(), LogError> {
let config = LoggerConfig {
level,
..LoggerConfig::default()
};
FStdoutLogger::with_config(None::<String>, config)?.init_with_level(level)
}
pub fn init_logger_from_env<P: AsRef<Path>>(file_path: Option<P>) -> Result<(), LogError> {
let config = LoggerConfig::from_env();
FStdoutLogger::with_config(file_path, config)?.init()
}
#[cfg(test)]
mod tests {
use super::*;
use log::{debug, error, info, trace, warn};
use std::fs;
use std::io::Read;
#[test]
fn test_stdout_logger() {
let config = LoggerConfig::builder()
.level(LevelFilter::Debug)
.show_file_info(false)
.build();
let result = init_stdout_logger(config);
assert!(result.is_ok());
}
#[test]
fn test_file_logger() {
let test_file = "test_log.txt";
let _ = fs::remove_file(test_file);
let config = LoggerConfig::builder()
.level(LevelFilter::Debug)
.show_file_info(true)
.use_colors(false)
.build();
let result = init_logger_with_config(Some(test_file), config);
assert!(result.is_ok());
trace!("This is a trace message");
debug!("This is a debug message");
info!("This is an info message");
warn!("This is a warning message");
error!("This is an error message");
let mut file = File::open(test_file).expect("Failed to open log file");
let mut contents = String::new();
file.read_to_string(&mut contents)
.expect("Failed to read log file");
assert!(!contents.contains("trace message"));
assert!(contents.contains("debug message"));
assert!(contents.contains("info message"));
assert!(contents.contains("warning message"));
assert!(contents.contains("error message"));
let _ = fs::remove_file(test_file);
}
}