log-io 2.2.0

Structured logging pipeline for Rust: filter, format, sink. Allocation-free hot path, JSON / logfmt / human output that is safe for any input, thread and async context propagation, per-target filtering, durable shutdown, background and rotating file sinks, plus optional log and tracing bridges.
Documentation
//! The process-wide logger.
//!
//! Most programs build one [`Logger`] at startup and use it everywhere.
//! [`init`] installs it globally and returns a [`FlushGuard`]; the
//! logging macros use the global logger when called without a logger
//! argument (`log_io::info!("ready")`).
//!
//! The global logger lives in a `static`, and Rust never drops statics.
//! Keep the [`FlushGuard`] alive for the whole of `main` so buffered
//! records are flushed (and background writers drained) when `main`
//! returns:
//!
#![cfg_attr(all(feature = "json"), doc = "```no_run")]
#![cfg_attr(not(all(feature = "json")), doc = "```ignore")]
//! use log_io::{Level, Logger};
//!
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//!     let logger = Logger::builder().level(Level::Info).stdout_json().build();
//!     let _guard = log_io::init(logger)?;
//!
//!     log_io::info!("service started", port = 8080_u16);
//!     Ok(())
//! } // `_guard` drops here: every sink is flushed and shut down.
//! ```
//!
//! Exit paths that skip destructors (`std::process::exit`, `abort`,
//! fatal signals) also skip the guard: call [`shutdown`] before
//! `process::exit`, and see [`install_panic_hook`] for
//! `panic = "abort"` builds.

use std::sync::OnceLock;

use crate::error::Result;
use crate::level::Level;
use crate::logger::{Logger, in_dispatch};
use crate::record::{Field, Metadata};
use crate::sink::on_background_worker;

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

/// Install `logger` as the process-wide logger.
///
/// # Errors
///
/// Returns [`SetGlobalError`] (which hands the logger back) if a global
/// logger is already installed. The global logger can be set once.
///
/// # Examples
///
/// ```
/// use log_io::{Logger, sink::NullSink};
///
/// let first = Logger::builder().with_sink(NullSink::new()).build();
/// let second = first.clone();
/// let _ = log_io::set_global(first);
/// assert!(log_io::set_global(second).is_err());
/// ```
pub fn set(logger: Logger) -> core::result::Result<(), SetGlobalError> {
    GLOBAL.set(logger).map_err(SetGlobalError)
}

/// Install `logger` globally and return a guard that flushes and shuts
/// down its sinks when dropped. Keep the guard alive in `main`.
///
/// # Errors
///
/// Returns [`SetGlobalError`] if a global logger is already installed.
pub fn init(logger: Logger) -> core::result::Result<FlushGuard, SetGlobalError> {
    let guard = FlushGuard::new(logger.clone());
    set(logger)?;
    Ok(guard)
}

/// The process-wide logger, if one has been installed.
#[must_use]
pub fn get() -> Option<&'static Logger> {
    GLOBAL.get()
}

/// Flush the global logger's sinks. Does nothing if none is installed.
///
/// # Errors
///
/// Returns the first sink error (see [`Logger::flush`]).
pub fn flush() -> Result<()> {
    GLOBAL.get().map_or(Ok(()), Logger::flush)
}

/// Shut down the global logger's sinks: flush, drain background queues
/// and join their threads. Call it before `std::process::exit`. The
/// logger keeps working afterwards (background sinks write
/// synchronously). Does nothing if no global logger is installed.
///
/// # Errors
///
/// Returns the first sink error (see [`Logger::shutdown`]).
pub fn shutdown() -> Result<()> {
    GLOBAL.get().map_or(Ok(()), Logger::shutdown)
}

/// Install a panic hook that logs every panic through the global logger
/// and flushes it, then runs the previously installed hook.
///
/// The record is emitted at [`Level::Error`] with target `panic`, the
/// panic message as its message, the panic location as its source
/// location, and `thread` and `column` fields. Because the hook runs
/// before unwinding (or before the process aborts under
/// `panic = "abort"`), this preserves the final records that would
/// otherwise sit in a buffer.
///
/// The hook does not touch the logger, and only runs the previous hook
/// (which prints the panic message by default), when the panic happens
/// inside the logging pipeline itself:
///
/// * in a sink or format on the panicking thread, because the
///   panicking frame may hold a sink's lock;
/// * on the background thread of a [`crate::sink::NonBlocking`] sink
///   (its writer or `on_error` handler panicked), because flushing
///   would wait for that same thread. The sink reports
///   [`crate::Error::Closed`] afterwards and the process can still shut
///   down.
///
/// Install it once, after [`init`]; installing it twice logs each panic
/// twice.
pub fn install_panic_hook() {
    let previous = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        if !in_dispatch() && !on_background_worker() {
            if let Some(logger) = GLOBAL.get() {
                log_panic(logger, info);
            }
        }
        previous(info);
    }));
}

fn log_panic(logger: &Logger, info: &std::panic::PanicHookInfo<'_>) {
    let payload = info.payload();
    let message = payload
        .downcast_ref::<&str>()
        .copied()
        .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
        .unwrap_or("panic with a non-string payload");
    let thread = std::thread::current();
    let thread_name = thread.name().unwrap_or("<unnamed>");
    let mut metadata = Metadata::new(Level::Error, "panic");
    let mut fields = [
        Field::new("thread", thread_name),
        Field::new("column", 0_u32),
    ];
    let mut count = 1;
    if let Some(location) = info.location() {
        metadata = metadata.with_location(location.file(), location.line());
        fields[1] = Field::new("column", location.column());
        count = 2;
    }
    // Errors were already passed to the logger's error handler.
    let _reported = logger.try_emit(metadata, message, &fields[..count]);
    let _flushed = logger.flush();
}

