Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
fast-observe
Errors, logs, traces, and profiling spans that actually know about each other.
fast-observe pulls four things into one crate and wires them up: an
exn-style error type with causal trees, the logforth logging pipeline,
fastrace traces, and the profiling crate's span facade. An error knows
its code, the scope path it happened in, its trace id, and where it was
constructed — and it counts and logs itself at construction, before you've
set anything up.
It's heavily type-driven. Error categories determine retry/abort policy,
codes are shape-checked by the macro at compile time, the report's
one-fact-per-line contract is enforced by a type that escapes newlines,
and scope guards are !Send so a span can't silently record against the
wrong thread.
// error! emits Error::provide
use Result;
use ;
error!
A runnable version is in examples/demo.rs.
What you get for free
No init() call, no setup, and none of this needs thinking about:
- Every constructed
Faultis counted per type (error_counts()), logged with its type, location, and scope, and emitted as a span event in the current trace. Hooks fan out and are panic-contained. ?just works on your error types;wrap/change_context/wrap_msgnest the cause chain instead of flattening it, so the treeDebugrenders is the treesource()walkers see.- Panics go through the same pipeline as returned errors (counted, hooked, rendered) instead of a separate stderr dump.
#[fast_observe::main]turnsfn main() -> Result<()>'s error exit into a full report on stderr plus a sysexits-style exit code from the error's category.
The report
render_report(&fault) — or OBSERVE_REPORT=text|json to have the error
hook emit it — gives you a deterministic, greppable block:
report: fast-observe/1
error: [E100] [my_crate::repo::NotFound] entity not found: 17
category: Content (policy: fix the input; retrying unchanged input will fail)
location: src/repo.rs:42:10
scope: request → load_entity (elapsed 3ms)
attachment: attempt=3
cause 0: [E100] [my_crate::repo::NotFound] entity not found: 17, at src/repo.rs:42:10
cause 1: No such file or directory (os error 2), at src/repo.rs:42:10
trace_id: 4f3c9a2b…
fingerprint: 9f86d081
advice: check the entity table
action: fix the input; retrying unchanged input will fail
hint: run `doctor E100`
Cause lines are labeled by how the frame got there: cause (source
chain), original (wrapped), attempt (retry), failure (batch merge).
The fingerprint is a stable hash of the failure site, so "have we seen
this one before" is a string match. The text is snapshot-testable: no
ANSI, no timestamps, and values are newline-escaped so data can't inject
fake lines.
Why not thiserror + log + tracing
Each of those does its slice well; the gaps are in the wiring you'd write yourself.
- thiserror derives
Display/Errorand stops. No codes, no registry, no retry policy, no report — andsource()wiring is on you, which is where cause chains quietly break.error!is thiserror-compatible attribute syntax plus#[code]/#[category]/#[advice], and it wires the chain for you. - log is a facade. You still choose and assemble a backend, and it
knows nothing about your errors. Here the deployment is one
init()(or theobserve()builder), and error events land in it with structured fields. - tracing instruments functions, but spans and errors never meet: an
error deep in a handler doesn't know which span it was in, and the span
tree can't tell you what ran right before the failure. And the
profilingcrate compiles exactly one backend in — fast-observe compiles several and picks at runtime (OBSERVE_PROFILE), with self-teaching warnings when you select one you didn't compile.
The same scope! calls also double as benchmark instrumentation: with
feature bench, bencher.bench_profiled(..) (divan) or
bench::measure_breakdown(n, f) hand you a per-phase span breakdown plus
the error-count delta, from the instrumentation you already wrote.
Nightly required
Feature gates, declared at the crate root with tracking issues:
error_generic_member_access (Error::provide/request_ref, so codes
and categories are readable through &dyn Error), error_iter
(Error::sources), backtrace_frames (feature backtrace), and
proc_macro_diagnostic (macro crate). The devenv pins a known-good
toolchain.
Configuration
| Env var | Effect |
|---|---|
OBSERVE_PROFILE |
Active profiling backends: comma-separated off|instant|fastrace|web|puffin|tracy|superluminal|tracing (default fastrace). off alone. |
OBSERVE_LOG |
Max log level (falls back to RUST_LOG, then info). |
OBSERVE_LOG_DIR |
With feature file: also log to <dir>/app.log. |
OBSERVE_ERROR_THROTTLE |
Cap sink-hook invocations per error type per second (default 0 = unlimited). |
OBSERVE_REPORT |
Error hook emits the full report block: text or json (default off). |
OBSERVE_REPORT_SOURCE |
1: reports include the source line at the error location. |
OBSERVE_COLOR |
Diagnostic colors: auto (default), always, never. |
OBSERVE_BACKTRACE |
Feature backtrace: overrides RUST_BACKTRACE in both directions. |
Compiled-in ≠ active: cargo features compile a backend in, the Backends
mask (config().set_backends(...) or OBSERVE_PROFILE) selects which run.
Selecting one you didn't compile logs a warning naming the feature to add.
Features
Default: fastrace + bridge-log. Nothing else compiles in unless named.
Weight: what it costs your build.
| Feature | Weight | What it wires |
|---|---|---|
instant |
tiny | thread-local span accumulator + per-phase breakdown (wasm-safe) |
web |
tiny | level-mapped browser console logs + devtools timeline marks (wasm32-unknown-unknown) |
json, layout-logfmt, layout-gcl |
tiny | stdout layouts (JSON / logfmt / Google Cloud Logging) |
file |
tiny | rolling file appender via OBSERVE_LOG_DIR |
log-syslog, log-journald |
light | unix syslog / systemd journald appenders |
log-async |
light | background-thread stdout/file appenders |
filter-rustlog |
light | RUST_LOG-style per-module filter |
diag-task-local |
tiny | task-local diagnostic context |
otel |
heavy | fastrace-opentelemetry + OTel log appender |
bridge-tracing |
light | tracing spans → fastrace |
http |
light | fastrace-reqwest trace-context propagation |
int-axum, int-poem, int-tonic, int-tower, int-futures |
light | framework/stream middleware re-exports |
reporter-datadog, reporter-jaeger |
heavy | vendor reporters (prefer otel for new setups) |
metrics-facade |
tiny | error_counts mirrored into the metrics facade |
profile-with-puffin, profile-with-tracing |
medium | runtime-selectable profiler backends |
profile-with-tracy |
heavy | tracy backend |
profile-with-superluminal |
tiny | superluminal backend (windows) |
backtrace |
tiny | backtrace capture hook |
flush-on-exit |
tiny | fastrace flush on atexit/SIGTERM/SIGHUP |
bench |
light | divan re-export + bench_profiled/measure_breakdown |
serde |
tiny | serde derives for Diagnostic etc.; enables render_report_json |
anyhow-boundary, compat-eyre, compat-error-stack |
tiny | explicit boundary conversions |
int-tokio |
tiny | JoinError → Fault, cancelled vs panicked |
Libraries: depend with default-features = false; fastrace forwards
fastrace/enable, and that's the binary's call to make.
Platform notes
- wasm32-wasip3 and wasm32-unknown-unknown are compile-verified in CI
(
just check-wasip3/just check-wasm). On wasip3,webdegrades toinstantspans (there's no browser console on WASI). - The error registry is link-time (
linkme), which doesn't exist on wasm. Callregister_statics(&[MyError::ENTRIES, ..])once at startup there;code()/category()/Display/Fromwork regardless. - Hook panic containment uses
catch_unwind; under wasm's defaultpanic = "abort"it can't contain anything, so don't panic in hooks.
Docs
- OBSERVE.md — the agent/user guide: the vocabulary, the error rules, the debugging workflow.
- DESIGN.md / SURFACE.md — design rationale and the user-surface contract.
- CONTRIBUTING.md — dev environment and verification.
License
MIT OR Apache-2.0