pub extern crate log;
use env_logger::{Builder, Env};
use std::io::Write;
use chrono::Local;
use colored::*;
use std::path::Path;
use std::thread;
use std::env;
use std::sync::Arc;
pub struct LoggerBuilder {
pub level: String,
pub only_project_logs: bool,
pub path_depth: usize,
pub time_format: String,
pub preset: LoggerPreset,
}
impl Default for LoggerBuilder {
fn default() -> Self {
LoggerBuilder {
level: "INFO".to_string(),
only_project_logs: false,
path_depth: 0,
time_format: "%Y-%m-%d %H:%M:%S".to_string(),
preset: LoggerPreset::FULL,
}
}
}
#[derive(Eq, PartialEq, Debug)]
pub enum LoggerPreset {
FULL,
THREAD,
SIMPLE,
}
pub fn init_logger(logger_builder: LoggerBuilder) {
let level = parse_level(&logger_builder.level);
let only_project_logs = logger_builder.only_project_logs;
let path_depth = logger_builder.path_depth;
let time_format = logger_builder.time_format;
let preset = logger_builder.preset;
let project_name = Arc::new(env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "unknown".to_string()));
let env = Env::default().filter_or("RUST_LOG", level.to_string());
let mut builder = Builder::from_env(env);
let project_name_clone = Arc::clone(&project_name);
builder.format(move |buf, record| {
let file_path = record.file().unwrap_or("unknown");
let project_relative_path = get_project_relative_path(file_path, path_depth);
let line = record.line().unwrap_or(0);
let level = match record.level() {
log::Level::Error => "ERROR".red().bold(),
log::Level::Warn => "WARN ".yellow().bold(),
log::Level::Info => "INFO ".green().bold(),
log::Level::Debug => "DEBUG".blue().bold(),
log::Level::Trace => "TRACE".magenta().bold(),
};
let thread_name = thread::current().name().unwrap_or("unknown").to_string();
let thread_colored = if thread_name == "main" {
thread_name.bright_green()
} else {
thread_name.bright_blue()
};
let project = if record.target().starts_with(&*project_name_clone) {
format!("{}",
project_relative_path.yellow())
} else {
format!("[{}] {}",
record.target().yellow(),
project_relative_path.yellow())
};
let timestamp = Local::now().format(&time_format).to_string().cyan();
let log_message = match preset {
LoggerPreset::FULL => {
format!(
"{} {} [{}] [{}:{}] {}",
timestamp,
level,
thread_colored,
project,
line.to_string().yellow(),
record.args()
)
}
LoggerPreset::THREAD => {
format!(
"{} {} [{}] {}",
timestamp,
level,
thread_colored,
record.args()
)
}
LoggerPreset::SIMPLE => {
format!(
"[ {} {}] {}",
timestamp,
level,
record.args()
)
}
};
write!(buf, "{}", log_message)
});
if only_project_logs {
builder
.filter(None, log::LevelFilter::Off)
.filter(Some(&*project_name), level);
} else {
builder.filter(None, level);
}
builder.init();
}
fn get_project_relative_path(file_path: &str, depth: usize) -> String {
let path = Path::new(file_path);
let components: Vec<&str> = path
.components()
.filter_map(|c| c.as_os_str().to_str())
.collect();
let src_index = components.iter().position(|&c| c == "src");
let relevant_components = if let Some(index) = src_index {
components.iter()
.skip(index)
.cloned()
.collect::<Vec<&str>>()
} else {
components.clone()
};
let total = relevant_components.len();
if depth == 0 || depth >= total {
relevant_components.join("/")
} else {
relevant_components[(total - depth)..].join("/")
}
}
fn parse_level(level: &str) -> log::LevelFilter {
match level.to_uppercase().as_str() {
"OFF" => log::LevelFilter::Off,
"ERROR" => log::LevelFilter::Error,
"WARN" => log::LevelFilter::Warn,
"INFO" => log::LevelFilter::Info,
"DEBUG" => log::LevelFilter::Debug,
"TRACE" => log::LevelFilter::Trace,
_ => {
eprintln!("Invalid log level '{}', using default level Info", level);
log::LevelFilter::Info
}
}
}
#[macro_export]
macro_rules! log_info {
($($arg:tt)*) => ({
$crate::log::info!(target: module_path!(), "{}\n", format_args!($($arg)*));
})
}
#[macro_export]
macro_rules! log_error {
($($arg:tt)*) => ({
$crate::log::error!(target: module_path!(), "{}\n", format_args!($($arg)*));
})
}
#[macro_export]
macro_rules! log_warn {
($($arg:tt)*) => ({
$crate::log::warn!(target: module_path!(), "{}\n", format_args!($($arg)*));
})
}
#[macro_export]
macro_rules! log_debug {
($($arg:tt)*) => ({
$crate::log::debug!(target: module_path!(), "{}\n", format_args!($($arg)*));
})
}
#[macro_export]
macro_rules! log_trace {
($($arg:tt)*) => ({
$crate::log::trace!(target: module_path!(), "{}\n", format_args!($($arg)*));
})
}
#[macro_export]
macro_rules! _log_info {
($($arg:tt)*) => ({
$crate::log::info!(target: module_path!(), "{}", format_args!($($arg)*));
})
}
#[macro_export]
macro_rules! _log_error {
($($arg:tt)*) => ({
$crate::log::error!(target: module_path!(), "{}", format_args!($($arg)*));
})
}
#[macro_export]
macro_rules! _log_warn {
($($arg:tt)*) => ({
$crate::log::warn!(target: module_path!(), "{}", format_args!($($arg)*));
})
}
#[macro_export]
macro_rules! _log_debug {
($($arg:tt)*) => ({
$crate::log::debug!(target: module_path!(), "{}", format_args!($($arg)*));
})
}
#[macro_export]
macro_rules! _log_trace {
($($arg:tt)*) => ({
$crate::log::trace!(target: module_path!(), "{}", format_args!($($arg)*));
})
}
pub use log_info as info;
pub use log_error as error;
pub use log_warn as warn;
pub use log_debug as debug;
pub use log_trace as trace;
pub use _log_info as _info;
pub use _log_error as _error;
pub use _log_warn as _warn;
pub use _log_debug as _debug;
pub use _log_trace as _trace;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_logger_builder_default() {
let builder = LoggerBuilder::default();
assert_eq!(builder.level, "INFO".to_string());
assert_eq!(builder.only_project_logs, false);
assert_eq!(builder.path_depth, 0);
assert_eq!(builder.time_format, "%Y-%m-%d %H:%M:%S");
assert_eq!(builder.preset, LoggerPreset::FULL);
}
#[test]
fn test_init_logger() {
let builder = LoggerBuilder {
level: "trace".to_string(),
only_project_logs: true,
..Default::default()
};
init_logger(builder);
log_error!("This is an error message");
log_warn!("This is a warning message");
log_info!("This is an info message");
log_debug!("This is a debug message");
log_trace!("This is a trace message");
log_info!("Test passed");
}
}