use std::{
fs::{File, OpenOptions},
io::{self, BufWriter, Write},
path::Path,
sync::{Arc, Mutex, RwLock},
time,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
pub fn as_str(&self) -> &'static str {
match self {
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
}
}
}
pub struct LogEvent<'a> {
pub level: LogLevel,
pub message: &'a str,
pub timestamp: std::time::SystemTime,
}
pub trait Layout: Send + Sync {
fn format(&self, event: &LogEvent) -> String;
}
pub struct SimpleLayout;
impl Layout for SimpleLayout {
fn format(&self, event: &LogEvent) -> String {
format!("[{:<5}] {} {}", event.level.as_str(), get_now_str(event.timestamp), event.message)
}
}
pub trait Appender: Send + Sync {
fn append(&self, event: &LogEvent);
}
pub struct ConsoleAppender {
layout: Box<dyn Layout>,
}
impl ConsoleAppender {
pub fn new(layout: Box<dyn Layout>) -> Self {
Self { layout }
}
}
impl Appender for ConsoleAppender {
fn append(&self, event: &LogEvent) {
println!("{}", self.layout.format(event));
}
}
pub struct FileAppender {
layout: Box<dyn Layout>,
writer: Mutex<BufWriter<File>>,
}
impl FileAppender {
pub fn open(path: impl AsRef<Path>, layout: Box<dyn Layout>) -> io::Result<Self> {
let file = OpenOptions::new().create(true).append(true).open(path)?;
Ok(Self {
layout,
writer: Mutex::new(BufWriter::new(file)),
})
}
}
impl Appender for FileAppender {
fn append(&self, event: &LogEvent) {
let line = self.layout.format(event);
if let Ok(mut writer) = self.writer.lock()
&& writeln!(writer, "{line}").is_ok()
{
let _ = writer.flush();
}
}
}
static LOGGER: RwLock<Option<Arc<Logger>>> = RwLock::new(None);
pub struct Logger {
level: LogLevel,
appenders: Vec<Box<dyn Appender>>,
}
pub struct LoggerBuilder {
level: LogLevel,
appenders: Vec<Box<dyn Appender>>,
}
impl LoggerBuilder {
pub fn level(mut self, level: LogLevel) -> Self {
self.level = level;
self
}
pub fn appender(mut self, appender: Box<dyn Appender>) -> Self {
self.appenders.push(appender);
self
}
pub fn build(self) -> Logger {
Logger {
level: self.level,
appenders: self.appenders,
}
}
pub fn init(self) {
Logger::set_logger(self.build());
}
}
fn get_now_str(sys_time: time::SystemTime) -> String {
use chrono::{Local, DateTime};
let dt = DateTime::<Local>::from(sys_time);
dt.format("%Y-%m-%d %H:%M:%S").to_string()
}
impl Logger {
pub fn builder(level: LogLevel) -> LoggerBuilder {
LoggerBuilder {
level,
appenders: Vec::new(),
}
}
pub fn set_logger(logger: Logger) {
*LOGGER.write().unwrap_or_else(|e| e.into_inner()) = Some(Arc::new(logger));
}
pub fn init(level: LogLevel) {
Logger::set_logger(Logger {
level,
appenders: vec![Box::new(ConsoleAppender::new(Box::new(SimpleLayout)))],
});
}
pub fn get() -> Option<Arc<Logger>> {
LOGGER.read().unwrap_or_else(|e| e.into_inner()).clone()
}
pub fn is_enabled(&self, level: LogLevel) -> bool {
level >= self.level
}
pub fn log(&self, level: LogLevel, message: &str) {
if self.is_enabled(level) {
let event = LogEvent {
level,
message,
timestamp: std::time::SystemTime::now(),
};
for appender in &self.appenders {
appender.append(&event);
}
}
}
}
#[macro_export]
macro_rules! trace {
($($arg:tt)*) => {
if let Some(logger) = $crate::Logger::get() {
if logger.is_enabled($crate::LogLevel::Trace) {
logger.log($crate::LogLevel::Trace, &format!($($arg)*));
}
}
};
}
#[macro_export]
macro_rules! debug {
($($arg:tt)*) => {
if let Some(logger) = $crate::Logger::get() {
if logger.is_enabled($crate::LogLevel::Debug) {
logger.log($crate::LogLevel::Debug, &format!($($arg)*));
}
}
};
}
#[macro_export]
macro_rules! info {
($($arg:tt)*) => {
if let Some(logger) = $crate::Logger::get() {
if logger.is_enabled($crate::LogLevel::Info) {
logger.log($crate::LogLevel::Info, &format!($($arg)*));
}
}
};
}
#[macro_export]
macro_rules! warn {
($($arg:tt)*) => {
if let Some(logger) = $crate::Logger::get() {
if logger.is_enabled($crate::LogLevel::Warn) {
logger.log($crate::LogLevel::Warn, &format!($($arg)*));
}
}
};
}
#[macro_export]
macro_rules! error {
($($arg:tt)*) => {
if let Some(logger) = $crate::Logger::get() {
if logger.is_enabled($crate::LogLevel::Error) {
logger.log($crate::LogLevel::Error, &format!($($arg)*));
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
static TEST_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn init_reconfigures_logger() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
Logger::init(LogLevel::Error);
assert_eq!(Logger::get().unwrap().level, LogLevel::Error);
Logger::init(LogLevel::Trace);
assert_eq!(Logger::get().unwrap().level, LogLevel::Trace);
}
#[test]
fn is_enabled_filters_by_level() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
Logger::init(LogLevel::Info);
let logger = Logger::get().unwrap();
assert!(!logger.is_enabled(LogLevel::Trace));
assert!(!logger.is_enabled(LogLevel::Debug));
assert!(logger.is_enabled(LogLevel::Info));
assert!(logger.is_enabled(LogLevel::Warn));
assert!(logger.is_enabled(LogLevel::Error));
}
#[test]
fn custom_appender_receives_events() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
struct CaptureAppender(Arc<Mutex<Vec<String>>>);
impl Appender for CaptureAppender {
fn append(&self, event: &LogEvent) {
self.0.lock().unwrap().push(event.message.to_string());
}
}
let buffer = Arc::new(Mutex::new(Vec::new()));
Logger::builder(LogLevel::Info)
.appender(Box::new(CaptureAppender(buffer.clone())))
.init();
crate::info!("hello {}", 42);
crate::debug!("filtered at Info level");
let messages = buffer.lock().unwrap();
assert_eq!(messages.len(), 1);
assert_eq!(messages[0], "hello 42");
}
#[test]
fn file_appender_writes_to_disk() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let path = std::env::temp_dir().join(format!("log4rc-test-{}.log", std::process::id()));
let _ = std::fs::remove_file(&path);
Logger::builder(LogLevel::Info)
.appender(Box::new(FileAppender::open(&path, Box::new(SimpleLayout)).unwrap()))
.init();
crate::info!("disk message");
crate::debug!("filtered");
let contents = std::fs::read_to_string(&path).unwrap();
assert!(contents.contains("disk message"));
assert!(!contents.contains("filtered"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn concurrent_threads_log_to_file_without_loss() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let path = std::env::temp_dir().join(format!("log4rc-threaded-{}.log", std::process::id()));
let _ = std::fs::remove_file(&path);
Logger::builder(LogLevel::Info)
.appender(Box::new(FileAppender::open(&path, Box::new(SimpleLayout)).unwrap()))
.init();
let threads: Vec<_> = (0..4)
.map(|t| {
std::thread::spawn(move || {
for i in 0..25 {
crate::info!("thread {} message {}", t, i);
}
})
})
.collect();
for thread in threads {
thread.join().unwrap();
}
let contents = std::fs::read_to_string(&path).unwrap();
assert_eq!(contents.lines().count(), 100);
let _ = std::fs::remove_file(&path);
}
}