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
//! `tracing` integration (feature `tracing`).
//!
//! [`ErrsightLayer`] is a [`tracing_subscriber::Layer`]: events at or above
//! `event_level` (default [`Level::Error`]) are captured as ErrSight events;
//! events between `breadcrumb_level` (default [`Level::Info`]) and
//! `event_level` become breadcrumbs. Event fields are attached as structured
//! metadata / breadcrumb data.
//!
//! ```no_run
//! # #[cfg(feature = "tracing")]
//! # fn demo() {
//! use errsight::integrations::tracing::ErrsightLayer;
//! use tracing_subscriber::layer::SubscriberExt;      // for `.with(..)`
//! use tracing_subscriber::util::SubscriberInitExt;   // for `.init()`
//!
//! // Compose with your existing subscriber stack (add a `fmt` layer too if
//! // you enable tracing-subscriber's `fmt` feature in your app).
//! tracing_subscriber::registry()
//!     .with(ErrsightLayer::new())
//!     .init();
//! # }
//! ```

use serde_json::{Map, Value};
use tracing::field::{Field, Visit};
use tracing::{Event, Subscriber};
use tracing_subscriber::layer::{Context, Layer};

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

fn from_tracing_level(level: &tracing::Level) -> Level {
    match *level {
        tracing::Level::ERROR => Level::Error,
        tracing::Level::WARN => Level::Warning,
        tracing::Level::INFO => Level::Info,
        tracing::Level::DEBUG => Level::Debug,
        tracing::Level::TRACE => Level::Debug,
    }
}

/// A `tracing` layer that turns events into ErrSight events / breadcrumbs.
pub struct ErrsightLayer {
    event_level: Level,
    breadcrumb_level: Level,
}

impl ErrsightLayer {
    /// A layer with default thresholds (`event_level = Error`,
    /// `breadcrumb_level = Info`).
    pub fn new() -> Self {
        ErrsightLayer {
            event_level: Level::Error,
            breadcrumb_level: Level::Info,
        }
    }

    /// Events at or above this level become ErrSight events. Default `Error`.
    pub fn event_level(mut self, level: Level) -> Self {
        self.event_level = level;
        self
    }

    /// Events 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
    }
}

impl Default for ErrsightLayer {
    fn default() -> Self {
        Self::new()
    }
}

impl<S: Subscriber> Layer<S> for ErrsightLayer {
    fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
        let metadata = event.metadata();
        let level = from_tracing_level(metadata.level());

        // Cheap gate before visiting fields.
        if level < self.breadcrumb_level {
            return;
        }

        let mut visitor = FieldVisitor::default();
        event.record(&mut visitor);
        let message = visitor
            .message
            .unwrap_or_else(|| metadata.target().to_string());

        if level >= self.event_level {
            let mut ev = ErrEvent::new(level, message);
            ev.set_metadata("logger.target", metadata.target());
            if let (Some(file), Some(line)) = (metadata.file(), metadata.line()) {
                ev.set_metadata("logger.location", format!("{file}:{line}"));
            }
            if !visitor.fields.is_empty() {
                ev.set_metadata("fields", Value::Object(visitor.fields));
            }
            crate::capture_event(ev);
        } else {
            let mut crumb = Breadcrumb::new(metadata.target(), message).level(level);
            for (k, v) in visitor.fields {
                crumb = crumb.data(k, v);
            }
            crate::add_breadcrumb(crumb);
        }
    }
}

/// Collects a tracing event's fields, pulling out the special `message` field.
#[derive(Default)]
struct FieldVisitor {
    message: Option<String>,
    fields: Map<String, Value>,
}

impl FieldVisitor {
    fn insert(&mut self, field: &Field, value: Value) {
        if field.name() == "message" {
            if let Value::String(s) = &value {
                self.message = Some(s.clone());
                return;
            }
        }
        self.fields.insert(field.name().to_string(), value);
    }
}

impl Visit for FieldVisitor {
    fn record_str(&mut self, field: &Field, value: &str) {
        self.insert(field, Value::String(value.to_string()));
    }
    fn record_i64(&mut self, field: &Field, value: i64) {
        self.insert(field, Value::from(value));
    }
    fn record_u64(&mut self, field: &Field, value: u64) {
        self.insert(field, Value::from(value));
    }
    fn record_f64(&mut self, field: &Field, value: f64) {
        self.insert(field, Value::from(value));
    }
    fn record_bool(&mut self, field: &Field, value: bool) {
        self.insert(field, Value::Bool(value));
    }
    fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
        self.insert(field, Value::String(value.to_string()));
    }
    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
        // The `message` field arrives here (it's recorded via Debug), so route
        // it through `insert` which special-cases the name.
        if field.name() == "message" {
            self.message = Some(format!("{value:?}"));
        } else {
            self.insert(field, Value::String(format!("{value:?}")));
        }
    }
}