logforth-core 0.3.1

Core structs and functions for Logforth.
Documentation
// Copyright 2024 FastLabs Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::io::Write;
use std::panic;
use std::sync::Once;
use std::sync::OnceLock;

use crate::Append;
use crate::Diagnostic;
use crate::Error;
use crate::Filter;
use crate::filter::FilterResult;
use crate::record::FilterCriteria;
use crate::record::Record;

static DEFAULT_LOGGER: OnceLock<Logger> = OnceLock::new();

/// Return the default global logger instance.
///
/// If no default logger has been set, `None` is returned.
pub fn default_logger() -> &'static Logger {
    static NOP_LOGGER: Logger = Logger { dispatches: vec![] };
    DEFAULT_LOGGER.get().unwrap_or(&NOP_LOGGER)
}

/// Set the default global logger instance.
///
/// If a default logger has already been set, the function returns the provided logger
/// as an error.
pub fn set_default_logger(logger: Logger) -> Result<(), Logger> {
    static ATEXIT_CALLBACK: Once = Once::new();

    DEFAULT_LOGGER.set(logger)?;
    ATEXIT_CALLBACK.call_once(flush_default_logger_at_exit);
    Ok(())
}

fn flush_default_logger_at_exit() {
    // Rust never calls `drop` for static variables.
    //
    // Setting up an exit handler gives us a chance to flush the default logger
    // once at the program exit, thus we don't lose the last logs.

    extern "C" fn handler() {
        if let Some(default_logger) = DEFAULT_LOGGER.get() {
            default_logger.exit();
        }
    }

    #[must_use]
    fn try_atexit() -> bool {
        use std::os::raw::c_int;

        unsafe extern "C" {
            fn atexit(cb: extern "C" fn()) -> c_int;
        }

        (unsafe { atexit(handler) }) == 0
    }

    fn hook_panic() {
        let previous_hook = panic::take_hook();

        panic::set_hook(Box::new(move |info| {
            handler();
            previous_hook(info);
        }));
    }

    if !try_atexit() {
        // if we failed to register the `atexit` handler, at least we hook into panic
        hook_panic();
    }
}

/// A logger that dispatches log records to one or more dispatcher.
#[derive(Debug)]
pub struct Logger {
    dispatches: Vec<Dispatch>,
}

impl Logger {
    pub(super) fn new(dispatches: Vec<Dispatch>) -> Self {
        Self { dispatches }
    }
}

impl Logger {
    /// Determine if a log message with the specified metadata would be logged.
    pub fn enabled(&self, criteria: &FilterCriteria) -> bool {
        self.dispatches
            .iter()
            .any(|dispatch| dispatch.enabled(criteria))
    }

    /// Log the [`Record`].
    pub fn log(&self, record: &Record) {
        for dispatch in &self.dispatches {
            for err in dispatch.log(record) {
                handle_log_error(record, &err);
            }
        }
    }

    /// Flush any buffered records.
    pub fn flush(&self) {
        for dispatch in &self.dispatches {
            for err in dispatch.flush() {
                handle_flush_error(&err);
            }
        }
    }

    /// Perform any cleanup work before the program exits.
    pub fn exit(&self) {
        for dispatch in &self.dispatches {
            for err in dispatch.exit() {
                handle_exit_error(&err);
            }
        }
    }
}

/// A grouped set of appenders and filters.
///
/// The [`Logger`] facade dispatches log records to one or more [`Dispatch`] instances.
/// Each [`Dispatch`] instance contains a set of filters and appenders.
///
/// `filters` are used to determine whether a log record should be passed to the appenders.
/// `appends` are used to write log records to a destination.
#[derive(Debug)]
pub(super) struct Dispatch {
    filters: Vec<Box<dyn Filter>>,
    diagnostics: Vec<Box<dyn Diagnostic>>,
    appends: Vec<Box<dyn Append>>,
}

impl Dispatch {
    pub(super) fn new(
        filters: Vec<Box<dyn Filter>>,
        diagnostics: Vec<Box<dyn Diagnostic>>,
        appends: Vec<Box<dyn Append>>,
    ) -> Self {
        debug_assert!(
            !appends.is_empty(),
            "A Dispatch must have at least one filter"
        );

        Self {
            filters,
            diagnostics,
            appends,
        }
    }

    fn enabled(&self, criteria: &FilterCriteria) -> bool {
        let diagnostics = &self.diagnostics;

        for filter in &self.filters {
            match filter.enabled(criteria, diagnostics) {
                FilterResult::Reject => return false,
                FilterResult::Accept => return true,
                FilterResult::Neutral => {}
            }
        }

        true
    }

    fn log(&self, record: &Record) -> Vec<Error> {
        let diagnostics = &self.diagnostics;

        for filter in &self.filters {
            match filter.matches(record, diagnostics) {
                FilterResult::Reject => return vec![],
                FilterResult::Accept => break,
                FilterResult::Neutral => {}
            }
        }

        let mut errors = vec![];
        for append in &self.appends {
            if let Err(err) = append.append(record, diagnostics) {
                errors.push(err);
            }
        }
        errors
    }

    fn flush(&self) -> Vec<Error> {
        let mut errors = vec![];
        for append in &self.appends {
            if let Err(err) = append.flush() {
                errors.push(err);
            }
        }
        errors
    }

    fn exit(&self) -> Vec<Error> {
        let mut errors = vec![];
        for append in &self.appends {
            if let Err(err) = append.exit() {
                errors.push(err);
            }
        }
        errors
    }
}

fn handle_log_error(record: &Record, error: &Error) {
    let Err(fallback_error) = write!(
        std::io::stderr(),
        r###"
Error perform logging.
    Attempted to log: {args}
    Record: {record:?}
    Error: {error:?}
"###,
        args = record.payload(),
        record = record,
        error = error,
    ) else {
        return;
    };

    panic!(
        r###"
Error performing stderr logging after error occurred during regular logging.
    Attempted to log: {args}
    Record: {record:?}
    Error: {error:?}
    Fallback error: {fallback_error}
"###,
        args = record.payload(),
        record = record,
        error = error,
        fallback_error = fallback_error,
    );
}

fn handle_flush_error(error: &Error) {
    let Err(fallback_error) = write!(
        std::io::stderr(),
        r###"
Error perform flush.
    Error: {error:?}
"###,
    ) else {
        return;
    };

    panic!(
        r###"
Error performing stderr logging after error occurred during regular flush.
    Error: {error:?}
    Fallback error: {fallback_error}
"###,
    );
}

fn handle_exit_error(error: &Error) {
    let Err(fallback_error) = write!(
        std::io::stderr(),
        r###"
Error perform exit.
    Error: {error:?}
"###,
    ) else {
        return;
    };

    panic!(
        r###"
Error performing stderr logging after error occurred during atexit.
    Error: {error:?}
    Fallback error: {fallback_error}
"###,
    );
}