Skip to content

ErrSight/errsight-rust

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

errsight

Rust client for ErrSight error tracking. Captures panics, errors, and log/tracing events and ships them to the ErrSight API from a background thread. Capture is non-blocking; delivery is batched, bounded, and drained on shutdown.

  • Works everywhere — sync binaries, CLIs, and async servers (axum, actix, …). The transport runs on its own OS thread, so there's no async runtime requirement.
  • Panics, errors, logs — a panic hook, capture_error/capture_anyhow, and optional log / tracing bridges.
  • Rich stacks — structured frames with in_app classification and source context (the lines of code around each failing frame) for your own code.
  • Safe by default — capture never panics, the queue is bounded, PII can be scrubbed with before_send, and the API key is never logged.

Requirements

  • Rust 1.85+ (edition 2024)

Installation

[dependencies]
errsight = "0.1"

Enable optional integrations as needed:

[dependencies]
errsight = { version = "0.1", features = ["anyhow", "log", "tracing"] }
Feature Adds
anyhow capture_anyhow with the error's origin backtrace
log ErrsightLogger, a log::Log sink
tracing ErrsightLayer, a tracing_subscriber::Layer

Quick start

use errsight::{Config, Level};

fn main() {
    // Keep the guard alive for the life of the process — dropping it flushes
    // queued events and shuts the client down cleanly.
    let _guard = errsight::init(
        Config::builder()
            .api_key(std::env::var("ERRSIGHT_API_KEY").unwrap_or_default())
            .environment("production")
            .release(env!("CARGO_PKG_VERSION"))
            .build(),
    );

    if let Err(err) = run() {
        errsight::capture_error(&err);
    }

    errsight::capture_message("started up", Level::Info);
}

fn run() -> Result<(), std::io::Error> {
    Ok(())
}

The client is a no-op until a non-blank API key is present, so you can ship capture_* calls unconditionally — they cost nothing when ErrSight isn't configured (e.g. in development or tests).

Configuration

Build a [Config] explicitly, or seed one from the environment with Config::from_env() / Config::builder() (which reads env vars first, then applies your overrides).

use errsight::{Config, Level};
use std::time::Duration;

let config = Config::builder()
    .api_key("elp_your_write_key")     // required to send (else no-op)
    .environment("production")          // default: ERRSIGHT_ENV or "production"
    .release("1.4.2")                   // default: ERRSIGHT_RELEASE
    .host("https://errsight.com")       // default: ERRSIGHT_HOST
    .min_level(Level::Warning)          // drop anything below this; default Warning
    .timeout(Duration::from_secs(5))    // per-request HTTP timeout
    .batch_size(10)                     // events per request
    .flush_interval(Duration::from_secs(2))
    .max_queue_size(1_000)              // drop new events when full
    .shutdown_timeout(Duration::from_secs(5))
    .attach_stacktrace(false)           // also attach a backtrace to messages
    .panic_hook(true)                   // install the panic hook on init
    .in_app_include(["my_crate"])       // force these into "your code"
    .in_app_exclude(["generated/"])     // force these out of "your code"
    .debug(false)                       // print internal diagnostics to stderr
    .before_send_fail_open(false)       // false = drop event if scrubber panics
    .allow_insecure_transport(false)    // true = silence the cleartext-http warning
    .before_send(|mut event| {          // final-mile filter (see below)
        event.tags.remove("internal");
        Some(event)
    })
    .build();

Environment variables

Variable Default Purpose
ERRSIGHT_API_KEY Project write key (required)
ERRSIGHT_ENV production Environment tag on events
ERRSIGHT_HOST https://errsight.com API base URL
ERRSIGHT_RELEASE Release/version tag
ERRSIGHT_DEBUG false Internal stderr diagnostics

Capturing

Panics

init installs a panic hook by default (opt out with .panic_hook(false)). Panics are captured as fatal events with the backtrace taken at the panic site, then the previous hook runs as usual. Build with debug info and run with RUST_BACKTRACE=1 for the richest stacks.

let _guard = errsight::init(errsight::Config::from_env());
// An unhandled panic anywhere from here is reported automatically.

Errors

// Concrete error type → ErrSight records the type name as `exception_class`.
errsight::capture_error(&my_error);

// Trait object → use the `_dyn` variant (type name isn't recoverable).
let boxed: Box<dyn std::error::Error> = my_error.into();
errsight::capture_error_dyn(&*boxed);

// Record a recovered error as a warning instead of an error.
errsight::capture_error_at_level(&my_error, errsight::Level::Warning);

