use std::env;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::sync::{
Mutex, Once,
atomic::{AtomicBool, Ordering},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
Error = 0,
Warn = 1,
Info = 2,
Debug = 3,
Trace = 4,
}
static INIT: Once = Once::new();
static mut LOG_LEVEL: LogLevel = LogLevel::Warn;
static BUFFER: Mutex<Vec<String>> = Mutex::new(Vec::new());
static BUFFERING: AtomicBool = AtomicBool::new(false);
static LOG_FILE: Mutex<Option<File>> = Mutex::new(None);
pub fn init() {
INIT.call_once(|| {
let level = env::var("LOG_LEVEL")
.or_else(|_| env::var("RUST_LOG"))
.unwrap_or_else(|_| "WARN".to_string())
.to_uppercase();
unsafe {
LOG_LEVEL = match level.as_str() {
"ERROR" => LogLevel::Error,
"INFO" => LogLevel::Info,
"DEBUG" | "VERBOSE" => LogLevel::Debug, "TRACE" => LogLevel::Trace,
_ => LogLevel::Warn, };
}
if let Ok(log_file_path) = env::var("LOG_FILE") {
match OpenOptions::new()
.create(true)
.append(true)
.open(&log_file_path)
{
Ok(file) => {
if let Ok(mut log_file) = LOG_FILE.lock() {
*log_file = Some(file);
}
}
Err(e) => {
eprintln!("Failed to open log file '{log_file_path}': {e}");
}
}
}
});
}
#[derive(Debug, PartialEq, Eq)]
enum LogSink {
File,
Buffer,
Stderr,
}
const fn choose_sink(buffering: bool, has_file: bool) -> LogSink {
match (buffering, has_file) {
(_, true) => LogSink::File,
(true, false) => LogSink::Buffer,
(false, false) => LogSink::Stderr,
}
}
fn has_log_file() -> bool {
LOG_FILE.lock().is_ok_and(|f| f.is_some())
}
pub fn log(level: LogLevel, message: &str) {
unsafe {
if level <= LOG_LEVEL {
let (name, colour) = match level {
LogLevel::Error => ("ERROR", "\x1b[31m"), LogLevel::Warn => ("WARN", "\x1b[33m"), LogLevel::Info => ("INFO", "\x1b[32m"), LogLevel::Debug => ("DEBUG", "\x1b[34m"), LogLevel::Trace => ("TRACE", "\x1b[35m"), };
let now = std::time::SystemTime::now();
let datetime = now
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = datetime.as_secs();
let subsec_millis = datetime.subsec_millis();
let days_since_epoch = secs / 86400;
let days_since_1970 = days_since_epoch as i32;
let mut year = 1970;
let mut remaining_days = days_since_1970;
while remaining_days >= 365 {
let is_leap =
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
let days_in_year = if is_leap { 366 } else { 365 };
if remaining_days >= days_in_year {
remaining_days -= days_in_year;
year += 1;
} else {
break;
}
}
let month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let is_leap =
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
let mut month = 1;
let mut day = remaining_days + 1;
for &days_in_month in &month_days {
let actual_days = if month == 2 && is_leap {
29
} else {
days_in_month
};
if day > actual_days {
day -= actual_days;
month += 1;
} else {
break;
}
}
let seconds_in_day = secs % 86400;
let hours = seconds_in_day / 3600;
let minutes = (seconds_in_day % 3600) / 60;
let seconds = seconds_in_day % 60;
let timestamp = format!(
"{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02}.{subsec_millis:03}"
);
match choose_sink(BUFFERING.load(Ordering::Relaxed), has_log_file())
{
LogSink::File => {
if let Ok(mut log_file) = LOG_FILE.lock()
&& let Some(file) = log_file.as_mut()
{
let _ =
writeln!(file, "[{timestamp}] [{name}] {message}");
let _ = file.flush();
}
}
LogSink::Buffer => {
if let Ok(mut buffer) = BUFFER.lock() {
buffer.push(format!(
"[{timestamp}] [{colour}{name}\x1b[0m] {message}"
));
}
}
LogSink::Stderr => {
eprintln!(
"[{timestamp}] [{colour}{name}\x1b[0m] {message}"
);
}
}
}
}
}
pub fn enable_buffering() {
BUFFERING.store(true, Ordering::Relaxed);
}
pub fn disable_buffering() {
BUFFERING.store(false, Ordering::Relaxed);
}
pub fn flush_buffered_logs() {
disable_buffering();
if let Ok(mut buffer) = BUFFER.lock() {
for message in buffer.iter() {
eprintln!("{message}");
}
buffer.clear();
}
}
#[macro_export]
macro_rules! error {
($($arg:tt)*) => {
$crate::utils::logger::log($crate::utils::logger::LogLevel::Error, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! warn {
($($arg:tt)*) => {
$crate::utils::logger::log($crate::utils::logger::LogLevel::Warn, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! info {
($($arg:tt)*) => {
$crate::utils::logger::log($crate::utils::logger::LogLevel::Info, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! debug {
($($arg:tt)*) => {
$crate::utils::logger::log($crate::utils::logger::LogLevel::Debug, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! trace {
($($arg:tt)*) => {
$crate::utils::logger::log($crate::utils::logger::LogLevel::Trace, &format!($($arg)*))
};
}
#[cfg(test)]
mod tests {
use super::{LogSink, choose_sink};
#[test]
fn a_configured_file_bypasses_buffering_so_lines_are_not_duplicated() {
assert_eq!(choose_sink(true, true), LogSink::File);
assert_eq!(choose_sink(false, true), LogSink::File);
}
#[test]
fn without_a_file_buffering_routes_to_the_buffer_else_stderr() {
assert_eq!(choose_sink(true, false), LogSink::Buffer);
assert_eq!(choose_sink(false, false), LogSink::Stderr);
}
}