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
//! Panic capture.
//!
//! [`register`] installs a panic hook that turns each panic into a `fatal`
//! event with the panic message, location, and the backtrace captured *at the
//! panic site* (so it points at the real fault, not the SDK). The previous
//! hook is chained, so the default abort/print behaviour still happens.
//!
//! After capturing, the hook does a short synchronous flush: a panic often
//! precedes process death (`panic = "abort"`, or the thread unwinding to the
//! top), and an event still sitting in the queue would never be delivered.

use std::panic::PanicHookInfo;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;

use crate::backtrace;
use crate::event::Event;
use crate::level::Level;

static REGISTERED: AtomicBool = AtomicBool::new(false);

/// Install the panic hook (idempotent). Called automatically by
/// [`crate::init`] unless `panic_hook` is disabled in the config.
pub fn register() {
    if REGISTERED.swap(true, Ordering::SeqCst) {
        return; // already installed; don't chain twice
    }
    let previous = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        // Our capture must never mask the panic. Guard it so a bug here can't
        // turn into a panic-inside-the-panic-hook abort.
        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| capture_panic(info)));
        previous(info);
    }));
}

fn capture_panic(info: &PanicHookInfo<'_>) {
    // If the panic is happening on our own worker thread (e.g. a custom
    // Transport panicked), do not re-enter the client: enqueueing and then
    // flush()-ing would block on the very worker that's unwinding, stalling for
    // the flush timeout. Just let the previous hook handle it.
    if std::thread::current().name() == Some("errsight-worker") {
        return;
    }

    // Capture the backtrace at the panic site before doing anything that could
    // unwind further. `::backtrace` is the extern crate (our own
    // `crate::backtrace` module is imported as `backtrace` below).
    let bt = ::backtrace::Backtrace::new();

    let captured = crate::with_client(|client| {
        let cfg = client.config();
        if !cfg.enabled() {
            return false;
        }

        let message = panic_message(info);
        let frames = backtrace::frames_from(&bt, cfg);

        let mut event = Event::new(Level::Fatal, format!("panic: {message}"));
        event.set_metadata("exception_class", "panic");
        if let Some(loc) = info.location() {
            let location = format!("{}:{}:{}", loc.file(), loc.line(), loc.column());
            event.set_metadata("panic_location", location);
        }
        if !frames.is_empty() {
            event.backtrace = Some(backtrace::frames_to_string(&frames));
            if let Ok(v) = serde_json::to_value(&frames) {
                event.set_metadata("exception_frames", v);
            }
        }

        crate::finalize_and_capture(client, event).is_some()
    });

    // Flush outside the client read-lock (acquired/released inside
    // `with_client`) so we don't nest RwLock reads on this thread.
    if captured == Some(true) {
        let _ = crate::flush(Some(Duration::from_secs(2)));
    }
}

/// Best-effort extraction of a human message from the panic payload.
fn panic_message(info: &PanicHookInfo<'_>) -> String {
    let payload = info.payload();
    if let Some(s) = payload.downcast_ref::<&str>() {
        (*s).to_string()
    } else if let Some(s) = payload.downcast_ref::<String>() {
        s.clone()
    } else {
        // `info` Displays as "panicked at <loc>: <msg>"; fall back to that.
        let rendered = info.to_string();
        if rendered.is_empty() {
            "panic".to_string()
        } else {
            rendered
        }
    }
}