capture_error records the message, type name, the source() cause chain, and a backtrace captured at the call site. For the error's origin backtrace, enable the anyhow feature and use capture_anyhow.

anyhow

# #[cfg(feature = "anyhow")]
# fn demo(result: anyhow::Result<()>) {
if let Err(err) = result {
    errsight::capture_anyhow(&err); // full cause chain + origin backtrace
}
# }

Messages

use errsight::Level;
errsight::capture_message("cache rebuild took too long", Level::Warning);

Scopes: user, tags, breadcrumbs

A scope is the ambient context attached to events — the current user, string tags, and a breadcrumb trail.

use errsight::{Breadcrumb, User, Level};

errsight::configure_scope(|scope| {
    scope.set_user(User::new().id("u-42").email("alice@example.com"));
    scope.set_tag("service", "checkout");
});

errsight::add_breadcrumb(Breadcrumb::new("http", "GET /cart").level(Level::Info));

Use with_scope to isolate context to a block — handy per request or per job. Mutations inside the block don't leak out:

errsight::with_scope(|| {
    errsight::configure_scope(|s| { s.set_tag("request_id", "abc123"); });
    // ... handle the request; any capture here is tagged request_id ...
});
// request_id is gone here.

Async note: scopes are keyed to OS threads. A future that hops across tokio worker threads won't carry its with_scope frame along. For async handlers, set context with configure_scope in the same poll, or attach per-event context when you capture.

log integration

# #[cfg(feature = "log")]
# fn demo() {
use errsight::integrations::log::ErrsightLogger;
use errsight::Level;

ErrsightLogger::builder()
    .event_level(Level::Error)       // these records become events
    .breadcrumb_level(Level::Info)   // these become breadcrumbs
    .max_level(log::LevelFilter::Info)
    .install()
    .expect("set global logger");

// Now `log::error!(...)` is captured; `log::info!(...)` becomes a breadcrumb.
# }

Tee to an existing logger (e.g. env_logger) with .backing(Box::new(other)).

tracing integration

# #[cfg(feature = "tracing")]
# fn demo() {
use errsight::integrations::tracing::ErrsightLayer;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;

tracing_subscriber::registry()
    .with(ErrsightLayer::new())   // error events → ErrSight, lower → breadcrumbs
    .init();
# }

Event fields are attached as structured metadata (events) or breadcrumb data.

before_send — filtering & PII scrubbing

Runs on the calling thread just before an event is queued. Return Some(event) (possibly mutated) to send, or None to drop. If the filter panics, the event is dropped by default (fail-closed) — since before_send is the PII/secret scrubber, shipping the un-scrubbed original would be a leak. Opt into fail-open (send unmodified) with before_send_fail_open(true) if you'd rather prioritise availability.

use errsight::Config;

let config = Config::builder()
    .api_key("elp_…")
    .before_send(|mut event| {
        if event.tags.get("noise").is_some() {
            return None; // drop noisy events entirely
        }
        event.metadata.remove("credit_card"); // scrub PII
        Some(event)
    })
    .build();

min_level is checked before before_send runs (below-threshold events are never built), so changing event.level inside the hook doesn't re-open the gate — None is the only way the hook drops an event.

How delivery works

Captured events are pushed onto a bounded in-memory queue and flushed by a single background thread:

  • Sent in batches (default 10) every flush_interval (default 2s), or sooner when a batch fills.
  • Oversized batches are split to stay under the 512 KB ingestion limit.
  • On HTTP 429 the worker backs off for the server's Retry-After (capped at 10 min) and retries the held events — without blocking capture.
  • When the queue is full, new events are dropped (with a debug log) rather than blocking your app.
  • On shutdown — when the init guard drops, or you call errsight::close — the queue is drained within shutdown_timeout.

errsight::flush(timeout) forces a synchronous drain (e.g. before a planned exit). errsight::close(timeout) shuts the client down.

Custom transport

The HTTP layer is a trait, so you can route events through your own stack (or capture them in tests):

use errsight::{SendOutcome, Transport};
use std::sync::Arc;

struct MyTransport;
impl Transport for MyTransport {
    fn send(&self, body: &[u8]) -> SendOutcome {
        // ... send `body` (a JSON array of events) however you like ...
        SendOutcome::Success
    }
}

let config = errsight::Config::builder()
    .api_key("elp_…")
    .transport(Arc::new(MyTransport))
    .build();

License

MIT © Jijo Bose. See LICENSE.

About

Rust client for ErrSight error tracking.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages