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
# 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"] }
```

| 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

```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

| 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.

```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).