/// Flushes and shuts down a logger's sinks when dropped.
///
/// Returned by [`init`] and [`Logger::flush_guard`]. Bind it to a named
/// variable (`let _guard = ...`), not `_`, which would drop it at once.
/// Errors during the final shutdown go to the logger's error handler.
#[must_use = "the guard flushes the logger when dropped; bind it to a named variable"]
#[derive(Debug)]
pub struct FlushGuard {
    logger: Logger,
}

impl FlushGuard {
    pub(crate) const fn new(logger: Logger) -> Self {
        Self { logger }
    }

    /// The logger this guard flushes.
    #[must_use]
    pub fn logger(&self) -> &Logger {
        &self.logger
    }
}

impl Drop for FlushGuard {
    fn drop(&mut self) {
        // Every failure was already passed to the error handler.
        let _reported = self.logger.shutdown();
    }
}

/// Error returned when a global logger is already installed. Holds the
/// logger that was not installed.
pub struct SetGlobalError(Logger);

impl SetGlobalError {
    /// Recover the logger that was not installed.
    #[must_use]
    pub fn into_inner(self) -> Logger {
        self.0
    }
}

impl core::fmt::Debug for SetGlobalError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str("SetGlobalError(..)")
    }
}

impl core::fmt::Display for SetGlobalError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str("a global logger is already installed")
    }
}

impl std::error::Error for SetGlobalError {}

/// Error returned by [`init_json`], [`init_logfmt`] and [`init_human`].
///
/// # Examples
///
#[cfg_attr(feature = "json", doc = "```")]
#[cfg_attr(not(feature = "json"), doc = "```ignore")]
/// match log_io::init_json() {
///     Ok(_guard) => { /* keep the guard alive in `main` */ }
///     Err(log_io::InitError::AlreadyInstalled) => { /* someone else set it */ }
///     Err(e) => eprintln!("logging not started: {e}"),
/// }
/// ```
#[derive(Debug)]
#[non_exhaustive]
pub enum InitError {
    /// A global logger is already installed.
    AlreadyInstalled,
    /// The `LOG_IO` environment variable is set but did not parse.
    Filter(crate::builder::BuildError),
}

impl core::fmt::Display for InitError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::AlreadyInstalled => f.write_str("a global logger is already installed"),
            Self::Filter(e) => write!(f, "LOG_IO: {e}"),
        }
    }
}

impl std::error::Error for InitError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::AlreadyInstalled => None,
            Self::Filter(e) => Some(e),
        }
    }
}

/// Builds a standard-output logger at `info` (or the `LOG_IO` filter
/// when that variable is set) with `sink`, and installs it globally.
#[cfg(any(feature = "json", feature = "logfmt", feature = "human"))]
fn init_stdout(
    sink: impl FnOnce(crate::builder::LoggerBuilder) -> crate::builder::LoggerBuilder,
) -> core::result::Result<FlushGuard, InitError> {
    let logger = sink(Logger::builder().env_filter("LOG_IO"))
        .try_build()
        .map_err(InitError::Filter)?;
    init(logger).map_err(|_| InitError::AlreadyInstalled)
}

/// Install a global logger that writes JSON lines to standard output, at
/// `info`, or with the filter in the `LOG_IO` environment variable when
/// it is set (for example `LOG_IO=debug,hyper=warn`). Keep the returned
/// guard alive in `main`.
///
/// The one-line set-up; use [`Logger::builder`] and [`init`] for anything
/// else.
///
/// # Errors
///
/// [`InitError::AlreadyInstalled`] if a global logger is already
/// installed, [`InitError::Filter`] if `LOG_IO` is set but does not parse.
///
/// # Examples
///
/// ```no_run
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
///     let _guard = log_io::init_json()?;
///     log_io::info!("listening", port = 8080_u16);
///     Ok(())
/// }
/// ```
#[cfg(feature = "json")]
pub fn init_json() -> core::result::Result<FlushGuard, InitError> {
    init_stdout(crate::builder::LoggerBuilder::stdout_json)
}

/// As [`init_json`], writing logfmt (`key=value`) lines.
///
/// # Errors
///
/// As [`init_json`].
///
/// # Examples
///
/// ```no_run
/// let _guard = log_io::init_logfmt()?;
/// log_io::info!("listening", port = 8080_u16);
/// # Ok::<(), log_io::InitError>(())
/// ```
#[cfg(feature = "logfmt")]
pub fn init_logfmt() -> core::result::Result<FlushGuard, InitError> {
    init_stdout(crate::builder::LoggerBuilder::stdout_logfmt)
}

/// As [`init_json`], writing the human-readable layout, coloured when
/// standard output is a terminal.
///
/// # Errors
///
/// As [`init_json`].
///
/// # Examples
///
/// ```no_run
/// let _guard = log_io::init_human()?;
/// log_io::info!("listening", port = 8080_u16);
/// # Ok::<(), log_io::InitError>(())
/// ```
#[cfg(feature = "human")]
pub fn init_human() -> core::result::Result<FlushGuard, InitError> {
    init_stdout(crate::builder::LoggerBuilder::stdout_human)
}