forge-ops-tracker
Rust error reporting client for a private, self-hosted ForgeOps tracker instance. Requires Rust 1.81+. It captures panics on any thread and explicitly reported errors, builds a backtrace, scrubs likely PII, and delivers events to ForgeOps over HTTP without blocking whatever raised them.
Installation
Not yet published to crates.io -- point at this path directly (or a local checkout, once split into its own repo):
[]
= { = "/path/to/forge_ops/sdks/rust" }
Dependencies
Rust's standard library has no HTTP client, no structured per-frame backtrace access on stable, and
no regular expression engine. This crate depends on exactly the three widely-used crates that fill
those three specific gaps, each doing something std genuinely can't:
ureq-- delivers events over HTTPS.stdhas no HTTP client at all.backtrace-- structured per-frame file/line/function data.std::backtrace::Backtracecaptures a trace on stable Rust, but only exposes it as a formatted string -- no public per-frame accessors -- so it can't produce the structured file/line/method/in_app shape this client needs to build an event payload.regex-- the PII scrubber's pattern matching.stdhas no regular expression engine.
Everything else -- the event payload's own JSON encoding, a DSN parser, timestamp formatting -- is
hand-rolled rather than reaching for serde_json/url/chrono: this crate only depends on
something outside std when the language truly can't do it itself.
Configuration
Set a DSN (from a project's settings page in ForgeOps), either via the FORGE_OPS_DSN environment
variable or explicitly:
init;
Call init once at startup, before your server starts accepting requests. Pass a closure to set
any Configuration field, so every option is available through the one call without a long list
of positional arguments or a separate setter for each field.
What gets reported automatically, and what doesn't
A panic on any thread needs no further wiring at all, once init() has run. Rust's panic hook
(std::panic::set_hook) is process-wide: it fires for a panic on any thread, including a web
framework's own worker threads (Actix, Axum/Tokio, a plain std::thread), with no per-framework
middleware needed at all.
init() installs this hook automatically unless Configuration.install_panic_hook is set to
false. It never swallows the panic: after reporting, it calls whatever hook was previously
installed (Rust's own default, which prints to stderr, unless something else already replaced it),
so reporting a panic never changes what your program actually does afterward. Call
install_panic_hook() directly only if you're managing configuration some other way than
init().
Rust doesn't have exceptions, so an error your own code already caught (a Result::Err) is a
second, separate case -- report it explicitly, right at the point you'd otherwise just log it:
if let Err = charge_card
Or, more concisely, via the ResultReportExt extension trait, which reports on Err and passes
the Result through unchanged:
use ResultReportExt;
charge_card.report_err?;
A plain Rust std::error::Error carries no stack trace of its own, so capture_error captures the
backtrace at its own call site. Call it as close to the point you learned about the error as you
reasonably can, for the most useful trace.
exception_class is inferred via std::any::type_name, which needs a concrete, statically-known
error type -- for a Box<dyn Error> or other trait object, where that isn't possible, use
capture_error_with_class(class, err, context) instead and supply the class yourself.
Delivery happens on a background thread with a bounded channel and a short per-request HTTP
timeout (Configuration.timeout, 2s default). Every failure mode -- network errors, timeouts, a
full queue, a malformed DSN -- is caught and dropped rather than propagated, so a broken or
unreachable tracker can never take down the host app. The worker thread starts eagerly, at init()
time, rather than waiting for the first push: Rust programs essentially never fork themselves at
the application level after startup, so there's no risk of an eagerly-started thread being left
dead in a forked child, and starting it up front means it's ready before the first event needs to
be delivered.
in_app backtrace frames
A Rust binary built with debug info embeds the real build-time source paths, so file-path matching
against Configuration.app_root is a straightforward prefix comparison against those embedded
paths. Defaults to the current working directory; set it explicitly if that doesn't match your
binary's actual build layout. Third-party crate source under Cargo's registry cache and the Rust
toolchain's own std/core source are never marked in_app, regardless of app_root.
PII scrubbing
By default, the message, backtrace, and any context you attach are scanned for likely personal
data -- email addresses, formatted SSNs/credit cards, known API key/token formats, and anything
under a suspiciously-named key (password, api_key, ssn, and similar) -- and redacted before
the payload ever leaves this process. ForgeOps itself scrubs again on arrival regardless, so this
is a second, earlier layer, not the only one.
To disable it:
init;
Running the tests