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 optionallog/tracingbridges. - Rich stacks — structured frames with
in_appclassification 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.
- Rust 1.85+ (edition 2024)
[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 |
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).
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();| 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 |
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.// 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.
# #[cfg(feature = "anyhow")]
# fn demo(result: anyhow::Result<()>) {
if let Err(err) = result {
errsight::capture_anyhow(&err); // full cause chain + origin backtrace
}
# }use errsight::Level;
errsight::capture_message("cache rebuild took too long", Level::Warning);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_scopeframe along. For async handlers, set context withconfigure_scopein the same poll, or attach per-event context when you capture.
# #[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)).
# #[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.
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_levelis checked beforebefore_sendruns (below-threshold events are never built), so changingevent.levelinside the hook doesn't re-open the gate —Noneis the only way the hook drops an event.
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
initguard drops, or you callerrsight::close— the queue is drained withinshutdown_timeout.
errsight::flush(timeout) forces a synchronous drain (e.g. before a planned
exit). errsight::close(timeout) shuts the client down.
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();MIT © Jijo Bose. See LICENSE.