Skip to main content

whereat/
lib.rs

1//! # whereat - Lightweight error location tracking
2//!
3//! **150x faster than `backtrace`** — production error tracing without debuginfo, panic, or overhead.
4//!
5//! ```text
6//! Error: UserNotFound
7//!    at src/db.rs:142:9
8//!       ╰─ user_id = 42
9//!    at src/api.rs:89:5
10//!       ╰─ in handle_request
11//!    at myapp @ https://github.com/you/myapp/blob/a1b2c3d/src/main.rs#L23
12//! ```
13//!
14//! ## Try It Now
15//!
16//! No setup required — just wrap errors with [`at()`] and propagate with [`.at()`](ResultAtExt::at):
17//!
18//! ```rust
19//! use whereat::{at, At, ResultAtExt};
20//!
21//! #[derive(Debug)]
22//! enum MyError { NotFound }
23//!
24//! fn inner() -> Result<(), At<MyError>> {
25//!     Err(at(MyError::NotFound))  // Wrap error, capture location
26//! }
27//!
28//! fn outer() -> Result<(), At<MyError>> {
29//!     inner().at_str("looking up user")?;  // Add context
30//!     Ok(())
31//! }
32//!
33//! let err = outer().unwrap_err();
34//! println!("{:?}", err);  // Shows locations + context
35//! ```
36//!
37//! ## Production Setup
38//!
39//! For **clickable GitHub links** in traces, add one line to your crate root and use [`at!()`](at!):
40//!
41//! ```rust,ignore
42//! // In lib.rs or main.rs
43//! whereat::define_at_crate_info!();
44//!
45//! fn find_user(id: u64) -> Result<String, At<MyError>> {
46//!     Err(at!(MyError::NotFound))  // Now includes repo links in traces
47//! }
48//! ```
49//!
50//! The `at!()` macro desugars to:
51//! ```rust,ignore
52//! At::wrap(err)
53//!     .set_crate_info(crate::at_crate_info())  // Enables GitHub/GitLab links
54//!     .at()                                     // Captures file:line:col
55//! ```
56//!
57//! ## Which Approach?
58//!
59//! | Situation | Use |
60//! |-----------|-----|
61//! | Existing struct/enum you don't want to modify | Wrap with [`At<YourError>`](At) |
62//! | Want traces embedded inside your error type | Implement [`AtTraceable`] trait |
63//!
64//! **Wrapper approach** (most common): Return `Result<T, At<YourError>>` from functions.
65//!
66//! **Embedded approach**: Implement [`AtTraceable`] and store an [`AtTrace`] (or `Box<AtTrace>`)
67//! field inside your error type. Return `Result<T, YourError>` directly.
68//!
69//! ## Starting a Trace
70//!
71//! | Function | Crate info | Use when |
72//! |----------|------------|----------|
73//! | [`at(err)`](at()) | ❌ None | Prototyping — no setup needed |
74//! | [`at!(err)`](at!) | ✅ GitHub links | **Production** — requires [`define_at_crate_info!()`](define_at_crate_info) |
75//! | [`err.start_at()`](ErrorAtExt::start_at) | ❌ None | Chaining on `Error` trait types |
76//!
77//! Start with `at()` to try things out. Upgrade to `at!()` before shipping — you'll want
78//! those clickable links when debugging production issues.
79//!
80//! ## Extending a Trace
81//!
82//! **Create a new location frame** (call site is recorded):
83//!
84//! | Method | Effect |
85//! |--------|--------|
86//! | [`.at()`](ResultAtExt::at) | New frame with just file:line:col |
87//! | [`.at_fn(\|\| {})`](ResultAtExt::at_fn) | New frame + captures function name |
88//! | [`.at_named("step")`](ResultAtExt::at_named) | New frame + custom label |
89//!
90//! **Add context to the last frame** (no new location):
91//!
92//! | Method | Effect |
93//! |--------|--------|
94//! | [`.at_str("msg")`](ResultAtExt::at_str) | Static string (zero-cost) |
95//! | [`.at_string(\|\| format!(...))`](ResultAtExt::at_string) | Dynamic string (lazy) |
96//! | [`.at_data(\|\| value)`](ResultAtExt::at_data) | Typed via Display (lazy) |
97//! | [`.at_debug(\|\| value)`](ResultAtExt::at_debug) | Typed via Debug (lazy) |
98//! | [`.at_aside_error(err)`](ResultAtExt::at_aside_error) | Attach a related error (diagnostic only, **not** in `.source()` chain) |
99//!
100//! **Key distinction**: `.at()` creates a NEW frame. `.at_str()` and friends add to the LAST frame.
101//!
102//! ```rust
103//! use whereat::{at, At, ResultAtExt};
104//!
105//! #[derive(Debug)]
106//! struct MyError;
107//!
108//! fn example() -> Result<(), At<MyError>> {
109//!     // One frame with two contexts attached
110//!     let e = at(MyError).at_str("a").at_str("b");
111//!     assert_eq!(e.frame_count(), 1);
112//!
113//!     // Two frames: at() creates first, .at() creates second
114//!     let e = at(MyError).at().at_str("on second frame");
115//!     assert_eq!(e.frame_count(), 2);
116//!     Ok(())
117//! }
118//! # example().ok();
119//! ```
120//!
121//! ## Foreign Crates and Errors
122//!
123//! When consuming errors from other crates, use [`at_crate!()`](at_crate) to mark the boundary.
124//! This ensures traces show your crate's GitHub links, not confusing paths from dependencies.
125//!
126//! ```rust,ignore
127//! whereat::define_at_crate_info!();  // Once in lib.rs
128//!
129//! use whereat::{at_crate, At, ResultAtExt};
130//!
131//! fn call_dependency() -> Result<(), At<DependencyError>> {
132//!     at_crate!(dependency::do_thing())?;  // Marks crate boundary
133//!     Ok(())
134//! }
135//! ```
136//!
137//! The `at_crate!()` macro desugars to:
138//! ```rust,ignore
139//! result.at_crate(crate::at_crate_info())  // Adds boundary marker with your crate's info
140//! ```
141//!
142//! For plain errors without traces (e.g., `std::io::Error`), use `map_err(at)` to start tracing:
143//!
144//! ```rust
145//! use whereat::{At, at, ResultAtExt};
146//!
147//! fn external_api() -> Result<(), &'static str> {
148//!     Err("external error")
149//! }
150//!
151//! fn wrapper() -> Result<(), At<&'static str>> {
152//!     external_api().map_err(at).at_str("calling external API")?;
153//!     Ok(())
154//! }
155//! ```
156//!
157//! ## Design Goals
158//!
159//! - **Tiny overhead**: `At<E>` is `sizeof(E) + 8` bytes; zero heap allocation on the Ok path
160//! - **Zero-cost context**: `.at_str("literal")` stores a pointer, no copy or allocation
161//! - **Lazy evaluation**: `.at_string(|| ...)` closures only run on error
162//! - **no_std compatible**: Works with just `core` + `alloc`
163//!
164//! ## OOM Behavior
165//!
166//! Trace allocations are fallible where possible — on OOM, trace entries are silently skipped
167//! but your error `E` always propagates (it's stored inline). See the README for details.
168
169#![no_std]
170#![deny(unsafe_code)]
171
172extern crate alloc;
173
174mod at;
175mod context;
176mod crate_info;
177mod ext;
178#[cfg(any(feature = "_termcolor", feature = "_html"))]
179mod format;
180mod inline_vec;
181pub mod prelude;
182mod trace;
183
184pub use at::At;
185pub use context::AtContextRef;
186pub use crate_info::{
187    AtCrateInfo, AtCrateInfoBuilder, BITBUCKET_LINK_FORMAT, GITEA_LINK_FORMAT, GITHUB_LINK_FORMAT,
188    GITLAB_LINK_FORMAT,
189};
190pub use ext::{ErrorAtExt, ResultAtExt, ResultAtTraceableExt};
191pub use trace::{
192    AT_MAX_CONTEXTS, AT_MAX_FRAMES, AtFrame, AtFrameOwned, AtTrace, AtTraceBoxed, AtTraceable,
193};
194
195// ============================================================================
196// Crate-level error tracking info (for whereat's own at!() / at_crate!() usage)
197// ============================================================================
198//
199// This is what `define_at_crate_info!()` generates. We define it manually here
200// because the macro isn't defined yet at this point in the file.
201
202// whereat's own crate info for internal at!() usage in doctests
203#[doc(hidden)]
204pub(crate) static __AT_CRATE_INFO: AtCrateInfo = AtCrateInfo::builder()
205    .name("whereat")
206    .repo(option_env!("CARGO_PKG_REPOSITORY"))
207    .commit(match option_env!("GIT_COMMIT") {
208        Some(c) => Some(c),
209        None => match option_env!("GITHUB_SHA") {
210            Some(c) => Some(c),
211            None => match option_env!("CI_COMMIT_SHA") {
212                Some(c) => Some(c),
213                None => Some(concat!("v", env!("CARGO_PKG_VERSION"))),
214            },
215        },
216    })
217    .module("whereat")
218    .build();
219
220#[doc(hidden)]
221pub fn at_crate_info() -> &'static AtCrateInfo {
222    &__AT_CRATE_INFO
223}
224
225/// Internal macro for commit detection chain.
226#[doc(hidden)]
227#[macro_export]
228macro_rules! __whereat_detect_commit {
229    () => {
230        match option_env!("GIT_COMMIT") {
231            Some(c) => Some(c),
232            None => match option_env!("GITHUB_SHA") {
233                Some(c) => Some(c),
234                None => match option_env!("CI_COMMIT_SHA") {
235                    Some(c) => Some(c),
236                    None => Some(concat!("v", env!("CARGO_PKG_VERSION"))),
237                },
238            },
239        }
240    };
241}
242
243/// Define crate-level error tracking info. Call once in your crate root (lib.rs or main.rs).
244///
245/// This creates a static and getter function that `at!()` and `at_crate!()` use.
246/// For compile-time configuration, use this macro. For runtime configuration,
247/// define your own `at_crate_info()` function using `OnceLock`.
248///
249/// ## Basic Usage
250///
251/// ```rust,ignore
252/// // In lib.rs or main.rs
253/// whereat::define_at_crate_info!();
254/// ```
255///
256/// ## With Options
257///
258/// ```rust,ignore
259/// whereat::define_at_crate_info!(
260///     path = "crates/mylib/",
261///     meta = &[("team", "platform"), ("service", "auth")],
262/// );
263/// ```
264///
265/// ## Runtime Configuration
266///
267/// For runtime metadata (e.g., instance IDs), define your own getter:
268///
269/// ```rust,ignore
270/// use std::sync::OnceLock;
271/// use whereat::AtCrateInfo;
272///
273/// static CRATE_INFO: OnceLock<AtCrateInfo> = OnceLock::new();
274///
275/// pub(crate) fn at_crate_info() -> &'static AtCrateInfo {
276///     CRATE_INFO.get_or_init(|| {
277///         AtCrateInfo::builder()
278///             .name_owned(env!("CARGO_PKG_NAME").into())
279///             .meta_owned(vec![("instance_id".into(), get_instance_id())])
280///             .build()
281///     })
282/// }
283/// ```
284///
285/// ## Available Options
286///
287/// - `path = "..."` - Crate path within repository (for workspace crates)
288/// - `meta = &[...]` - Custom key-value metadata (compile-time)
289///
290/// ## How It Works
291///
292/// The macro captures at compile time:
293/// - `CARGO_PKG_NAME` - crate name
294/// - `CARGO_PKG_REPOSITORY` - repository URL from Cargo.toml
295/// - `GIT_COMMIT` / `GITHUB_SHA` / `CI_COMMIT_SHA` - commit hash (or `v{VERSION}` fallback)
296#[macro_export]
297macro_rules! define_at_crate_info {
298    // Base case: no options (uses CRATE_PATH from env if set)
299    () => {
300        #[doc(hidden)]
301        #[allow(dead_code)]
302        static __AT_CRATE_INFO: $crate::AtCrateInfo = $crate::AtCrateInfo::builder()
303            .name(env!("CARGO_PKG_NAME"))
304            .repo(option_env!("CARGO_PKG_REPOSITORY"))
305            .commit($crate::__whereat_detect_commit!())
306            .path(option_env!("CRATE_PATH"))
307            .module(module_path!())
308            .build();
309
310        #[doc(hidden)]
311        #[allow(dead_code)]
312        pub(crate) fn at_crate_info() -> &'static $crate::AtCrateInfo {
313            &__AT_CRATE_INFO
314        }
315    };
316
317    // With path only
318    (path = $path:literal $(,)?) => {
319        #[doc(hidden)]
320        #[allow(dead_code)]
321        static __AT_CRATE_INFO: $crate::AtCrateInfo = $crate::AtCrateInfo::builder()
322            .name(env!("CARGO_PKG_NAME"))
323            .repo(option_env!("CARGO_PKG_REPOSITORY"))
324            .commit($crate::__whereat_detect_commit!())
325            .path(Some($path))
326            .module(module_path!())
327            .build();
328
329        #[doc(hidden)]
330        #[allow(dead_code)]
331        pub(crate) fn at_crate_info() -> &'static $crate::AtCrateInfo {
332            &__AT_CRATE_INFO
333        }
334    };
335
336    // With meta only (uses CRATE_PATH from env if set)
337    (meta = $meta:expr $(,)?) => {
338        #[doc(hidden)]
339        #[allow(dead_code)]
340        static __AT_CRATE_INFO: $crate::AtCrateInfo = $crate::AtCrateInfo::builder()
341            .name(env!("CARGO_PKG_NAME"))
342            .repo(option_env!("CARGO_PKG_REPOSITORY"))
343            .commit($crate::__whereat_detect_commit!())
344            .path(option_env!("CRATE_PATH"))
345            .module(module_path!())
346            .meta($meta)
347            .build();
348
349        #[doc(hidden)]
350        #[allow(dead_code)]
351        pub(crate) fn at_crate_info() -> &'static $crate::AtCrateInfo {
352            &__AT_CRATE_INFO
353        }
354    };
355
356    // With path and meta
357    (path = $path:literal, meta = $meta:expr $(,)?) => {
358        #[doc(hidden)]
359        #[allow(dead_code)]
360        static __AT_CRATE_INFO: $crate::AtCrateInfo = $crate::AtCrateInfo::builder()
361            .name(env!("CARGO_PKG_NAME"))
362            .repo(option_env!("CARGO_PKG_REPOSITORY"))
363            .commit($crate::__whereat_detect_commit!())
364            .path(Some($path))
365            .module(module_path!())
366            .meta($meta)
367            .build();
368
369        #[doc(hidden)]
370        #[allow(dead_code)]
371        pub(crate) fn at_crate_info() -> &'static $crate::AtCrateInfo {
372            &__AT_CRATE_INFO
373        }
374    };
375
376    // With meta and path (reversed order)
377    (meta = $meta:expr, path = $path:literal $(,)?) => {
378        $crate::define_at_crate_info!(path = $path, meta = $meta);
379    };
380}
381
382/// Start tracing an error with crate metadata for repository links.
383///
384/// Requires `define_at_crate_info!()` or a custom `at_crate_info()` function.
385///
386/// ## Setup (once in lib.rs)
387///
388/// ```rust,ignore
389/// whereat::define_at_crate_info!();
390/// ```
391///
392/// ## Usage
393///
394/// ```rust,ignore
395/// use whereat::{at, At};
396///
397/// fn find_user(id: u64) -> Result<String, At<MyError>> {
398///     if id == 0 {
399///         return Err(at!(MyError::NotFound));
400///     }
401///     Ok(format!("User {}", id))
402/// }
403/// ```
404///
405/// ## Without Crate Info
406///
407/// If you don't need GitHub links, use the `at()` function instead:
408///
409/// ```rust
410/// use whereat::{at, At};
411///
412/// #[derive(Debug)]
413/// struct MyError;
414///
415/// let err: At<MyError> = at(MyError);  // No crate info, no getter needed
416/// ```
417#[macro_export]
418#[allow(clippy::crate_in_macro_def)] // Intentional: calls caller's crate getter
419macro_rules! at {
420    ($err:expr) => {{
421        $crate::At::wrap($err)
422            .set_crate_info(crate::at_crate_info())
423            .at()
424    }};
425}
426
427/// Add crate boundary marker to a Result with an `At<E>` error.
428///
429/// Requires `define_at_crate_info!()` or a custom `at_crate_info()` function.
430/// Use at crate boundaries when consuming errors from dependencies.
431///
432/// ## Setup (once in lib.rs)
433///
434/// ```rust,ignore
435/// whereat::define_at_crate_info!();
436/// ```
437///
438/// ## Usage
439///
440/// ```rust,ignore
441/// use whereat::{at_crate, At, ResultAtExt};
442///
443/// fn my_function() -> Result<(), At<DepError>> {
444///     at_crate!(dependency::call())?;  // Mark crate boundary
445///     Ok(())
446/// }
447/// ```
448#[macro_export]
449#[allow(clippy::crate_in_macro_def)] // Intentional: calls caller's crate getter
450macro_rules! at_crate {
451    ($result:expr) => {{ $crate::ResultAtExt::at_crate($result, crate::at_crate_info()) }};
452}
453
454/// Wrap any value in `At<E>` and capture the caller's location.
455///
456/// This function works with any type, not just `Error` types.
457/// For types implementing `Error`, you can also use `.start_at()`.
458/// For crate-aware tracing with GitHub links, use `at!()` instead.
459///
460/// ## Example
461///
462/// ```rust
463/// use whereat::{at, At};
464///
465/// #[derive(Debug)]
466/// struct SimpleError { code: u32 }
467///
468/// fn fallible() -> Result<(), At<SimpleError>> {
469///     Err(at(SimpleError { code: 42 }))
470/// }
471/// ```
472#[track_caller]
473#[inline]
474pub fn at<E>(err: E) -> At<E> {
475    At::wrap(err).at()
476}
477
478// Extension traits are in ext.rs
479
480#[cfg(test)]
481mod tests;