use log::{LevelFilter, Log};
use mtlog_core::{
spawn_log_thread_file, spawn_log_thread_stdout, FileLogger, LogFile, LogFileSizeRotation,
LogFileTimeRotation, LogMessage, LogSender, LogStdout,
};
pub use mtlog_core::{LogFilter, SizeRotationConfig, TimeRotationConfig};
use std::{
future::Future,
path::Path,
sync::{Arc, LazyLock, RwLock},
};
#[derive(Clone)]
struct LogConfig {
sender_file: Option<Arc<LogSender>>,
sender_stdout: Option<Arc<LogSender>>,
name: Option<String>,
level: LevelFilter,
filter: Option<LogFilter>,
}
static GLOBAL_LOG_CONFIG: LazyLock<Arc<RwLock<LogConfig>>> = LazyLock::new(|| {
log::set_boxed_logger(Box::new(MTLogger)).unwrap();
log::set_max_level(LevelFilter::Info);
let sender = spawn_log_thread_stdout(LogStdout::default());
Arc::new(RwLock::new(LogConfig {
sender_stdout: Some(Arc::new(sender)),
sender_file: None,
name: None,
level: LevelFilter::Info,
filter: None,
}))
});
tokio::task_local! {
pub static LOG_CONFIG: LogConfig;
}
struct MTLogger;
impl Log for MTLogger {
fn enabled(&self, _: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
LOG_CONFIG.with(|config| {
let level = record.level();
if level > config.level {
return;
}
if let Some(ref filter) = config.filter {
if !filter.is_match(record.target(), &record.args().to_string()) {
return;
}
}
let log_message = Arc::new(LogMessage {
level,
name: config.name.clone(),
message: record.args().to_string(),
});
if let Some(sender) = &config.sender_stdout {
sender
.send(log_message.clone())
.expect("Unable to send log message to stdout logging thread");
}
if let Some(sender) = &config.sender_file {
sender
.send(log_message)
.expect("Unable to send log message to file logging thread");
}
});
}
fn flush(&self) {
if let Some(s) = GLOBAL_LOG_CONFIG.write().unwrap().sender_stdout.as_deref() {
s.shutdown();
}
if let Some(s) = GLOBAL_LOG_CONFIG.write().unwrap().sender_file.as_deref() {
s.shutdown();
}
}
}
pub struct ConfigBuilder {
log_file: Option<FileLogger>,
no_stdout: bool,
no_file: bool,
log_level: LevelFilter,
name: Option<String>,
filter: Option<LogFilter>,
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self {
log_file: None,
no_stdout: false,
no_file: false,
log_level: LevelFilter::Info,
name: None,
filter: None,
}
}
}
impl ConfigBuilder {
fn build(self) -> LogConfig {
let Self {
log_file,
no_stdout,
no_file,
log_level,
name,
filter,
} = self;
let sender_file = if no_file {
None
} else if let Some(log_file) = log_file {
let sender = spawn_log_thread_file(log_file);
Some(Arc::new(sender))
} else {
GLOBAL_LOG_CONFIG.read().unwrap().sender_file.clone()
};
let sender_stdout = if no_stdout {
None
} else {
GLOBAL_LOG_CONFIG.read().unwrap().sender_stdout.clone()
};
LogConfig {
sender_file,
sender_stdout,
name,
level: log_level,
filter,
}
}
pub fn with_log_file<P: AsRef<Path>>(self, path: P) -> Result<Self, std::io::Error> {
Ok(Self {
log_file: Some(FileLogger::Single(LogFile::new(path)?)),
..self
})
}
pub fn maybe_with_log_file<P: AsRef<Path>>(
self,
path: Option<P>,
) -> Result<Self, std::io::Error> {
Ok(Self {
log_file: path
.map(|p| LogFile::new(p).map(FileLogger::Single))
.transpose()?,
..self
})
}
pub fn with_time_rotation(self, config: TimeRotationConfig) -> Result<Self, std::io::Error> {
Ok(Self {
log_file: Some(FileLogger::TimeRotation(LogFileTimeRotation::new(config)?)),
..self
})
}
pub fn with_size_rotation(self, config: SizeRotationConfig) -> Result<Self, std::io::Error> {
Ok(Self {
log_file: Some(FileLogger::SizeRotation(LogFileSizeRotation::new(config)?)),
..self
})
}
pub fn no_stdout(self) -> Self {
Self {
no_stdout: true,
..self
}
}
pub fn with_stdout(self, yes: bool) -> Self {
Self {
no_stdout: !yes,
..self
}
}
pub fn no_file(self) -> Self {
Self {
no_file: true,
..self
}
}
pub fn with_name(self, name: &str) -> Self {
Self {
name: Some(name.into()),
..self
}
}
pub fn maybe_with_name(self, name: Option<&str>) -> Self {
Self {
name: name.map(String::from),
..self
}
}
pub fn with_filter(self, filter: LogFilter) -> Self {
Self {
filter: Some(filter),
..self
}
}
pub async fn scope_global<F: Future>(self, f: F) -> F::Output {
let config = self.build();
let mut senders = Vec::new();
if let Some(ref sender) = config.sender_stdout {
senders.push(Arc::clone(sender));
}
if let Some(ref sender) = config.sender_file {
senders.push(Arc::clone(sender));
}
*GLOBAL_LOG_CONFIG.write().unwrap() = config.clone();
let result = LOG_CONFIG.scope(config, f).await;
for sender in senders {
sender.shutdown();
}
result
}
pub async fn scope_local<F: Future>(self, f: F) -> F::Output {
LOG_CONFIG.scope(self.build(), f).await
}
}
pub fn logger_config() -> ConfigBuilder {
ConfigBuilder::default()
}