use crate::config::{LogLevel, LoggerConfig};
use crate::error::{LoggerError, Result};
use crate::formatters::{create_formatter, get_thread_info, CallerInfo, LogRecord};
use crate::writers::{ConsoleWriter, FileWriter, MultiWriter, Writer};
#[cfg(feature = "async")]
use crossbeam_channel::{unbounded, Receiver, Sender};
use once_cell::sync::OnceCell;
use parking_lot::{Mutex, RwLock};
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::Arguments;
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::{Duration, SystemTime};
static GLOBAL_LOGGER: OnceCell<Arc<LoggerInstance>> = OnceCell::new();
thread_local! {
static THREAD_LOCAL_LOGGER: RefCell<Option<Arc<LoggerInstance>>> = const { RefCell::new(None) };
}
pub fn with_scoped_logger<F, R>(logger: Arc<LoggerInstance>, f: F) -> R
where
F: FnOnce() -> R,
{
THREAD_LOCAL_LOGGER.with(|tl| {
let previous = tl.replace(Some(logger));
let result = f();
tl.replace(previous);
result
})
}
pub fn current_logger() -> Result<Arc<LoggerInstance>> {
THREAD_LOCAL_LOGGER.with(|tl| {
if let Some(logger) = tl.borrow().as_ref() {
return Ok(Arc::clone(logger));
}
if let Some(logger) = GLOBAL_LOGGER.get() {
return Ok(Arc::clone(logger));
}
if let Ok(()) = init_default() {
return Ok(Arc::clone(GLOBAL_LOGGER.get().unwrap()));
}
if let Some(logger) = GLOBAL_LOGGER.get() {
return Ok(Arc::clone(logger));
}
Err(LoggerError::NotInitialized)
})
}
#[cfg(feature = "async")]
#[derive(Debug)]
struct AsyncLogMessage {
record: LogRecord,
#[allow(dead_code)]
caller: Option<CallerInfo>,
#[allow(dead_code)]
module: Option<String>,
}
pub struct LoggerInstance {
config: RwLock<LoggerConfig>,
writer: Mutex<Box<dyn Writer>>,
#[cfg(feature = "async")]
async_sender: Option<Sender<AsyncLogMessage>>,
#[cfg(feature = "async")]
_async_handle: Option<JoinHandle<()>>,
stats: Mutex<LoggerStats>,
}
#[derive(Debug, Default, Clone)]
pub struct LoggerStats {
pub total_messages: u64,
pub messages_by_level: HashMap<LogLevel, u64>,
pub start_time: Option<SystemTime>,
pub error_count: u64,
}
impl LoggerInstance {
pub fn new(config: LoggerConfig) -> Result<Self> {
config.validate()?;
let mut multi_writer = MultiWriter::new();
if config.console_enabled {
let formatter = create_formatter(
config.format,
config.console.colors,
&config.datetime_format,
config.include_caller,
config.include_thread,
true, );
let console_writer = ConsoleWriter::new(config.console.use_stderr, formatter);
multi_writer = multi_writer.add_writer(Box::new(console_writer));
}
if config.file_enabled {
let formatter = create_formatter(
config.format,
false, &config.datetime_format,
config.include_caller,
config.include_thread,
true, );
let file_writer = FileWriter::new(config.file.clone(), formatter)?;
multi_writer = multi_writer.add_writer(Box::new(file_writer));
}
#[cfg(feature = "async")]
let (async_sender, async_handle) = if config.async_enabled {
let (sender, receiver) = unbounded();
let mut async_multi_writer = MultiWriter::new();
if config.console_enabled {
let formatter = create_formatter(
config.format,
config.console.colors,
&config.datetime_format,
config.include_caller,
config.include_thread,
true,
);
let console_writer = ConsoleWriter::new(config.console.use_stderr, formatter);
async_multi_writer = async_multi_writer.add_writer(Box::new(console_writer));
}
if config.file_enabled {
let formatter = create_formatter(
config.format,
false,
&config.datetime_format,
config.include_caller,
config.include_thread,
true,
);
let file_writer = FileWriter::new(config.file.clone(), formatter)?;
async_multi_writer = async_multi_writer.add_writer(Box::new(file_writer));
}
let writer_clone = Box::new(async_multi_writer);
let handle = Self::start_async_thread(receiver, writer_clone)?;
(Some(sender), Some(handle))
} else {
(None, None)
};
let stats = LoggerStats {
start_time: Some(SystemTime::now()),
..Default::default()
};
Ok(LoggerInstance {
config: RwLock::new(config),
writer: Mutex::new(Box::new(multi_writer)),
#[cfg(feature = "async")]
async_sender,
#[cfg(feature = "async")]
_async_handle: async_handle,
stats: Mutex::new(stats),
})
}
#[cfg(feature = "async")]
fn start_async_thread(
receiver: Receiver<AsyncLogMessage>,
mut writer: Box<dyn Writer>,
) -> Result<JoinHandle<()>> {
let handle = thread::Builder::new()
.name("firo-logger-async".to_string())
.spawn(move || {
let mut last_flush = SystemTime::now();
const FLUSH_INTERVAL: Duration = Duration::from_millis(100);
loop {
match receiver.recv_timeout(FLUSH_INTERVAL) {
Ok(msg) => {
let formatted = {
let config = LoggerConfig::default(); let formatter = create_formatter(
config.format,
false,
&config.datetime_format,
config.include_caller,
config.include_thread,
true,
);
formatter.format(&msg.record)
};
if writer.write(&msg.record, &formatted).is_err() {
}
}
Err(crossbeam_channel::RecvTimeoutError::Timeout) => {
if last_flush.elapsed().unwrap_or(Duration::ZERO) >= FLUSH_INTERVAL {
let _ = writer.flush();
last_flush = SystemTime::now();
}
}
Err(crossbeam_channel::RecvTimeoutError::Disconnected) => {
let _ = writer.flush();
break;
}
}
}
})?;
Ok(handle)
}
pub fn log_with_caller(
&self,
level: LogLevel,
args: Arguments,
caller: Option<CallerInfo>,
module: Option<&str>,
) -> Result<()> {
let caller_clone = caller.clone();
let config = self.config.read();
let effective_level = if let Some(module_name) = module {
config.effective_level(module_name)
} else {
config.level
};
if level > effective_level {
return Ok(());
}
let mut record = LogRecord::new(level, args);
if let Some(module_name) = module {
record = record.with_module(module_name);
}
if let Some(caller_info) = &caller_clone {
record = record.with_caller(caller_info.clone());
}
if config.include_thread {
record = record.with_thread(get_thread_info());
}
record = record.with_metadata_map(config.metadata.clone());
{
let mut stats = self.stats.lock();
stats.total_messages += 1;
*stats.messages_by_level.entry(level).or_insert(0) += 1;
}
drop(config);
#[cfg(feature = "async")]
if let Some(ref sender) = self.async_sender {
let async_msg = AsyncLogMessage {
record,
caller: caller_clone,
module: module.map(|s| s.to_string()),
};
sender.send(async_msg).map_err(|_| {
let mut stats = self.stats.lock();
stats.error_count += 1;
LoggerError::Channel("Failed to send message to async thread".to_string())
})?;
} else {
let config = self.config.read();
let formatter = create_formatter(
config.format,
config.console.colors,
&config.datetime_format,
config.include_caller,
config.include_thread,
true,
);
let formatted = formatter.format(&record);
drop(config);
let mut writer = self.writer.lock();
writer.write(&record, &formatted).inspect_err(|_e| {
let mut stats = self.stats.lock();
stats.error_count += 1;
})?;
}
#[cfg(not(feature = "async"))]
{
let config = self.config.read();
let formatter = create_formatter(
config.format,
config.console.colors,
&config.datetime_format,
config.include_caller,
config.include_thread,
true,
);
let formatted = formatter.format(&record);
drop(config);
let mut writer = self.writer.lock();
writer.write(&record, &formatted).map_err(|e| {
let mut stats = self.stats.lock();
stats.error_count += 1;
e
})?;
}
Ok(())
}
pub fn log(&self, level: LogLevel, args: Arguments) -> Result<()> {
self.log_with_caller(level, args, None, None)
}
pub fn error(&self, args: Arguments) -> Result<()> {
self.log(LogLevel::Error, args)
}
pub fn warning(&self, args: Arguments) -> Result<()> {
self.log(LogLevel::Warning, args)
}
pub fn info(&self, args: Arguments) -> Result<()> {
self.log(LogLevel::Info, args)
}
pub fn success(&self, args: Arguments) -> Result<()> {
self.log(LogLevel::Success, args)
}
pub fn debug(&self, args: Arguments) -> Result<()> {
self.log(LogLevel::Debug, args)
}
pub fn flush(&self) -> Result<()> {
#[cfg(feature = "async")]
if self.async_sender.is_some() {
return Ok(());
}
let mut writer = self.writer.lock();
writer.flush()
}
pub fn config(&self) -> LoggerConfig {
self.config.read().clone()
}
pub fn update_config(&self, new_config: LoggerConfig) -> Result<()> {
new_config.validate()?;
*self.config.write() = new_config;
Ok(())
}
pub fn stats(&self) -> LoggerStats {
self.stats.lock().clone()
}
pub fn reset_stats(&self) {
let mut stats = self.stats.lock();
*stats = LoggerStats {
start_time: Some(SystemTime::now()),
..Default::default()
};
}
}
impl Drop for LoggerInstance {
fn drop(&mut self) {
let _ = self.flush();
#[cfg(feature = "async")]
if let Some(sender) = &self.async_sender {
let _ = sender;
}
}
}
pub fn init(config: LoggerConfig) -> Result<()> {
let logger = Arc::new(LoggerInstance::new(config)?);
GLOBAL_LOGGER
.set(logger)
.map_err(|_| LoggerError::AlreadyInitialized)?;
Ok(())
}
pub fn init_default() -> Result<()> {
init(LoggerConfig::default())
}
pub fn init_from_env() -> Result<()> {
init(LoggerConfig::from_env())
}
pub fn logger() -> Result<&'static Arc<LoggerInstance>> {
GLOBAL_LOGGER.get().ok_or(LoggerError::NotInitialized)
}
pub fn is_initialized() -> bool {
GLOBAL_LOGGER.get().is_some()
}
pub fn log_error(args: Arguments) -> Result<()> {
current_logger()?.error(args)
}
pub fn log_warning(args: Arguments) -> Result<()> {
current_logger()?.warning(args)
}
pub fn log_info(args: Arguments) -> Result<()> {
current_logger()?.info(args)
}
pub fn log_success(args: Arguments) -> Result<()> {
current_logger()?.success(args)
}
pub fn log_debug(args: Arguments) -> Result<()> {
current_logger()?.debug(args)
}
pub fn log_with_caller(
level: LogLevel,
args: Arguments,
caller: Option<CallerInfo>,
module: Option<&str>,
) -> Result<()> {
current_logger()?.log_with_caller(level, args, caller, module)
}
pub fn flush() -> Result<()> {
current_logger()?.flush()
}
pub fn config() -> Result<LoggerConfig> {
Ok(current_logger()?.config())
}
pub fn stats() -> Result<LoggerStats> {
Ok(current_logger()?.stats())
}
#[doc(hidden)]
pub fn __log_with_location(
level: LogLevel,
args: Arguments,
file: &'static str,
line: u32,
module: Option<&'static str>,
) -> Result<()> {
let caller = CallerInfo { file, line, module };
log_with_caller(level, args, Some(caller), module)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_logger_creation() {
let config = LoggerConfig::default();
let logger = LoggerInstance::new(config).unwrap();
assert!(logger.info(format_args!("Test message")).is_ok());
assert!(logger.flush().is_ok());
}
#[test]
fn test_global_logger_init() {
let config = LoggerConfig::builder()
.level(LogLevel::Debug)
.console(true)
.colors(false)
.build();
assert!(init(config).is_ok());
assert!(is_initialized());
assert!(log_info(format_args!("Global logger test")).is_ok());
assert!(flush().is_ok());
}
#[test]
fn test_level_filtering() -> Result<()> {
let config = LoggerConfig::builder()
.level(LogLevel::Warning)
.console(true)
.colors(false)
.build();
let logger = LoggerInstance::new(config)?;
assert!(logger.error(format_args!("Error message")).is_ok());
assert!(logger.warning(format_args!("Warning message")).is_ok());
assert!(logger.info(format_args!("Info message")).is_ok());
assert!(logger.success(format_args!("Success message")).is_ok());
assert!(logger.debug(format_args!("Debug message")).is_ok());
Ok(())
}
#[test]
fn test_file_logging() -> Result<()> {
let temp_file = NamedTempFile::new()?;
let config = LoggerConfig::builder()
.console(false)
.file(temp_file.path())
.build();
let logger = LoggerInstance::new(config)?;
logger.info(format_args!("File test message"))?;
logger.flush()?;
let content = std::fs::read_to_string(temp_file.path())?;
assert!(content.contains("File test message"));
Ok(())
}
#[test]
fn test_async_logging() -> Result<()> {
let config = LoggerConfig::builder()
.console(true)
.colors(false)
.async_logging(100)
.build();
let logger = LoggerInstance::new(config)?;
for i in 0..10 {
logger.info(format_args!("Async message {i}"))?;
}
std::thread::sleep(Duration::from_millis(50));
Ok(())
}
#[test]
fn test_logger_stats() -> Result<()> {
let config = LoggerConfig::builder().console(true).colors(false).build();
let logger = LoggerInstance::new(config)?;
logger.error(format_args!("Error"))?;
logger.warning(format_args!("Warning"))?;
logger.info(format_args!("Info"))?;
let stats = logger.stats();
assert_eq!(stats.total_messages, 3);
assert_eq!(stats.messages_by_level.get(&LogLevel::Error), Some(&1));
assert_eq!(stats.messages_by_level.get(&LogLevel::Warning), Some(&1));
assert_eq!(stats.messages_by_level.get(&LogLevel::Info), Some(&1));
Ok(())
}
#[test]
fn test_module_filtering() -> Result<()> {
let mut config = LoggerConfig::builder()
.level(LogLevel::Warning)
.console(true)
.colors(false)
.build();
config
.module_filters
.insert("test_module".to_string(), LogLevel::Debug);
let logger = LoggerInstance::new(config)?;
assert!(logger
.log_with_caller(
LogLevel::Debug,
format_args!("Debug in test_module"),
None,
Some("test_module")
)
.is_ok());
assert!(logger
.log_with_caller(
LogLevel::Debug,
format_args!("Debug in other_module"),
None,
Some("other_module")
)
.is_ok());
Ok(())
}
}