use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Mutex, OnceLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum LogLevel {
Error = 0,
Warn = 1,
Info = 2,
Debug = 3,
Trace = 4,
}
impl LogLevel {
pub fn from_name(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"error" => Some(Self::Error),
"warn" | "warning" => Some(Self::Warn),
"info" => Some(Self::Info),
"debug" => Some(Self::Debug),
"trace" => Some(Self::Trace),
_ => None,
}
}
pub fn label(self) -> &'static str {
match self {
Self::Error => "ERROR",
Self::Warn => "WARN",
Self::Info => "INFO",
Self::Debug => "DEBUG",
Self::Trace => "TRACE",
}
}
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
impl std::str::FromStr for LogLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_name(s).ok_or_else(|| format!("unknown log level: {s:?}"))
}
}
pub type LogBackend = Box<dyn Fn(LogLevel, &str) + Send + Sync + 'static>;
static GLOBAL_LOG_LEVEL: AtomicU8 = AtomicU8::new(LogLevel::Warn as u8);
fn global_backend() -> &'static Mutex<Option<LogBackend>> {
static BACKEND: OnceLock<Mutex<Option<LogBackend>>> = OnceLock::new();
BACKEND.get_or_init(|| Mutex::new(None))
}
pub fn set_log_level(level: LogLevel) {
GLOBAL_LOG_LEVEL.store(level as u8, Ordering::Relaxed);
}
pub fn current_log_level() -> LogLevel {
match GLOBAL_LOG_LEVEL.load(Ordering::Relaxed) {
0 => LogLevel::Error,
1 => LogLevel::Warn,
2 => LogLevel::Info,
3 => LogLevel::Debug,
_ => LogLevel::Trace,
}
}
pub fn set_log_backend(backend: Option<LogBackend>) {
if let Ok(mut guard) = global_backend().lock() {
*guard = backend;
}
}
#[inline]
pub fn emit(level: LogLevel, message: &str) {
if level > current_log_level() {
return;
}
match global_backend().lock() {
Ok(guard) if guard.is_some() => {
(guard.as_ref().expect("checked is_some"))(level, message);
}
_ => {
eprintln!("[tenflowers::{level}] {message}");
}
}
}
pub fn init_from_env() {
if let Ok(val) = std::env::var("TENFLOWERS_LOG") {
if let Some(level) = LogLevel::from_name(&val) {
set_log_level(level);
}
}
}
#[macro_export]
macro_rules! log_error {
($($arg:tt)*) => {{
$crate::logging::emit($crate::logging::LogLevel::Error, &format!($($arg)*));
}};
}
#[macro_export]
macro_rules! log_warn {
($($arg:tt)*) => {{
$crate::logging::emit($crate::logging::LogLevel::Warn, &format!($($arg)*));
}};
}
#[macro_export]
macro_rules! log_info {
($($arg:tt)*) => {{
$crate::logging::emit($crate::logging::LogLevel::Info, &format!($($arg)*));
}};
}
#[macro_export]
macro_rules! log_debug {
($($arg:tt)*) => {{
$crate::logging::emit($crate::logging::LogLevel::Debug, &format!($($arg)*));
}};
}
#[macro_export]
macro_rules! log_trace {
($($arg:tt)*) => {{
$crate::logging::emit($crate::logging::LogLevel::Trace, &format!($($arg)*));
}};
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
fn reset_level() {
set_log_level(LogLevel::Warn);
}
#[test]
fn test_set_and_get_level() {
set_log_level(LogLevel::Info);
assert_eq!(current_log_level(), LogLevel::Info);
reset_level();
}
#[test]
fn test_level_ordering() {
assert!(LogLevel::Error < LogLevel::Warn);
assert!(LogLevel::Warn < LogLevel::Info);
assert!(LogLevel::Info < LogLevel::Debug);
assert!(LogLevel::Debug < LogLevel::Trace);
}
#[test]
fn test_level_from_str() {
assert_eq!(LogLevel::from_name("info"), Some(LogLevel::Info));
assert_eq!(LogLevel::from_name("WARN"), Some(LogLevel::Warn));
assert_eq!(LogLevel::from_name("trace"), Some(LogLevel::Trace));
assert_eq!(LogLevel::from_name("error"), Some(LogLevel::Error));
assert_eq!(LogLevel::from_name("???"), None);
}
#[test]
fn test_level_display() {
assert_eq!(LogLevel::Error.to_string(), "ERROR");
assert_eq!(LogLevel::Debug.to_string(), "DEBUG");
}
#[test]
fn test_emit_below_threshold_skipped() {
let count = Arc::new(Mutex::new(0u32));
let count_clone = Arc::clone(&count);
set_log_level(LogLevel::Warn);
set_log_backend(Some(Box::new(move |_, _| {
*count_clone.lock().unwrap() += 1;
})));
emit(LogLevel::Debug, "this should be suppressed");
assert_eq!(*count.lock().unwrap(), 0);
set_log_backend(None);
reset_level();
}
#[test]
fn test_emit_at_threshold_fires() {
let recorded = Arc::new(Mutex::new(Vec::<String>::new()));
let rec_clone = Arc::clone(&recorded);
set_log_level(LogLevel::Info);
set_log_backend(Some(Box::new(move |_, msg: &str| {
rec_clone.lock().unwrap().push(msg.to_owned());
})));
emit(LogLevel::Info, "hello from test");
let msgs = recorded.lock().unwrap().clone();
assert!(msgs.iter().any(|m| m.contains("hello from test")));
set_log_backend(None);
reset_level();
}
#[test]
fn test_init_from_env_no_variable() {
std::env::remove_var("TENFLOWERS_LOG");
init_from_env();
}
}