use std::fs::OpenOptions;
use std::io::{self, Write};
use std::sync::{Arc, Mutex, Once};
static INIT: Once = Once::new();
static mut LOGGER: Option<Arc<Mutex<Logger>>> = None;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
}
impl LogLevel {
fn as_str(&self) -> &'static str {
match self {
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
}
}
}
struct Logger {
writer: Box<dyn Write + Send>,
level: LogLevel,
}
impl Logger {
fn log(&mut self, level: LogLevel, message: &str) -> io::Result<()> {
if level >= self.level {
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
writeln!(
self.writer,
"[{}] {}: {}",
timestamp,
level.as_str(),
message
)?;
self.writer.flush()?;
}
Ok(())
}
}
pub fn init_file_logger(path: &str) -> io::Result<()> {
let file = OpenOptions::new().create(true).append(true).open(path)?;
init_with_writer(Box::new(file))
}
pub fn init_with_writer(writer: Box<dyn Write + Send>) -> io::Result<()> {
init_with_writer_and_level(writer, LogLevel::Debug)
}
pub fn init_with_writer_and_level(
writer: Box<dyn Write + Send>,
level: LogLevel,
) -> io::Result<()> {
let result = Ok(());
INIT.call_once(|| {
let logger = Arc::new(Mutex::new(Logger { writer, level }));
unsafe {
LOGGER = Some(logger);
}
});
result
}
pub fn debug(message: &str) {
log(LogLevel::Debug, message);
}
pub fn info(message: &str) {
log(LogLevel::Info, message);
}
pub fn warn(message: &str) {
log(LogLevel::Warn, message);
}
pub fn error(message: &str) {
log(LogLevel::Error, message);
}
fn log(level: LogLevel, message: &str) {
unsafe {
if let Some(ref logger) = LOGGER {
if let Ok(mut logger) = logger.lock() {
let _ = logger.log(level, message);
}
}
}
}
pub fn log_debug<M: crate::core::Message>(message: impl Into<String>) -> crate::core::Cmd<M> {
let msg = message.into();
crate::commands::custom(move || {
debug(&msg);
None
})
}
pub fn log_info<M: crate::core::Message>(message: impl Into<String>) -> crate::core::Cmd<M> {
let msg = message.into();
crate::commands::custom(move || {
info(&msg);
None
})
}
pub fn log_warn<M: crate::core::Message>(message: impl Into<String>) -> crate::core::Cmd<M> {
let msg = message.into();
crate::commands::custom(move || {
warn(&msg);
None
})
}
pub fn log_error<M: crate::core::Message>(message: impl Into<String>) -> crate::core::Cmd<M> {
let msg = message.into();
crate::commands::custom(move || {
error(&msg);
None
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
struct TestWriter {
buffer: Arc<Mutex<Vec<u8>>>,
}
impl Write for TestWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buffer.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn test_logging_levels() {
let buffer = Arc::new(Mutex::new(Vec::new()));
let writer = TestWriter {
buffer: buffer.clone(),
};
let _ = init_with_writer_and_level(Box::new(writer), LogLevel::Info);
debug("Should not appear");
info("Should appear");
warn("Should also appear");
}
}