use super::{Event, Level};
use crate::log::format::{JsonFormatter, PrettyFormatter};
use std::io::Write as _;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::OnceLock;
static GLOBAL_MIN_LEVEL: AtomicU8 = AtomicU8::new(Level::Trace as u8);
pub fn set_min_level(level: Level) {
GLOBAL_MIN_LEVEL.store(level as u8, Ordering::Release);
}
pub fn min_level() -> Level {
let raw = GLOBAL_MIN_LEVEL.load(Ordering::Acquire);
match raw {
0 => Level::Trace,
1 => Level::Debug,
2 => Level::Info,
3 => Level::Warn,
_ => Level::Error,
}
}
pub trait Subscriber: Send + Sync + 'static {
fn on_event(&self, event: &Event);
}
static GLOBAL_SUBSCRIBER: OnceLock<Box<dyn Subscriber>> = OnceLock::new();
pub fn set_global_subscriber(sub: impl Subscriber) -> Result<(), &'static str> {
GLOBAL_SUBSCRIBER
.set(Box::new(sub))
.map_err(|_| "global subscriber already set")
}
pub fn dispatch(event: &Event) {
if let Some(sub) = GLOBAL_SUBSCRIBER.get() {
sub.on_event(event);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogFormat {
Pretty,
Json,
}
pub struct LogSubscriber {
pub min_level: Level,
pub format: LogFormat,
}
impl LogSubscriber {
pub fn new(min_level: Level, format: LogFormat) -> Self {
Self { min_level, format }
}
pub fn from_env() -> Self {
let level = std::env::var("MODUVEX_LOG")
.ok()
.and_then(|s| parse_level(&s))
.unwrap_or(Level::Info);
let format = match std::env::var("MODUVEX_LOG_FORMAT")
.as_deref()
.unwrap_or("")
.to_ascii_lowercase()
.as_str()
{
"json" => LogFormat::Json,
_ => LogFormat::Pretty,
};
Self::new(level, format)
}
}
impl Subscriber for LogSubscriber {
fn on_event(&self, event: &Event) {
if event.level < self.min_level {
return;
}
let stderr = std::io::stderr();
let mut w = stderr.lock();
match self.format {
LogFormat::Pretty => {
let _ = PrettyFormatter::format(event, &mut w);
}
LogFormat::Json => {
let _ = JsonFormatter::format(event, &mut w);
}
}
let _ = w.flush();
}
}
pub fn parse_level(s: &str) -> Option<Level> {
match s.to_ascii_lowercase().as_str() {
"trace" => Some(Level::Trace),
"debug" => Some(Level::Debug),
"info" => Some(Level::Info),
"warn" | "warning" => Some(Level::Warn),
"error" => Some(Level::Error),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::log::Level;
use std::sync::{Arc, Mutex};
struct CaptureSub {
events: Arc<Mutex<Vec<Event>>>,
min: Level,
}
impl Subscriber for CaptureSub {
fn on_event(&self, event: &Event) {
if event.level >= self.min {
self.events.lock().unwrap().push(event.clone());
}
}
}
#[test]
fn dispatch_without_subscriber_is_noop() {
let event = Event::now(Level::Info, "test");
dispatch(&event);
}
#[test]
fn parse_level_valid() {
assert_eq!(parse_level("trace"), Some(Level::Trace));
assert_eq!(parse_level("debug"), Some(Level::Debug));
assert_eq!(parse_level("info"), Some(Level::Info));
assert_eq!(parse_level("warn"), Some(Level::Warn));
assert_eq!(parse_level("warning"), Some(Level::Warn));
assert_eq!(parse_level("error"), Some(Level::Error));
}
#[test]
fn parse_level_case_insensitive() {
assert_eq!(parse_level("INFO"), Some(Level::Info));
assert_eq!(parse_level("Debug"), Some(Level::Debug));
assert_eq!(parse_level("WARN"), Some(Level::Warn));
}
#[test]
fn parse_level_unknown_returns_none() {
assert_eq!(parse_level("verbose"), None);
assert_eq!(parse_level(""), None);
assert_eq!(parse_level("3"), None);
}
#[test]
fn set_min_level_and_read_back() {
let original = min_level();
set_min_level(Level::Warn);
assert_eq!(min_level(), Level::Warn);
set_min_level(Level::Trace);
assert_eq!(min_level(), Level::Trace);
set_min_level(original);
}
#[test]
fn log_subscriber_filters_below_min_level() {
let events = Arc::new(Mutex::new(Vec::new()));
let cap = CaptureSub {
events: Arc::clone(&events),
min: Level::Warn,
};
cap.on_event(&Event::now(Level::Debug, "debug msg"));
cap.on_event(&Event::now(Level::Info, "info msg"));
cap.on_event(&Event::now(Level::Warn, "warn msg"));
cap.on_event(&Event::now(Level::Error, "error msg"));
let captured = events.lock().unwrap();
assert_eq!(captured.len(), 2);
assert_eq!(captured[0].level, Level::Warn);
assert_eq!(captured[1].level, Level::Error);
}
#[test]
fn log_subscriber_pretty_writes_to_buffer() {
let sub = LogSubscriber::new(Level::Trace, LogFormat::Pretty);
sub.on_event(&Event::now(Level::Info, "pretty test").field("k", "v"));
}
#[test]
fn log_subscriber_json_writes_to_buffer() {
let sub = LogSubscriber::new(Level::Trace, LogFormat::Json);
sub.on_event(&Event::now(Level::Info, "json test").field("x", 1_i32));
}
#[test]
fn log_subscriber_from_env_defaults_to_info_pretty() {
std::env::remove_var("MODUVEX_LOG");
std::env::remove_var("MODUVEX_LOG_FORMAT");
let sub = LogSubscriber::from_env();
assert_eq!(sub.min_level, Level::Info);
assert_eq!(sub.format, LogFormat::Pretty);
}
#[test]
fn log_subscriber_from_env_reads_level() {
std::env::set_var("MODUVEX_LOG", "debug");
let sub = LogSubscriber::from_env();
assert_eq!(sub.min_level, Level::Debug);
std::env::remove_var("MODUVEX_LOG");
}
#[test]
fn log_subscriber_from_env_reads_json_format() {
std::env::set_var("MODUVEX_LOG_FORMAT", "json");
let sub = LogSubscriber::from_env();
assert_eq!(sub.format, LogFormat::Json);
std::env::remove_var("MODUVEX_LOG_FORMAT");
}
#[test]
fn log_subscriber_from_env_unknown_level_defaults_to_info() {
std::env::set_var("MODUVEX_LOG", "verbose");
let sub = LogSubscriber::from_env();
assert_eq!(sub.min_level, Level::Info);
std::env::remove_var("MODUVEX_LOG");
}
#[test]
fn log_format_eq() {
assert_eq!(LogFormat::Pretty, LogFormat::Pretty);
assert_eq!(LogFormat::Json, LogFormat::Json);
assert_ne!(LogFormat::Pretty, LogFormat::Json);
}
}