#![deny(warnings, clippy::all, clippy::pedantic, missing_docs)]
#![forbid(unsafe_code)]
use std::sync::{Arc, RwLock, Weak};
use log::Log;
#[derive(Debug)]
pub struct LevelFilter<T> {
level: log::Level,
logger: T,
}
impl<T> LevelFilter<T> {
pub fn new(level: log::Level, logger: T) -> Self {
Self { level, logger }
}
pub fn level(&self) -> log::Level {
self.level
}
pub fn set_level(&mut self, level: log::Level) {
self.level = level;
}
fn level_passes(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= self.level
}
pub fn inner(&self) -> &T {
&self.logger
}
pub fn set_inner(&mut self, logger: T) {
self.logger = logger;
}
}
impl<T: Log> log::Log for LevelFilter<T> {
fn enabled(&self, metadata: &log::Metadata) -> bool {
self.level_passes(metadata) && self.logger.enabled(metadata)
}
fn log(&self, record: &log::Record) {
if self.level_passes(record.metadata()) {
self.logger.log(record);
}
}
fn flush(&self) {
self.logger.flush();
}
}
#[derive(Debug)]
pub struct ReloadLog<T> {
underlying: Arc<RwLock<T>>,
}
impl<T> ReloadLog<T> {
pub fn new(logger: T) -> Self {
Self {
underlying: Arc::new(RwLock::new(logger)),
}
}
#[must_use]
pub fn handle(&self) -> ReloadHandle<T> {
ReloadHandle {
underlying: Arc::downgrade(&self.underlying),
}
}
}
impl<T: Log> Log for ReloadLog<T> {
fn enabled(&self, metadata: &log::Metadata) -> bool {
self.underlying.read().is_ok_and(|l| l.enabled(metadata))
}
fn log(&self, record: &log::Record) {
let _ = self.underlying.read().map(|l| l.log(record));
}
fn flush(&self) {
let _ = self.underlying.read().map(|l| l.flush());
}
}
#[derive(Debug, Clone, Copy)]
pub enum ReloadError {
Gone,
Poisoned,
}
impl std::fmt::Display for ReloadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReloadError::Gone => write!(f, "Referenced logger was dropped"),
ReloadError::Poisoned => write!(f, "Lock poisoned"),
}
}
}
impl std::error::Error for ReloadError {}
#[derive(Debug, Clone)]
pub struct ReloadHandle<T> {
underlying: Weak<RwLock<T>>,
}
impl<T> ReloadHandle<T> {
pub fn replace(&self, logger: T) -> Result<(), ReloadError> {
let lock = self.underlying.upgrade().ok_or(ReloadError::Gone)?;
let mut guard = lock.write().unwrap_or_else(|error| {
lock.clear_poison();
error.into_inner()
});
*guard = logger;
Ok(())
}
pub fn modify<F>(&self, f: F) -> Result<(), ReloadError>
where
F: FnOnce(&mut T),
{
let lock = self.underlying.upgrade().ok_or(ReloadError::Gone)?;
let mut guard = lock.write().map_err(|_| ReloadError::Poisoned)?;
f(&mut *guard);
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::{LevelFilter, ReloadLog};
use log::{Log, Record};
use similar_asserts::assert_eq;
use std::sync::{Arc, Mutex};
struct CollectMessages {
messages: Mutex<Vec<String>>,
}
impl CollectMessages {
fn new() -> Self {
Self {
messages: Mutex::new(Vec::new()),
}
}
}
impl Log for CollectMessages {
fn enabled(&self, _metadata: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
let mut guard = self.messages.try_lock().unwrap();
guard.push(format!("{}", record.args()));
}
fn flush(&self) {}
}
#[test]
fn sanity_check_log_level_ordering() {
use log::Level;
assert!(Level::Error <= Level::Warn);
assert!(Level::Warn <= Level::Warn);
assert!(Level::Debug >= Level::Warn);
}
#[test]
fn level_filter() {
let collect_logs = Arc::new(CollectMessages::new());
let mut filter = LevelFilter::new(log::Level::Warn, collect_logs.clone());
for level in log::Level::iter() {
filter.log(
&Record::builder()
.level(level)
.args(format_args!("{level}"))
.build(),
);
}
let mut messages = collect_logs.messages.try_lock().unwrap();
assert_eq!(*messages, vec!["ERROR", "WARN"]);
messages.clear();
drop(messages);
filter.set_level(log::Level::Debug);
for level in log::Level::iter() {
filter.log(
&Record::builder()
.level(level)
.args(format_args!("{level}"))
.build(),
);
}
let messages = collect_logs.messages.try_lock().unwrap();
assert_eq!(*messages, &["ERROR", "WARN", "INFO", "DEBUG"]);
}
#[test]
fn reloadlog_replace() {
let collect_logs_1 = Arc::new(CollectMessages::new());
let collect_logs_2 = Arc::new(CollectMessages::new());
let reload_log = ReloadLog::new(collect_logs_1.clone());
let reload_handle = reload_log.handle();
reload_log.log(&Record::builder().args(format_args!("Message 1")).build());
reload_handle.replace(collect_logs_2.clone()).unwrap();
reload_log.log(&Record::builder().args(format_args!("Message 2")).build());
let messages_1 = collect_logs_1.messages.try_lock().unwrap();
let messages_2 = collect_logs_2.messages.try_lock().unwrap();
assert_eq!(*messages_1, &["Message 1"]);
assert_eq!(*messages_2, &["Message 2"]);
}
#[test]
fn reloadlog_modify() {
let collect_logs = Arc::new(CollectMessages::new());
let reload_log = ReloadLog::new(collect_logs.clone());
let reload_handle = reload_log.handle();
reload_log.log(&Record::builder().args(format_args!("Message 1")).build());
let messages = collect_logs.messages.try_lock().unwrap();
assert_eq!(*messages, &["Message 1"]);
drop(messages);
reload_handle
.modify(|l| l.messages.try_lock().unwrap().clear())
.unwrap();
reload_log.log(&Record::builder().args(format_args!("Message 2")).build());
let messages = collect_logs.messages.try_lock().unwrap();
assert_eq!(*messages, &["Message 2"]);
}
}