Skip to content

Commit a1868ea

Browse files
authored
attributes: add err(Debug) meta to use Debug impl (#1631)
## Motivation This PR attempts to solve #1630 by introducing `err(Debug)` meta to `intrument` attribute macro. As `err` meta causes the error (`e`) returned by instrumented function to be passed to `tracing::error!(error = %e)` i.e. makes it use the `Display` implementation of `e`, the newly added `err(Debug)` makes expands to `tracing::error!(error = ?e)` which makes the `error!` macro to use `Debug` implementation for `e`. `err` and `err(Debug)` are mutually exclusive, adding both will create a compilation error. `err(Display)` is also supported to specify `Display` explicitly. As tried to describe, for some types implementing `Error` it might be more suitable to use `Debug` implementation as in the case of `eyre::Result`. This frees us to manually go over the error chain and print them all, so that `instrument` attribute macro would do it for us. ## Solution - Added a custom keyword `err(Debug)` similar to `err`, - Add `err(Debug)` field to `InstrumentArgs`, - Add parsing for `err(Debug)` arg and check for conflicts with `err`, - Generate `tracing::error!(error = ?e)` when `err(Debug)` is `true` and `tracing::error!(error = %e)` when `err(Display)` or `err` is `true`, - Interpolate generated `err_block` into `Err` branches in both async and sync return positions, if `err` or `err(Debug)` is `true`.
1 parent 9396211 commit a1868ea

File tree

2 files changed

+114
-15
lines changed

2 files changed

+114
-15
lines changed

tracing-attributes/src/lib.rs

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ use syn::{
182182
/// ```
183183
///
184184
/// If the function returns a `Result<T, E>` and `E` implements `std::fmt::Display`, you can add
185-
/// `err` to emit error events when the function returns `Err`:
185+
/// `err` or `err(Display)` to emit error events when the function returns `Err`:
186186
///
187187
/// ```
188188
/// # use tracing_attributes::instrument;
@@ -192,6 +192,18 @@ use syn::{
192192
/// }
193193
/// ```
194194
///
195+
/// By default, error values will be recorded using their `std::fmt::Display` implementations.
196+
/// If an error implements `std::fmt::Debug`, it can be recorded using its `Debug` implementation
197+
/// instead, by writing `err(Debug)`:
198+
///
199+
/// ```
200+
/// # use tracing_attributes::instrument;
201+
/// #[instrument(err(Debug))]
202+
/// fn my_function(arg: usize) -> Result<(), std::io::Error> {
203+
/// Ok(())
204+
/// }
205+
/// ```
206+
///
195207
/// `async fn`s may also be instrumented:
196208
///
197209
/// ```
@@ -393,8 +405,6 @@ fn gen_block(
393405
instrumented_function_name: &str,
394406
self_type: Option<&syn::TypePath>,
395407
) -> proc_macro2::TokenStream {
396-
let err = args.err;
397-
398408
// generate the span's name
399409
let span_name = args
400410
// did the user override the span's name?
@@ -507,29 +517,34 @@ fn gen_block(
507517
))
508518
})();
509519

520+
let err_event = match args.err_mode {
521+
Some(ErrorMode::Display) => Some(quote!(tracing::error!(error = %e))),
522+
Some(ErrorMode::Debug) => Some(quote!(tracing::error!(error = ?e))),
523+
_ => None,
524+
};
525+
510526
// Generate the instrumented function body.
511527
// If the function is an `async fn`, this will wrap it in an async block,
512528
// which is `instrument`ed using `tracing-futures`. Otherwise, this will
513529
// enter the span and then perform the rest of the body.
514530
// If `err` is in args, instrument any resulting `Err`s.
515531
if async_context {
516-
let mk_fut = if err {
517-
quote_spanned!(block.span()=>
532+
let mk_fut = match err_event {
533+
Some(err_event) => quote_spanned!(block.span()=>
518534
async move {
519535
match async move { #block }.await {
520536
#[allow(clippy::unit_arg)]
521537
Ok(x) => Ok(x),
522538
Err(e) => {
523-
tracing::error!(error = %e);
539+
#err_event;
524540
Err(e)
525541
}
526542
}
527543
}
528-
)
529-
} else {
530-
quote_spanned!(block.span()=>
544+
),
545+
None => quote_spanned!(block.span()=>
531546
async move { #block }
532-
)
547+
),
533548
};
534549

535550
return quote!(
@@ -566,15 +581,15 @@ fn gen_block(
566581
}
567582
);
568583

569-
if err {
584+
if let Some(err_event) = err_event {
570585
return quote_spanned!(block.span()=>
571586
#span
572587
#[allow(clippy::redundant_closure_call)]
573588
match (move || #block)() {
574589
#[allow(clippy::unit_arg)]
575590
Ok(x) => Ok(x),
576591
Err(e) => {
577-
tracing::error!(error = %e);
592+
#err_event;
578593
Err(e)
579594
}
580595
}
@@ -603,7 +618,7 @@ struct InstrumentArgs {
603618
target: Option<LitStr>,
604619
skips: HashSet<Ident>,
605620
fields: Option<Fields>,
606-
err: bool,
621+
err_mode: Option<ErrorMode>,
607622
/// Errors describing any unrecognized parse inputs that we skipped.
608623
parse_warnings: Vec<syn::Error>,
609624
}
@@ -728,8 +743,9 @@ impl Parse for InstrumentArgs {
728743
}
729744
args.fields = Some(input.parse()?);
730745
} else if lookahead.peek(kw::err) {
731-
let _ = input.parse::<kw::err>()?;
732-
args.err = true;
746+
let _ = input.parse::<kw::err>();
747+
let mode = ErrorMode::parse(input)?;
748+
args.err_mode = Some(mode);
733749
} else if lookahead.peek(Token![,]) {
734750
let _ = input.parse::<Token![,]>()?;
735751
} else {
@@ -787,6 +803,39 @@ impl Parse for Skips {
787803
}
788804
}
789805

806+
#[derive(Debug, Hash, PartialEq, Eq)]
807+
enum ErrorMode {
808+
Display,
809+
Debug,
810+
}
811+
812+
impl Default for ErrorMode {
813+
fn default() -> Self {
814+
ErrorMode::Display
815+
}
816+
}
817+
818+
impl Parse for ErrorMode {
819+
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
820+
if !input.peek(syn::token::Paren) {
821+
return Ok(ErrorMode::default());
822+
}
823+
let content;
824+
let _ = syn::parenthesized!(content in input);
825+
let maybe_mode: Option<Ident> = content.parse()?;
826+
maybe_mode.map_or(Ok(ErrorMode::default()), |ident| {
827+
match ident.to_string().as_str() {
828+
"Debug" => Ok(ErrorMode::Debug),
829+
"Display" => Ok(ErrorMode::Display),
830+
_ => Err(syn::Error::new(
831+
ident.span(),
832+
"unknown error mode, must be Debug or Display",
833+
)),
834+
}
835+
})
836+
}
837+
}
838+
790839
#[derive(Debug)]
791840
struct Fields(Punctuated<Field, Token![,]>);
792841

tracing-attributes/tests/err.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,53 @@ fn impl_trait_return_type() {
174174

175175
handle.assert_finished();
176176
}
177+
178+
#[instrument(err(Debug))]
179+
fn err_dbg() -> Result<u8, TryFromIntError> {
180+
u8::try_from(1234)
181+
}
182+
183+
#[test]
184+
fn test_err_dbg() {
185+
let span = span::mock().named("err_dbg");
186+
let (collector, handle) = collector::mock()
187+
.new_span(span.clone())
188+
.enter(span.clone())
189+
.event(
190+
event::mock().at_level(Level::ERROR).with_fields(
191+
field::mock("error")
192+
// use the actual error value that will be emitted, so
193+
// that this test doesn't break if the standard library
194+
// changes the `fmt::Debug` output from the error type
195+
// in the future.
196+
.with_value(&tracing::field::debug(u8::try_from(1234).unwrap_err())),
197+
),
198+
)
199+
.exit(span.clone())
200+
.drop_span(span)
201+
.done()
202+
.run_with_handle();
203+
with_default(collector, || err_dbg().ok());
204+
handle.assert_finished();
205+
}
206+
207+
#[test]
208+
fn test_err_display_default() {
209+
let span = span::mock().named("err");
210+
let (collector, handle) = collector::mock()
211+
.new_span(span.clone())
212+
.enter(span.clone())
213+
.event(
214+
event::mock().at_level(Level::ERROR).with_fields(
215+
field::mock("error")
216+
// by default, errors will be emitted with their display values
217+
.with_value(&tracing::field::display(u8::try_from(1234).unwrap_err())),
218+
),
219+
)
220+
.exit(span.clone())
221+
.drop_span(span)
222+
.done()
223+
.run_with_handle();
224+
with_default(collector, || err().ok());
225+
handle.assert_finished();
226+
}

0 commit comments

Comments
 (0)