errsight 0.1.1

Rust client for ErrSight error tracking — captures panics, errors, and log/tracing events and ships them to the ErrSight API from a background thread.
Documentation
//! `log` integration (feature `log`).
//!
//! [`ErrsightLogger`] implements [`log::Log`]: records at or above
//! `event_level` (default [`Level::Error`]) are captured as ErrSight events;
//! records between `breadcrumb_level` (default [`Level::Info`]) and
//! `event_level` become breadcrumbs that ride along with the next event. An
//! optional backing logger still receives every record, so you can tee to
//! stdout/`env_logger` as usual.
//!
//! ```no_run
//! # #[cfg(feature = "log")]
//! # fn demo() {
//! use errsight::integrations::log::ErrsightLogger;
//! use errsight::Level;
//!
//! ErrsightLogger::builder()
//!     .event_level(Level::Error)
//!     .breadcrumb_level(Level::Info)
//!     .max_level(log::LevelFilter::Info)
//!     .install()
//!     .expect("set logger");
//! # }
//! ```

use ::log::{Log, Metadata, Record};

use crate::event::{Breadcrumb, Event};
use crate::level::Level;

/// Map a `log` level onto an ErrSight level.
fn from_log_level(level: ::log::Level) -> Level {
    match level {
        ::log::Level::Error => Level::Error,
        ::log::Level::Warn => Level::Warning,
        ::log::Level::Info => Level::Info,
        ::log::Level::Debug => Level::Debug,
        ::log::Level::Trace => Level::Debug,
    }
}

/// A `log::Log` sink forwarding to ErrSight (and optionally a backing logger).
pub struct ErrsightLogger {
    event_level: Level,
    breadcrumb_level: Level,
    backing: Option<Box<dyn Log>>,
}

impl ErrsightLogger {
    /// Start configuring a logger.
    pub fn builder() -> ErrsightLoggerBuilder {
        ErrsightLoggerBuilder {
            event_level: Level::Error,
            breadcrumb_level: Level::Info,
            backing: None,
            max_level: ::log::LevelFilter::Info,
        }
    }
}

impl Log for ErrsightLogger {
    fn enabled(&self, metadata: &Metadata<'_>) -> bool {
        match &self.backing {
            Some(b) => b.enabled(metadata),
            // We can't know the breadcrumb/event thresholds in `log` terms
            // here cheaply; accept and let `log` happen, where we filter.
            None => true,
        }
    }

    fn log(&self, record: &Record<'_>) {
        // Always tee to the backing logger first so console output is intact
        // regardless of what we do with the record.
        if let Some(b) = &self.backing {
            if b.enabled(record.metadata()) {
                b.log(record);
            }
        }

        let level = from_log_level(record.level());
        let message = format!("{}", record.args());

        if level >= self.event_level {
            let mut event = Event::new(level, message);
            event.set_metadata("logger.target", record.target());
            if let Some(module) = record.module_path() {
                event.set_metadata("logger.module", module);
            }
            if let (Some(file), Some(line)) = (record.file(), record.line()) {
                event.set_metadata("logger.location", format!("{file}:{line}"));
            }
            crate::capture_event(event);
        } else if level >= self.breadcrumb_level {
            let crumb = Breadcrumb::new(record.target(), message).level(level);
            crate::add_breadcrumb(crumb);
        }
    }

    fn flush(&self) {
        if let Some(b) = &self.backing {
            b.flush();
        }
    }
}

/// Builder for [`ErrsightLogger`].
pub struct ErrsightLoggerBuilder {
    event_level: Level,
    breadcrumb_level: Level,
    backing: Option<Box<dyn Log>>,
    max_level: ::log::LevelFilter,
}

impl ErrsightLoggerBuilder {
    /// Records at or above this level become ErrSight events. Default `Error`.
    pub fn event_level(mut self, level: Level) -> Self {
        self.event_level = level;
        self
    }
    /// Records at or above this (and below `event_level`) become breadcrumbs.
    /// Default `Info`.
    pub fn breadcrumb_level(mut self, level: Level) -> Self {
        self.breadcrumb_level = level;
        self
    }
    /// A backing logger to tee every record to (e.g. `env_logger`).
    pub fn backing(mut self, logger: Box<dyn Log>) -> Self {
        self.backing = Some(logger);
        self
    }
    /// The global max level filter `log` applies before dispatching. Set this
    /// at least as permissive as `breadcrumb_level` or breadcrumbs are dropped
    /// before they reach us. Default `Info`.
    pub fn max_level(mut self, filter: ::log::LevelFilter) -> Self {
        self.max_level = filter;
        self
    }

    /// Build the logger without installing it globally.
    pub fn build(self) -> ErrsightLogger {
        ErrsightLogger {
            event_level: self.event_level,
            breadcrumb_level: self.breadcrumb_level,
            backing: self.backing,
        }
    }

    /// Build and install as the global `log` logger, setting the max level.
    pub fn install(self) -> Result<(), ::log::SetLoggerError> {
        let max = self.max_level;
        let logger = self.build();
        ::log::set_boxed_logger(Box::new(logger))?;
        ::log::set_max_level(max);
        Ok(())
    }
}