use std::io::Write;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::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);
struct StderrLogger;
impl Log for StderrLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= log::max_level()
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let name = quercus_level_name(record.level());
let mut err = std::io::stderr();
let _ = writeln!(err, "[{}] {}", name, record.args());
let _ = err.flush();
}
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 quercus_level_name(level: log::Level) -> &'static str {
match level {
log::Level::Debug => "DEBUG",
log::Level::Info => "INFO",
log::Level::Warn => "WARNING",
log::Level::Error => "ERROR",
log::Level::Trace => "FATAL",
}
}
fn ensure_logger() {
LOGGER_INIT.call_once(|| {
let _ = log::set_logger(&StderrLogger);
log::set_max_level(DEFAULT_LOG_LEVEL.to_filter());
});
}
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();
let facade_level = if level == Level::Fatal {
log::Level::Trace
} else {
level.to_log_level()
};
log::log!(facade_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();
let mut err = std::io::stderr();
let _ = writeln!(err, "[UNKNOWN] {}", msg);
let _ = err.flush();
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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() {
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() {
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() {
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() {
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() {
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);
}
}