# errsight
Rust client for [ErrSight](https://errsight.com) 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
```toml
[dependencies]
errsight = "0.1"
```
Enable optional integrations as needed:
```toml
[dependencies]
errsight = { version = "0.1", features = ["anyhow", "log", "tracing"] }
```
| `anyhow` | `capture_anyhow` with the error's origin backtrace |
| `log` | `ErrsightLogger`, a `log::Log` sink |
| `tracing` | `ErrsightLayer`, a `tracing_subscriber::Layer` |
## Quick start
```rust
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).
```rust
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
| `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.
```rust
let _guard = errsight::init(errsight::Config::from_env());
// An unhandled panic anywhere from here is reported automatically.
```
### Errors
```rust
// 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`
```rust
# #[cfg(feature = "anyhow")]
# fn demo(result: anyhow::Result<()>) {
if let Err(err) = result {
errsight::capture_anyhow(&err); // full cause chain + origin backtrace
}
# }
```
### Messages
```rust
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.
```rust
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:
```rust
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
```rust
# #[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
```rust
# #[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.
```rust
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):
```rust
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](LICENSE).