use std::{
fs::{File, OpenOptions},
io::{BufWriter, Write},
ops::DerefMut,
path::PathBuf,
sync::{
atomic::{AtomicU8, Ordering},
Arc, Mutex,
},
};
use log::{Level, LevelFilter, Log, Metadata, Record};
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
#[repr(u8)]
pub enum LogLevel {
OFF = 0,
ERROR,
WARN,
#[default]
INFO,
DEBUG,
TRACE,
}
impl From<u8> for LogLevel {
fn from(verbosity: u8) -> Self {
match verbosity {
0 => Self::OFF,
1 => Self::ERROR,
2 => Self::WARN,
3 => Self::INFO,
4 => Self::DEBUG,
_ => Self::TRACE,
}
}
}
impl From<Level> for LogLevel {
fn from(l: Level) -> Self {
match l {
Level::Error => Self::ERROR,
Level::Warn => Self::WARN,
Level::Info => Self::INFO,
Level::Debug => Self::DEBUG,
Level::Trace => Self::TRACE,
}
}
}
impl From<LogLevel> for Level {
fn from(l: LogLevel) -> Self {
match l {
LogLevel::ERROR => Self::Error,
LogLevel::WARN => Self::Warn,
LogLevel::OFF | LogLevel::INFO => Self::Info,
LogLevel::DEBUG => Self::Debug,
LogLevel::TRACE => Self::Trace,
}
}
}
impl From<LevelFilter> for LogLevel {
fn from(l: LevelFilter) -> Self {
match l {
LevelFilter::Off => Self::OFF,
LevelFilter::Error => Self::ERROR,
LevelFilter::Warn => Self::WARN,
LevelFilter::Info => Self::INFO,
LevelFilter::Debug => Self::DEBUG,
LevelFilter::Trace => Self::TRACE,
}
}
}
impl From<LogLevel> for LevelFilter {
fn from(l: LogLevel) -> Self {
match l {
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 std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
OFF => "OFF",
ERROR => "ERROR",
WARN => "WARN",
INFO => "INFO",
DEBUG => "DEBUG",
TRACE => "TRACE",
}
)
}
}
use LogLevel::*;
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
pub enum Destination {
File,
#[default]
Stderr,
None,
}
enum Writer {
Stderr(BufWriter<std::io::Stderr>),
File(BufWriter<std::fs::File>),
}
impl std::ops::Deref for Writer {
type Target = dyn std::io::Write;
#[inline]
fn deref(&self) -> &Self::Target {
match self {
Self::Stderr(ref inner) => inner,
Self::File(ref inner) => inner,
}
}
}
impl std::ops::DerefMut for Writer {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Self::Stderr(ref mut inner) => inner,
Self::File(ref mut inner) => inner,
}
}
}
struct FileOutput {
writer: Writer,
path: PathBuf,
}
#[derive(Clone)]
pub struct StderrLogger {
dest: Arc<Mutex<FileOutput>>,
level: Arc<AtomicU8>,
print_level: bool,
print_module_names: bool,
debug_dest: Destination,
}
impl std::fmt::Debug for StderrLogger {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
fmt.debug_struct(crate::identify!(StderrLogger))
.field("level", &LogLevel::from(self.level.load(Ordering::SeqCst)))
.field("print_level", &self.print_level)
.field("print_module_names", &self.print_module_names)
.field("debug_dest", &self.debug_dest)
.finish()
}
}
impl Default for StderrLogger {
fn default() -> Self {
Self::new(LogLevel::default())
}
}
impl StderrLogger {
pub fn new_with(level: LogLevel, test: bool) -> Self {
use std::sync::Once;
static INIT_STDERR_LOGGING: Once = Once::new();
let logger = if test {
Self {
dest: Arc::new(Mutex::new(FileOutput {
writer: Writer::Stderr(BufWriter::new(std::io::stderr())),
path: PathBuf::new(),
})),
level: Arc::new(AtomicU8::new(level as u8)),
print_level: true,
print_module_names: true,
debug_dest: Destination::Stderr,
}
} else {
#[inline(always)]
fn __inline_err_wrap() -> Result<(PathBuf, File), Box<dyn std::error::Error>> {
let data_dir = xdg::BaseDirectories::with_prefix("meli")?;
let path = data_dir.place_data_file("meli.log")?;
let log_file = OpenOptions::new().append(true)
.create(true)
.read(true)
.open(&path)?;
Ok((path, log_file))
}
let (path, log_file) =
__inline_err_wrap().expect("Could not create log file in XDG_DATA_DIR");
Self {
dest: Arc::new(Mutex::new(FileOutput {
writer: Writer::File(BufWriter::new(log_file)),
path,
})),
level: Arc::new(AtomicU8::new(level as u8)),
print_level: true,
print_module_names: true,
debug_dest: if std::env::var("MELI_DEBUG_STDERR").is_ok() {
Destination::Stderr
} else {
Destination::None
},
}
};
if cfg!(feature = "debug-tracing") {
log::set_max_level(
if matches!(LevelFilter::from(logger.log_level()), LevelFilter::Off) {
LevelFilter::Off
} else {
LevelFilter::Trace
},
)
} else {
log::set_max_level(LevelFilter::from(logger.log_level()));
}
INIT_STDERR_LOGGING.call_once(|| {
log::set_boxed_logger(Box::new(logger.clone())).unwrap();
});
logger
}
pub fn new(level: LogLevel) -> Self {
Self::new_with(level, cfg!(test))
}
pub fn log_level(&self) -> LogLevel {
self.level.load(Ordering::SeqCst).into()
}
pub fn change_log_dest(&self, path: PathBuf) {
use crate::utils::shellexpand::ShellExpandTrait;
let path = path.expand(); let mut dest = self.dest.lock().unwrap();
*dest = FileOutput {
writer: Writer::File(BufWriter::new(
OpenOptions::new()
.append(true)
.create(true)
.read(true)
.open(&path)
.unwrap(),
)),
path,
};
}
pub fn log_dest(&self) -> PathBuf {
self.dest.lock().unwrap().path.clone()
}
}
impl Log for StderrLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
!["polling", "async_io", "tracing"]
.iter()
.any(|t| metadata.target().starts_with(t))
&& (metadata.level() <= Level::from(self.log_level())
|| !matches!(self.debug_dest, Destination::None))
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
fn write(
writer: &mut (impl Write + ?Sized),
record: &Record,
(print_level, print_module_names): (bool, bool),
) -> Option<()> {
writer
.write_all(
super::datetime::timestamp_to_string(super::datetime::now(), None, false)
.as_bytes(),
)
.ok()?;
writer.write_all(b" [").ok()?;
if print_level {
writer
.write_all(record.level().to_string().as_bytes())
.ok()?;
}
write!(writer, "]: ").ok()?;
if print_module_names {
write!(writer, "{}: ", record.metadata().target()).ok()?;
}
#[cfg(any(feature = "http", feature = "http-static"))]
if matches!(record.metadata(), m if (m.target().starts_with("isahc::handler") || m.target().starts_with("isahc::wire")) && m.level() >= Level::Debug)
&& matches!(record.args().to_string(), s if s.contains("Bearer") || s.contains("Basic"))
{
fn redact_http_auth(
writer: &mut (impl Write + ?Sized),
record: &Record,
) -> Option<()> {
use std::borrow::Cow;
use regex::Regex;
let log_string = record.args().to_string();
match Regex::new(r"(?m)((?:[bB]earer)|(?:[bB]asic)) (?:[^]\\]+)")
.unwrap()
.replace_all(&log_string, "$1 <REDACTED>")
{
Cow::Borrowed(_) => {
if log_string.contains("Bearer") {
writeln!(writer, "Bearer <REDACTED>").ok()?;
} else if log_string.contains("Basic") {
writeln!(writer, "Basic <REDACTED>").ok()?;
} else {
writeln!(writer, "<REDACTED>").ok()?;
}
}
Cow::Owned(s) => writeln!(writer, "{s}").ok()?,
}
writer.flush().ok()?;
Some(())
}
return redact_http_auth(writer, record);
}
write!(writer, "{}", record.args()).ok()?;
writer.write_all(b"\n").ok()?;
writer.flush().ok()?;
Some(())
}
match (
self.debug_dest,
record.metadata().level() <= Level::from(self.log_level()),
) {
(Destination::None, false) => {}
(Destination::None | Destination::File, _) => {
_ = self.dest.lock().ok().and_then(|mut d| {
write(
d.writer.deref_mut(),
record,
(self.print_level, self.print_module_names),
)
});
}
(Destination::Stderr, true) => {
_ = self.dest.lock().ok().and_then(|mut d| {
write(
d.writer.deref_mut(),
record,
(self.print_level, self.print_module_names),
)
});
_ = write(
&mut std::io::stderr(),
record,
(self.print_level, self.print_module_names),
);
}
(Destination::Stderr, false) => {
_ = write(
&mut std::io::stderr(),
record,
(self.print_level, self.print_module_names),
);
}
}
}
fn flush(&self) {
self.dest
.lock()
.ok()
.and_then(|mut w| w.writer.flush().ok());
}
}