log-wrap 0.1.0

Utilities for log by wrapping `Box<dyn log::Log>`, such as log filtering, log capturing
Documentation
#![doc = include_str!("../README.md")]
#![feature(local_key_cell_methods)]

/// log wrapper
pub struct LogWrap {
    pub enabled: Box<dyn Fn(&dyn log::Log, &log::Metadata) -> bool + Send + Sync>,
    pub log: Box<dyn Fn(&dyn log::Log, &log::Record) + Send + Sync>,
    pub logger: Box<dyn log::Log>,
}

impl log::Log for LogWrap {
    fn enabled(&self, metadata: &log::Metadata) -> bool {
        (self.enabled)(self.logger.as_ref(), metadata)
    }

    fn log(&self, record: &log::Record) {
        (self.log)(self.logger.as_ref(), record)
    }

    fn flush(&self) {
        self.logger.flush()
    }
}

impl From<Box<dyn log::Log>> for LogWrap {
    fn from(value: Box<dyn log::Log>) -> Self {
        Self::new(value)
    }
}

/// Basic log-wrapper methods
impl LogWrap {
    pub fn new(logger: Box<dyn log::Log>) -> Self {
        Self {
            enabled: Box::new(|prev, metadata| prev.enabled(metadata)),
            log: Box::new(|prev, record| prev.log(record)),
            logger,
        }
    }

    /// Make self as the default logger
    #[cfg(feature = "std")]
    pub fn init(self) -> Result<(), log::SetLoggerError> {
        log::set_boxed_logger(Box::new(self))
    }

    /// Make self as the default logger, set set the max level
    #[cfg(feature = "std")]
    pub fn init_with_default_level(self) -> Result<(), log::SetLoggerError> {
        log::set_max_level(if cfg!(debug_assertions) {
            log::LevelFilter::Debug
        } else {
            log::LevelFilter::Info
        });
        self.init()
    }

    /// Intecept the [`log::Log::log`] method, callback function's first param is the original Log object
    pub fn log(self, f: impl Fn(&dyn log::Log, &log::Record) + Send + Sync + 'static) -> Self {
        Self {
            log: Box::new(f),
            ..self
        }
    }

    /// Intecept the [`log::Log::log`] method in append mode, new callback and old callback are all enabled
    pub fn chain(self, f: impl Fn(&dyn log::Log, &log::Record) + Send + Sync + 'static) -> Self {
        let prev = self.log;
        Self {
            log: Box::new(move |l, r| {
                prev(l, r);
                f(l, r);
            }),
            ..self
        }
    }

    /// Intecept the [`log::Log::log`] method in filter mode, old callback is only invoked when old callback return true
    pub fn filter(
        self,
        f: impl Fn(&dyn log::Log, &log::Record) -> bool + Send + Sync + 'static,
    ) -> Self {
        let prev = self.log;
        Self {
            log: Box::new(move |l, r| {
                if f(l, r) {
                    prev(l, r);
                }
            }),
            ..self
        }
    }

    /// Intecept the [`log::Log::enabled`] method
    pub fn enabled(
        self,
        f: impl Fn(&dyn log::Log, &log::Metadata) -> bool + Send + Sync + 'static,
    ) -> Self {
        Self {
            enabled: Box::new(f),
            ..self
        }
    }
}

// Utilities of log-wrapper
impl LogWrap {
    /// Discard the log output by these blacked modules
    pub fn black_module(self, mods: impl IntoIterator<Item = impl Into<String>>) -> Self {
        let mods = mods.into_iter().map(Into::into).collect::<Vec<_>>();
        self.filter(move |_, record| {
            if record
                .module_path()
                .or(record.module_path_static())
                .filter(|m| mods.iter().any(|s| m.starts_with(s)))
                .is_some()
            {
                return false;
            }
            true
        })
    }

    /// Enable log-capturing
    pub fn enable_thread_capture(self) -> Self {
        self.log(move |logger, record| {
            if ENABLED.get() {
                CAPTURED.with_borrow_mut(|v| v.push(LogInfo::from(record)));
                return;
            }
            logger.log(record);
        })
    }
}

static MUTEX: Mutex<()> = Mutex::new(());

pub struct CaptureGuard;

impl Drop for CaptureGuard {
    fn drop(&mut self) {
        ENABLED.set(false);
        let _guard = MUTEX.lock().unwrap();
        let logger = log::logger();
        for item in CAPTURED.take() {
            logger.log(
                &log::RecordBuilder::new()
                    .args(format_args!("{}", item.text))
                    .module_path(item.module.as_ref().map(AsRef::as_ref))
                    .file(item.file.as_ref().map(AsRef::as_ref))
                    .level(item.level)
                    .line((item.line > 0).then_some(item.line))
                    .build(),
            );
        }
    }
}

/// Capture subsequent logs in current thread, and output them when CaptureGuard dropping
pub fn capture_thread_log() -> CaptureGuard {
    ENABLED.set(true);
    CaptureGuard
}

#[derive(Debug, Clone, PartialEq)]
pub struct LogInfo {
    pub line: u32,
    pub level: log::Level,
    pub text: String,
    pub file: Option<Box<str>>,
    pub module: Option<Box<str>>,
}

impl From<&log::Record<'_>> for LogInfo {
    fn from(record: &log::Record) -> Self {
        LogInfo {
            line: record.line().unwrap_or(0),
            level: record.level(),
            file: record.file().or(record.file_static()).map(Into::into),
            text: record.args().to_string(),
            module: record
                .module_path()
                .or(record.module_path_static())
                .map(Into::into),
        }
    }
}

use std::{
    cell::{Cell, RefCell},
    sync::Mutex,
};

thread_local! {
    static CAPTURED: RefCell<Vec<LogInfo>> = RefCell::new(vec![]);
    static ENABLED: Cell<bool> = Cell::new(false);
}