fast-observe 0.1.0

Error faults with causal trees + fastrace-first profiling/log orchestration
docs.rs failed to build fast-observe-0.1.0
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.

#![feature(error_generic_member_access)] // error! emits Error::provide
use fast_observe::exn::Result;
use fast_observe::{error, init, lookup_error, scope};

error! {
    pub enum AppError {
        /// Disk read failed.
        #[error("disk read failed: {path}")]
        #[code = "E100", category = Transient, advice = "check the path exists"]
        Disk { path: String },
    }
}

fn load(path: &str) -> Result<(), AppError> {
    let _span = scope!("load"); // feeds the active profiler set; also error context
    log::info!(path = path; "loading"); // plain log macros; kv becomes fields
    if path.is_empty() {
        // construct + count + span event + log, in one verb
        fast_observe::bail!(AppError::Disk(Disk { path: path.to_string() }));
    }
    Ok(())
}

fn main() {
    init(); // logs + traces with defaults; observe().init()? for toggles

    if let Err(f) = load("") {
        eprintln!("{f:?}"); // causal tree: codes, locations, scope path
    }

    let entry = lookup_error("E100").unwrap(); // works across every linked crate
    assert_eq!(entry.advice, Some("check the path exists"));
}

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 Fault is 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_msg nest the cause chain instead of flattening it, so the tree Debug renders is the tree source() walkers see.
  • Panics go through the same pipeline as returned errors (counted, hooked, rendered) instead of a separate stderr dump.
  • #[fast_observe::main] turns fn 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/Error and stops. No codes, no registry, no retry policy, no report — and source() 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 the observe() 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 profiling crate 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 JoinErrorFault, 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, web degrades to instant spans (there's no browser console on WASI).
  • The error registry is link-time (linkme), which doesn't exist on wasm. Call register_statics(&[MyError::ENTRIES, ..]) once at startup there; code()/category()/Display/From work regardless.
  • Hook panic containment uses catch_unwind; under wasm's default panic = "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