use std::{
env, fmt, fs,
io::Write,
path::{Path, PathBuf},
str::FromStr,
sync::Mutex
};
use time::macros::format_description;
use tracing_subscriber::{EnvFilter, fmt::time::UtcTime};
#[cfg(feature = "clap")]
use clap::ValueEnum;
use crate::err::Error;
static DEFAULT_LOG_FILTERS: Mutex<Vec<(String, log::LevelFilter)>> =
Mutex::new(Vec::new());
pub fn set_default_log_filters(filters: &[(&str, log::LevelFilter)]) {
let filters: Vec<_> = filters
.iter()
.map(|(module, level)| ((*module).to_string(), *level))
.collect();
if let Ok(mut g) = DEFAULT_LOG_FILTERS.lock() {
*g = filters;
} else {
panic!("Unable to acquire default log filters lock");
}
}
#[derive(Default)]
enum LogOut {
#[default]
Console,
#[cfg(windows)]
WinEvtLog { svcname: String }
}
pub struct LumberJack {
init: bool,
as_service: bool,
log_out: LogOut,
log_level: LogLevel,
log_filter: Option<String>,
trace_filter: Option<String>,
trace_file: Option<PathBuf>
}
impl Default for LumberJack {
fn default() -> Self {
let log_level =
std::env::var("LOG_LEVEL").map_or(LogLevel::Warn, |level| {
level
.parse::<LogLevel>()
.map_or(LogLevel::Warn, |level| level)
});
let log_filter = std::env::var("LOG_FILTER").ok();
let trace_file =
std::env::var("TRACE_FILE").map_or(None, |v| Some(PathBuf::from(v)));
let trace_filter = env::var("TRACE_FILTER").ok();
Self {
init: true,
as_service: false,
log_out: LogOut::default(),
log_level,
log_filter,
trace_filter,
trace_file
}
}
}
impl LumberJack {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn noinit() -> Self {
Self {
init: false,
..Default::default()
}
}
#[must_use]
pub const fn service(mut self) -> Self {
self.service_r();
self
}
pub const fn service_r(&mut self) -> &mut Self {
self.as_service = true;
self
}
#[must_use]
pub const fn set_init(mut self, flag: bool) -> Self {
self.init = flag;
self
}
#[cfg(windows)]
pub fn from_winsvc(svcname: &str) -> Result<Self, Error> {
let params = crate::rt::winsvc::get_service_param(svcname)?;
let loglevel = params
.get_string("LogLevel")
.unwrap_or_else(|_| String::from("warn"))
.parse::<LogLevel>()
.unwrap_or(LogLevel::Warn);
let logfilter = params.get_string("LogFilter").ok();
let tracefilter = params.get_string("TraceFilter");
let tracefile = params.get_string("TraceFile");
let mut this = Self::new().log_level(loglevel);
this.log_out = LogOut::WinEvtLog {
svcname: svcname.to_string()
};
if let Some(filter) = logfilter {
this.log_filter_r(filter);
}
let this =
if let (Ok(tracefilter), Ok(tracefile)) = (tracefilter, tracefile) {
this.trace_filter(tracefilter).trace_file(tracefile)
} else {
this
};
Ok(this)
}
#[must_use]
pub const fn log_level(mut self, level: LogLevel) -> Self {
self.log_level = level;
self
}
#[must_use]
pub fn log_filter(mut self, filters: String) -> Self {
self.log_filter = Some(filters);
self
}
pub fn log_filter_r(&mut self, filters: String) -> &mut Self {
self.log_filter = Some(filters);
self
}
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn trace_filter(mut self, filter: impl ToString) -> Self {
self.trace_filter = Some(filter.to_string());
self
}
#[must_use]
pub fn trace_file<P>(mut self, fname: P) -> Self
where
P: AsRef<Path>
{
self.trace_file = Some(fname.as_ref().to_path_buf());
self
}
pub fn init(self) -> Result<(), Error> {
if self.init {
match self.log_out {
LogOut::Console => {
self.init_cons_logging()?;
}
#[cfg(windows)]
LogOut::WinEvtLog { ref svcname } => {
self.init_winsvc_logger(svcname)?;
}
}
if let Some(fname) = self.trace_file {
init_file_tracing(fname, self.trace_filter.as_deref());
} else {
init_console_tracing(self.trace_filter.as_deref());
}
}
Ok(())
}
#[expect(clippy::unnecessary_wraps)]
fn init_cons_logging(&self) -> Result<(), Error> {
let mut bldr = env_logger::Builder::new();
if self.as_service {
bldr.format(|buf, record| {
writeln!(buf, "[{}] {}", record.level(), record.args())
});
} else {
bldr.format(|buf, record| {
writeln!(
buf,
"{} [{}] {}",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
record.args()
)
});
}
self.init_log_filtering(&mut bldr);
bldr.init();
log::set_max_level(self.log_level.into());
Ok(())
}
#[cfg(windows)]
fn init_winsvc_logger(&self, svcname: &str) -> Result<(), Error> {
let mut bldr = env_logger::Builder::new();
self.init_log_filtering(&mut bldr);
let loglvl = match self.log_level {
LogLevel::Off => todo!(),
LogLevel::Error => log::Level::Error,
LogLevel::Warn => log::Level::Warn,
LogLevel::Info => log::Level::Info,
LogLevel::Debug => log::Level::Debug,
LogLevel::Trace => log::Level::Trace
};
fltevtlog::init(bldr, svcname, loglvl)?;
log::set_max_level(self.log_level.into());
Ok(())
}
fn init_log_filtering(&self, bldr: &mut env_logger::Builder) {
if let Some(ref filters) = self.log_filter {
bldr.parse_filters(filters);
} else if let Ok(mut g) = DEFAULT_LOG_FILTERS.lock() {
let filters: Vec<_> = g.drain(..).collect();
drop(g);
if filters.is_empty() {
let lf = self.log_level.into();
bldr.filter(None, lf);
} else {
for (module, level) in filters {
bldr.filter_module(&module, level);
}
}
}
}
}
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "clap", derive(ValueEnum))]
pub enum LogLevel {
#[cfg_attr(feature = "clap", clap(name = "off"))]
Off,
#[cfg_attr(feature = "clap", clap(name = "error"))]
Error,
#[cfg_attr(feature = "clap", clap(name = "warn"))]
#[default]
Warn,
#[cfg_attr(feature = "clap", clap(name = "info"))]
Info,
#[cfg_attr(feature = "clap", clap(name = "debug"))]
Debug,
#[cfg_attr(feature = "clap", clap(name = "trace"))]
Trace
}
impl FromStr for LogLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"off" => Ok(Self::Off),
"error" => Ok(Self::Error),
"warn" => Ok(Self::Warn),
"info" => Ok(Self::Info),
"debug" => Ok(Self::Debug),
"trace" => Ok(Self::Trace),
_ => Err(format!("Unknown log level '{s}'"))
}
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Off => "off",
Self::Error => "error",
Self::Warn => "warn",
Self::Info => "info",
Self::Debug => "debug",
Self::Trace => "trace"
};
write!(f, "{s}")
}
}
impl From<LogLevel> for log::LevelFilter {
fn from(ll: LogLevel) -> Self {
match ll {
LogLevel::Off => Self::Off,
LogLevel::Error => Self::Error,
LogLevel::Warn => Self::Warn,
LogLevel::Info => Self::Info,
LogLevel::Debug => Self::Debug,
LogLevel::Trace => Self::Trace
}
}
}
impl From<LogLevel> for Option<tracing::Level> {
fn from(ll: LogLevel) -> Self {
match ll {
LogLevel::Off => None,
LogLevel::Error => Some(tracing::Level::ERROR),
LogLevel::Warn => Some(tracing::Level::WARN),
LogLevel::Info => Some(tracing::Level::INFO),
LogLevel::Debug => Some(tracing::Level::DEBUG),
LogLevel::Trace => Some(tracing::Level::TRACE)
}
}
}
fn init_console_tracing(filter: Option<&str>) {
let filter = filter.map_or_else(|| EnvFilter::new("none"), EnvFilter::new);
tracing_subscriber::fmt()
.with_env_filter(filter)
.init();
}
fn init_file_tracing<P>(fname: P, filter: Option<&str>)
where
P: AsRef<Path>
{
let timer = UtcTime::new(format_description!(
"[year]-[month]-[day] [hour]:[minute]:[second]"
));
let Ok(f) = fs::OpenOptions::new().create(true).append(true).open(fname)
else {
return;
};
let filter = filter.map_or_else(|| EnvFilter::new("warn"), EnvFilter::new);
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(f)
.with_ansi(false)
.with_timer(timer)
.init();
}