tsoracle_server/bt.rs
1//
2// ░▀█▀░█▀▀░█▀█░█▀▄░█▀█░█▀▀░█░░░█▀▀
3// ░░█░░▀▀█░█░█░█▀▄░█▀█░█░░░█░░░█▀▀
4// ░░▀░░▀▀▀░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀
5//
6// tsoracle — Distributed Timestamp Oracle
7// https://www.tsoracle.rs
8//
9// Copyright (c) 2026 Prisma Risk
10//
11// Licensed under the Apache License, Version 2.0 (the "License");
12// you may not use this file except in compliance with the License.
13// You may obtain a copy of the License at
14//
15// https://www.apache.org/licenses/LICENSE-2.0
16//
17// Unless required by applicable law or agreed to in writing, software
18// distributed under the License is distributed on an "AS IS" BASIS,
19// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20// See the License for the specific language governing permissions and
21// limitations under the License.
22//
23
24//! Optional backtrace capture for error types.
25//!
26//! [`Bt`] is a zero-sized type unless the `bt` Cargo feature is enabled. With
27//! the feature off, [`Bt::capture`] is a no-op and the field adds nothing to
28//! the enclosing error's layout. With the feature on, [`Bt::capture`] delegates
29//! to [`std::backtrace::Backtrace::capture`], which itself honors the
30//! `RUST_BACKTRACE` / `RUST_LIB_BACKTRACE` environment variables — so symbol
31//! resolution stays opt-in at runtime even when the feature is compiled in.
32//!
33//! Embedding `Bt` in a `thiserror` enum variant:
34//!
35//! ```ignore
36//! use tsoracle_server::Bt;
37//!
38//! #[derive(Debug, thiserror::Error)]
39//! pub enum MyError {
40//! #[error("something went wrong{bt}")]
41//! SomethingWentWrong { bt: Bt },
42//! }
43//!
44//! let err = MyError::SomethingWentWrong { bt: Bt::capture() };
45//! ```
46
47use core::fmt;
48
49/// Optional captured backtrace.
50///
51/// See module docs for behavior under the `bt` feature.
52#[cfg(feature = "bt")]
53#[derive(Debug)]
54pub struct Bt(std::backtrace::Backtrace);
55
56#[cfg(not(feature = "bt"))]
57#[derive(Debug, Clone, Copy)]
58pub struct Bt;
59
60impl Bt {
61 /// Capture a backtrace at the call site.
62 ///
63 /// With the `bt` feature off this is a no-op that returns the unit-sized
64 /// [`Bt`]. With the feature on, capture is delegated to
65 /// [`std::backtrace::Backtrace::capture`] and is gated by the
66 /// `RUST_BACKTRACE` / `RUST_LIB_BACKTRACE` environment variables.
67 #[inline]
68 pub fn capture() -> Self {
69 #[cfg(feature = "bt")]
70 {
71 Self(std::backtrace::Backtrace::capture())
72 }
73 #[cfg(not(feature = "bt"))]
74 {
75 Self
76 }
77 }
78}
79
80impl fmt::Display for Bt {
81 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82 #[cfg(feature = "bt")]
83 {
84 use std::backtrace::BacktraceStatus;
85 if matches!(self.0.status(), BacktraceStatus::Captured) {
86 // Newline-prefix so the backtrace renders on its own line
87 // beneath the error message in default `Display` output.
88 write!(formatter, "\n{}", self.0)?;
89 }
90 Ok(())
91 }
92 #[cfg(not(feature = "bt"))]
93 {
94 let _ = formatter;
95 Ok(())
96 }
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn capture_does_not_panic() {
106 let _ = Bt::capture();
107 }
108
109 #[test]
110 fn display_is_format_safe() {
111 // Must format without panic in either feature mode. Whether content
112 // is present depends on `RUST_BACKTRACE`/`RUST_LIB_BACKTRACE` and the
113 // feature flag; only absence-of-panic is asserted here.
114 let _ = format!("{}", Bt::capture());
115 }
116
117 #[cfg(not(feature = "bt"))]
118 #[test]
119 fn bt_is_zero_sized_without_feature() {
120 // Without the feature, embedding `Bt` in a struct must add no bytes.
121 // This is the load-bearing claim that makes opt-in instrumentation
122 // free for downstream crates that never enable `bt`.
123 assert_eq!(core::mem::size_of::<Bt>(), 0);
124 }
125
126 #[cfg(not(feature = "bt"))]
127 #[test]
128 fn display_is_empty_without_feature() {
129 // No trailing backtrace section under `Display` when the feature is
130 // off. Important so `#[error("...{bt}")]` formats identically to
131 // `#[error("...")]` in the off configuration.
132 assert_eq!(format!("{}", Bt::capture()), "");
133 }
134
135 #[cfg(feature = "bt")]
136 #[test]
137 fn bt_is_nonzero_sized_with_feature() {
138 // With the feature on, `Bt` holds a `std::backtrace::Backtrace` and
139 // therefore must occupy some space.
140 assert!(core::mem::size_of::<Bt>() > 0);
141 }
142
143 #[cfg(feature = "bt")]
144 #[test]
145 fn display_renders_captured_backtrace() {
146 // Exercise the `BacktraceStatus::Captured` branch of `Display::fmt`
147 // without relying on `RUST_BACKTRACE` at test time (coverage runs
148 // don't set it). `force_capture` ignores the env var and always
149 // produces a `Captured` status, which is what gates the `write!`.
150 let bt = Bt(std::backtrace::Backtrace::force_capture());
151 let rendered = format!("{bt}");
152 assert!(
153 rendered.starts_with('\n'),
154 "captured backtrace must render with a leading newline so it sits \
155 beneath the error message; got {rendered:?}",
156 );
157 assert!(rendered.len() > 1, "captured backtrace must be non-empty");
158 }
159}