tor_error/tracing.rs
1//! Support for using `tor-error` with the `tracing` crate.
2
3use crate::ErrorKind;
4
5#[doc(hidden)]
6pub use static_assertions;
7#[doc(hidden)]
8pub use tracing::{event, Level};
9
10use paste::paste;
11
12impl ErrorKind {
13 /// Return true if this [`ErrorKind`] should always be logged as
14 /// a warning (or more severe).
15 pub fn is_always_a_warning(&self) -> bool {
16 matches!(self, ErrorKind::Internal | ErrorKind::BadApiUsage)
17 }
18}
19
20#[doc(hidden)]
21/// Return true if a given string has an ending that makes sense for our
22/// formats.
23pub const fn fmt_ending_ok(s: &str) -> bool {
24 // This implementation is slightly roundabout because we need this function
25 // to be const.
26 match s.as_bytes() {
27 [.., b'.', b'.', b'.'] => Ok(()),
28 [.., b' ' | b'.' | b':'] => Err(()),
29 _ => Ok(()),
30 }
31 .is_ok()
32}
33
34/// Log a [`Report`](crate::Report) of a provided error at a given level, or a
35/// higher level if appropriate.
36///
37/// (If [`ErrorKind::is_always_a_warning`] returns true for the error's kind, we
38/// log it at WARN, unless this event is already at level WARN or ERROR.)
39///
40/// We require that the format string _not_ end with ' ', ',' or ':'; if it doesn't,
41/// we produce a compile-time error.
42///
43/// # Examples
44///
45/// ```
46/// # // this is what implements HasKind in this crate.
47/// # fn demo(err: &futures::task::SpawnError) {
48/// # let num = 7;
49/// use tor_error::event_report;
50/// use tracing::Level;
51///
52/// event_report!(Level::DEBUG, err, "Couldn't chew gum while walking");
53///
54/// event_report!(Level::TRACE, err, "Ephemeral error on attempt #{}", num);
55/// # }
56/// ```
57///
58/// # Limitations
59///
60/// This macro does not support the full range of syntaxes supported by
61/// [`tracing::event!`].
62///
63/// The compile-time error produced when the format string has a bad ending is
64/// kind of confusing. This is a limitation of the `static_assertions` crate.
65//
66// NOTE: We need this fancy conditional here because tracing::event! insists on
67// getting a const expression for its `Level`. So we can do
68// `if cond {debug!(..)} else {warn!(..)}`,
69// but we can't do
70// `event!(if cond {DEBUG} else {WARN}, ..)`.
71#[macro_export]
72macro_rules! event_report {
73 ($level:expr, $err:expr, $fmt:literal, $($arg:expr),* $(,)?) => {
74 {
75 use $crate::{tracing as tr, HasKind as _, };
76 let err = $err;
77 if err.kind().is_always_a_warning() && tr::Level::WARN < $level {
78 $crate::event_report!(@raw tr::Level::WARN, err, $fmt, $($arg),*);
79 } else {
80 $crate::event_report!(@raw $level, err, $fmt, $($arg),*);
81 }
82 }
83 };
84
85 ($level:expr, $err:expr, $fmt:literal) => {
86 $crate::event_report!($level, $err, $fmt, )
87 };
88
89 (@raw $level:expr, $err:expr, $fmt:literal $(, $arg:expr)* $(,)?) => {
90 {
91 use $crate::{tracing as tr, ErrorReport as _};
92 tr::static_assertions::const_assert!(
93 tr::fmt_ending_ok($fmt)
94 );
95 tr::event!(
96 $level,
97 concat!($fmt, ": {}"),
98 $($arg ,)*
99 ($err).report()
100 )
101 }
102 }
103}
104
105/// Define a macro `$level_report`
106///
107/// The title line for the doc comment will be
108/// ``$title_1 `LEVEL` $title_2``
109///
110/// A standard body, containing a set of examples, will be provided.
111///
112/// You must pass a dollar sign for `D`, because there is no dollar escaping mechanism
113/// for macro_rules macros in stable Rust (!)
114macro_rules! define_report_macros { {
115 # $title_1:tt
116 LEVEL
117 # $title_2:tt
118
119 $D:tt
120 $( [$($flag:tt)*] $level:ident )*
121} => { $( paste!{
122 # $title_1
123 #[doc = concat!("`", stringify!( [< $level:upper >] ), "`")]
124 # $title_2
125 ///
126 /// # Examples:
127 ///
128 /// ```
129 /// # fn demo(err: &futures::task::SpawnError) {
130 /// # let msg = ();
131 #[doc = concat!("use tor_error::", stringify!($level), "_report;")]
132 #[doc = concat!(stringify!($level), "_report!",
133 r#"(err, "Cheese exhausted (ephemeral)");"#)]
134 #[doc = concat!(stringify!($level), "_report!",
135 r#"(err, "Unable to parse message {:?}", msg);"#)]
136 /// # }
137 /// ```
138 #[macro_export]
139 macro_rules! [< $level _report >] {
140 ( $D err:expr, $D ($D rest:expr),+ $D (,)? ) => {
141 $D crate::event_report!($($flag)*
142 $D crate::tracing::Level::[< $level:upper >],
143 $D err, $D ($D rest),+)
144 }
145 }
146} )* } }
147
148define_report_macros! {
149 /// Log a report for `err` at level
150 LEVEL
151 /// (or higher if it is a bug).
152
153 $ [] trace
154 [] debug
155 [] info
156}
157
158define_report_macros! {
159 /// Log a report for `err` at level
160 LEVEL
161 ///
162 $ [@raw] warn
163 [@raw] error
164}
165
166#[cfg(test)]
167mod test {
168 // @@ begin test lint list maintained by maint/add_warning @@
169 #![allow(clippy::bool_assert_comparison)]
170 #![allow(clippy::clone_on_copy)]
171 #![allow(clippy::dbg_macro)]
172 #![allow(clippy::mixed_attributes_style)]
173 #![allow(clippy::print_stderr)]
174 #![allow(clippy::print_stdout)]
175 #![allow(clippy::single_char_pattern)]
176 #![allow(clippy::unwrap_used)]
177 #![allow(clippy::unchecked_duration_subtraction)]
178 #![allow(clippy::useless_vec)]
179 #![allow(clippy::needless_pass_by_value)]
180 //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
181 use crate::report::ErrorReport;
182 use thiserror::Error;
183 use tracing_test::traced_test;
184
185 #[derive(Error, Debug)]
186 #[error("my error")]
187 struct MyError;
188
189 #[test]
190 #[traced_test]
191 fn warn_report() {
192 let me = MyError;
193 let _ = me.report();
194 warn_report!(me, "reporting unwrapped");
195
196 let ae = anyhow::Error::from(me).context("context");
197 let _ = ae.report();
198 warn_report!(ae, "reporting anyhow");
199 }
200}