use std::io::Write;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::{Arc, Mutex, Once};
use log::{LevelFilter, Log, Metadata, Record};
const DEFAULT_LOG_LEVEL: Level = Level::Info;
static LOGGER_INIT: Once = Once::new();
static LEVEL_MIRROR: AtomicI32 = AtomicI32::new(DEFAULT_LOG_LEVEL as i32);
const FATAL_TARGET: &str = "quercus_core::debug::fatal";
pub trait LogSink: Send + Sync {
fn log(&self, level: i32, message: &str);
}
static SINK: Mutex<Option<Arc<dyn LogSink>>> = Mutex::new(None);
struct DispatcherLogger;
impl DispatcherLogger {
fn emit(&self, level: Level, message: &str) {
let line = format!("[{}] {}", level.name(), message);
let sink = SINK.lock().unwrap_or_else(|e| e.into_inner()).clone();
match sink {
Some(sink) => {
sink.log(level as i32, &line);
}
None => write_stderr(&line),
}
}
}
fn write_stderr(line: &str) {
let mut err = std::io::stderr();
let _ = writeln!(err, "{line}");
let _ = err.flush();
}
impl Log for DispatcherLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= log::max_level()
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let level = if record.target() == FATAL_TARGET {
Level::Fatal
} else {
level_from_facade(record.level())
};
self.emit(level, &record.args().to_string());
}
fn flush(&self) {
let _ = std::io::stderr().flush();
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Level {
Debug,
Info,
Warning,
Error,
Fatal,
}
impl Level {
pub fn from_code(code: i32) -> Option<Level> {
match code {
0 => Some(Level::Debug),
1 => Some(Level::Info),
2 => Some(Level::Warning),
3 => Some(Level::Error),
4 => Some(Level::Fatal),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
Level::Debug => "DEBUG",
Level::Info => "INFO",
Level::Warning => "WARNING",
Level::Error => "ERROR",
Level::Fatal => "FATAL",
}
}
fn to_filter(self) -> LevelFilter {
match self {
Level::Debug => LevelFilter::Debug,
Level::Info => LevelFilter::Info,
Level::Warning => LevelFilter::Warn,
Level::Error => LevelFilter::Error,
Level::Fatal => LevelFilter::Error,
}
}
fn to_log_level(self) -> log::Level {
match self {
Level::Debug => log::Level::Debug,
Level::Info => log::Level::Info,
Level::Warning => log::Level::Warn,
Level::Error | Level::Fatal => log::Level::Error,
}
}
}
fn level_from_facade(level: log::Level) -> Level {
match level {
log::Level::Debug => Level::Debug,
log::Level::Info => Level::Info,
log::Level::Warn => Level::Warning,
log::Level::Error => Level::Error,
log::Level::Trace => Level::Fatal,
}
}
fn ensure_logger() {
LOGGER_INIT.call_once(|| {
let _ = log::set_logger(&DispatcherLogger);
log::set_max_level(DEFAULT_LOG_LEVEL.to_filter());
});
}
pub fn set_log_sink(sink: Option<Arc<dyn LogSink>>) {
ensure_logger();
let old = {
let mut slot = SINK.lock().unwrap_or_else(|e| e.into_inner());
std::mem::replace(&mut *slot, sink)
};
std::mem::drop(old);
}
pub fn log_is_sink_installed() -> bool {
SINK.lock().unwrap_or_else(|e| e.into_inner()).is_some()
}
pub fn log_get_level() -> Level {
ensure_logger();
Level::from_code(LEVEL_MIRROR.load(Ordering::Relaxed)).unwrap_or(Level::Info)
}
pub fn log_set_level(level: Level) {
ensure_logger();
LEVEL_MIRROR.store(level as i32, Ordering::Relaxed);
log::set_max_level(level.to_filter());
}
pub fn log(level: Level, msg: &str) -> crate::error::Result<()> {
ensure_logger();
if (level as i32) < LEVEL_MIRROR.load(Ordering::Relaxed) {
return Ok(());
}
if level == Level::Fatal {
log::log!(target: FATAL_TARGET, log::Level::Error, "{}", msg);
} else {
log::log!(level.to_log_level(), "{}", msg);
}
Ok(())
}
pub fn log_raw(level: i32, msg: &str) -> crate::error::Result<()> {
match Level::from_code(level) {
Some(l) => log(l, msg),
None => {
ensure_logger();
write_stderr(&format!("[UNKNOWN] {msg}"));
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
static TEST_LOCK: Mutex<()> = Mutex::new(());
static DROP_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
fn test_lock() -> std::sync::MutexGuard<'static, ()> {
TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
struct RecordingSink(Arc<Mutex<Vec<(i32, String)>>>);
impl LogSink for RecordingSink {
fn log(&self, level: i32, message: &str) {
self.0
.lock()
.unwrap_or_else(|e| e.into_inner())
.push((level, message.to_string()));
}
}
impl Drop for RecordingSink {
fn drop(&mut self) {
DROP_COUNT.fetch_add(1, Ordering::SeqCst);
}
}
fn install_recording_sink() -> Arc<Mutex<Vec<(i32, String)>>> {
let captured = Arc::new(Mutex::new(Vec::new()));
set_log_sink(Some(Arc::new(RecordingSink(captured.clone()))));
captured
}
#[test]
fn level_from_code_valid() {
assert_eq!(Level::from_code(0), Some(Level::Debug));
assert_eq!(Level::from_code(1), Some(Level::Info));
assert_eq!(Level::from_code(2), Some(Level::Warning));
assert_eq!(Level::from_code(3), Some(Level::Error));
assert_eq!(Level::from_code(4), Some(Level::Fatal));
}
#[test]
fn level_from_code_out_of_range() {
assert_eq!(Level::from_code(-1), None);
assert_eq!(Level::from_code(5), None);
assert_eq!(Level::from_code(i32::MIN), None);
assert_eq!(Level::from_code(i32::MAX), None);
}
#[test]
fn level_name() {
assert_eq!(Level::Debug.name(), "DEBUG");
assert_eq!(Level::Info.name(), "INFO");
assert_eq!(Level::Warning.name(), "WARNING");
assert_eq!(Level::Error.name(), "ERROR");
assert_eq!(Level::Fatal.name(), "FATAL");
}
#[test]
fn level_discriminants_match_cpp_order() {
assert!((Level::Debug as i32) < (Level::Info as i32));
assert!((Level::Info as i32) < (Level::Warning as i32));
assert!((Level::Warning as i32) < (Level::Error as i32));
assert!((Level::Error as i32) < (Level::Fatal as i32));
}
#[test]
fn default_level_is_info() {
let _guard = test_lock();
assert_eq!(DEFAULT_LOG_LEVEL, Level::Info);
log_set_level(DEFAULT_LOG_LEVEL);
assert_eq!(log_get_level(), Level::Info);
assert_eq!(log::max_level(), DEFAULT_LOG_LEVEL.to_filter());
}
#[test]
fn set_get_level_round_trip() {
let _guard = test_lock();
for level in [
Level::Debug,
Level::Info,
Level::Warning,
Level::Error,
Level::Fatal,
] {
log_set_level(level);
assert_eq!(log_get_level(), level);
}
log_set_level(Level::Info); }
#[test]
fn log_returns_ok() {
let _guard = test_lock();
log_set_level(Level::Error);
assert!(log(Level::Info, "below threshold").is_ok());
assert!(log(Level::Error, "at threshold").is_ok());
assert!(log(Level::Fatal, "above threshold").is_ok());
log_set_level(Level::Info);
}
#[test]
fn log_raw_returns_ok_for_any_level() {
let _guard = test_lock();
for code in [-1, 0, 4, 5, i32::MIN, i32::MAX] {
assert!(log_raw(code, "raw message").is_ok());
}
}
#[test]
fn filtering_drops_below_threshold() {
let _guard = test_lock();
for threshold in [
Level::Debug,
Level::Info,
Level::Warning,
Level::Error,
Level::Fatal,
] {
log_set_level(threshold);
assert_eq!(log::max_level(), threshold.to_filter());
if threshold != Level::Debug {
assert!(
!log::log_enabled!(log::Level::Debug),
"debug traffic must be filtered at {threshold:?}"
);
}
assert!(log(Level::Debug, "x").is_ok());
assert!(log(Level::Fatal, "y").is_ok());
}
log_set_level(Level::Info);
}
#[test]
fn sink_receives_only_messages_that_pass_the_level() {
let _guard = test_lock();
set_log_sink(None);
log_set_level(Level::Warning);
let captured = install_recording_sink();
assert!(log_is_sink_installed());
assert!(log(Level::Debug, "debug").is_ok());
assert!(log(Level::Info, "info").is_ok());
assert!(log(Level::Warning, "warn").is_ok());
assert!(log(Level::Error, "error").is_ok());
assert!(log(Level::Fatal, "fatal").is_ok());
assert_eq!(
captured.lock().unwrap_or_else(|e| e.into_inner()).clone(),
vec![
(2, "[WARNING] warn".to_string()),
(3, "[ERROR] error".to_string()),
(4, "[FATAL] fatal".to_string()),
]
);
log_set_level(Level::Fatal);
captured.lock().unwrap_or_else(|e| e.into_inner()).clear();
assert!(log(Level::Error, "below fatal").is_ok());
assert!(log(Level::Fatal, "fatal again").is_ok());
assert_eq!(
captured.lock().unwrap_or_else(|e| e.into_inner()).clone(),
vec![(4, "[FATAL] fatal again".to_string())]
);
set_log_sink(None);
assert!(!log_is_sink_installed());
log_set_level(Level::Info);
}
#[test]
fn replacing_or_clearing_drops_the_old_sink_exactly_once() {
let _guard = test_lock();
set_log_sink(None);
DROP_COUNT.store(0, Ordering::SeqCst);
let first = Arc::new(Mutex::new(Vec::new()));
set_log_sink(Some(Arc::new(RecordingSink(first.clone()))));
assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 0);
let second = Arc::new(Mutex::new(Vec::new()));
set_log_sink(Some(Arc::new(RecordingSink(second.clone()))));
assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 1);
set_log_sink(None);
assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 2);
assert!(!log_is_sink_installed());
}
#[test]
fn clearing_restores_stderr_and_stops_sink_capture() {
let _guard = test_lock();
set_log_sink(None);
log_set_level(Level::Info);
let captured = install_recording_sink();
assert!(log(Level::Error, "captured").is_ok());
assert_eq!(captured.lock().unwrap_or_else(|e| e.into_inner()).len(), 1);
set_log_sink(None);
assert!(!log_is_sink_installed());
assert!(log(Level::Error, "to stderr").is_ok());
assert_eq!(
captured.lock().unwrap_or_else(|e| e.into_inner()).len(),
1,
"cleared sink must not be called"
);
}
}