Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

attributes: add err(Debug) meta to use Debug impl #1631

Merged
merged 5 commits into from
Oct 15, 2021

Conversation

ciuncan
Copy link
Contributor

@ciuncan ciuncan commented Oct 10, 2021

Motivation

This PR attempts to solve #1630 by introducing err_dbg 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_dbg makes expands to tracing::error!(error = ?e) which makes the error! macro to use Debug implementation for e. err and err_dbg are mutually exclusive, adding both will create a compilation error.

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_db similar to err,
  • Add err_dbg field to InstrumentArgs,
  • Add parsing for err_dbg arg and check for conflicts with err,
  • Generate tracing::error!(error = ?e) when err_dbg is true and tracing::error!(error = %e) when err is true,
  • Interpolate generated err_block into Err branches in both async and sync return positions, if err or err_dbg is true.

Even though I copied one of the tests, I'm not sure if it helps to check the new behavior. I manually tested it using VSCode/rust-analyzer's "Expand macro recursively" facility. But I am open to better test suggestions.

Copy link
Member

@hawkw hawkw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this looks great. I had a few suggestions that we might want to address before merging this.

Also, it would be nice to add to the documentation for #[instrument(err)] to explain how to use Debug instead of Display using the new syntax:

/// If the function returns a `Result<T, E>` and `E` implements `std::fmt::Display`, you can add
/// `err` to emit error events when the function returns `Err`:
///
/// ```
/// # use tracing_attributes::instrument;
/// #[instrument(err)]
/// fn my_function(arg: usize) -> Result<(), std::io::Error> {
/// Ok(())
/// }
/// ```

Thanks for working on this!!

@@ -507,29 +506,34 @@ fn gen_block(
))
})();

let err_block = match args.err_mode {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, take it or leave it: not sure if err_block is the best name for this; it's not actually a block, just an expression. maybe

Suggested change
let err_block = match args.err_mode {
let err_event = match args.err_mode {

Comment on lines 521 to 522
let mk_fut = match err_block {
Some(err_block) => quote_spanned!(block.span()=>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you apply my naming suggestion from above, we would also need to change this

Suggested change
let mk_fut = match err_block {
Some(err_block) => quote_spanned!(block.span()=>
let mk_fut = match err_block {
Some(err_block) => quote_spanned!(block.span()=>
Suggested change
let mk_fut = match err_block {
Some(err_block) => quote_spanned!(block.span()=>
let mk_fut = match err_event {
Some(err_event) => quote_spanned!(block.span()=>

async move {
match async move { #block }.await {
#[allow(clippy::unit_arg)]
Ok(x) => Ok(x),
Err(e) => {
tracing::error!(error = %e);
#err_block;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...and we'd change this as well

Suggested change
#err_block;
#err_event;

@@ -566,15 +570,15 @@ fn gen_block(
}
);

if err {
if let Some(err_block) = err_block {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...and we'd change this

Suggested change
if let Some(err_block) = err_block {
if let Some(err_event) = err_event {

return quote_spanned!(block.span()=>
#span
#[allow(clippy::redundant_closure_call)]
match (move || #block)() {
#[allow(clippy::unit_arg)]
Ok(x) => Ok(x),
Err(e) => {
tracing::error!(error = %e);
#err_block;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#err_block;
#err_event;

Comment on lines 798 to 799
}
impl Default for ErrorMode {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: prefer a line of whitespace between definitions & impl blocks:

Suggested change
}
impl Default for ErrorMode {
}
impl Default for ErrorMode {

Comment on lines 828 to 837
let modes: Punctuated<ErrorMode, Token![,]> = content.parse_terminated(ErrorMode::parse)?;
let modes = modes.into_iter().collect::<HashSet<_>>();
return match modes.len() {
0 => Ok(Self(ErrorMode::Display)),
1 => Ok(Self(modes.into_iter().next().unwrap())),
_ => Err(syn::Error::new(
content.span(),
"at most one of Debug or Display can be given",
))
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify this a bit --- it shouldn't be necessary to use parse_terminated to parse any number of error modes, and then emit an error if we parsed more than one. I think, instead, we can use the Parse impl for Option<T: Parse + Token> in syn.

I believe that should allow us to replace the ErrorModes type entirely. We could move the parenthesized! inside the ErrorMode parser, and just try to parse an Option<ErrorMode> when we find an err` token.

This should hopefully be much simpler!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might have misunderstood but, I couldn't implement Token trait. It appears to be supposed to parse some punctuation, identifiers, keywords etc. Also it looks like sealed.

I went ahead and utilized Option<Ident> to parse an ErrorMode by its name, I moved most of the parsing including the parenthesis into impl Parse for ErrorMode.

Overall this looks simpler, but let me know if I further can improve it.

let (collector, handle) = collector::mock()
.new_span(span.clone())
.enter(span.clone())
.event(event::mock().at_level(Level::ERROR))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding the test! it might be nice to change this line to also expect that the error field is recorded with its Debug implementation (rather than with Display).

we could do that like this:

Suggested change
.event(event::mock().at_level(Level::ERROR))
.event(
event::mock()
.at_level(Level::ERROR)
.with_field(
field::mock("error")
// use the actual error value that will be emitted, so
// that this test doesn't break if the standard library
// changes the `fmt::Debug` output from the error type
// in the future.
.with_value(&tracing::field::debug(u8::try_from(1234).unwrap_err())
)
)

but, the other tests don't make assertions about the formatted value of the error, so this isn't strictly necessary --- I just thought it might be nice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect! Honestly I didn't know what I was doing, and just copied one of the tests. This is what I was looking for. 👍 Will apply.

@ciuncan ciuncan requested a review from hawkw October 14, 2021 18:26
@hawkw hawkw marked this pull request as ready for review October 14, 2021 21:20
@hawkw hawkw requested review from davidbarsky and a team as code owners October 14, 2021 21:20
Copy link
Member

@hawkw hawkw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this looks good to me --- I commented on a couple minor things. However, it does look like CI is failing because rustfmt needs to be run: https://github.com/tokio-rs/tracing/pull/1631/checks?check_run_id=3899421739 (this is actually my fault, since I wrote the suggestion for the test change in the GitHub suggestions UI and didn't run rustfmt on it...). Would you mind running rustfmt on this branch and committing the results?

Comment on lines 195 to 197
/// The above example will be emitting error events using the `std::fmt::Display` implementation.
/// If `E` implements `std::fmt::Debug`, you can also make it use that implementation with
/// `err(Debug)`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about changing this to something like:

Suggested change
/// The above example will be emitting error events using the `std::fmt::Display` implementation.
/// If `E` implements `std::fmt::Debug`, you can also make it use that implementation with
/// `err(Debug)`:
/// By default, error values will be recorded using their `std::fmt::Display` implementations.
/// If an error implements `std::fmt::Debug`, it can be recorded using its `Debug` implementation
/// instead, by writing `err(Debug)`:

Comment on lines 840 to 849
struct ErrorModes(ErrorMode);

impl Parse for ErrorModes {

fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let _ = input.parse::<kw::err>();
let mode = ErrorMode::parse(input)?;
Ok(Self(mode))
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the name ErrorModes is not really accurate for this anymore, since it no longer parses a list of error modes, and now simply parses the err keyword along with the mode. Maybe we should just rename it to

Suggested change
struct ErrorModes(ErrorMode);
impl Parse for ErrorModes {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let _ = input.parse::<kw::err>();
let mode = ErrorMode::parse(input)?;
Ok(Self(mode))
}
}
struct Error(ErrorMode);
impl Parse for Error {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let _ = input.parse::<kw::err>();
let mode = ErrorMode::parse(input)?;
Ok(Self(mode))
}
}

or, we could probably just get rid of this entirely, and move the err keyword into InstrumentArgs, like it was before --- we would just try to parse an ErrorMode if we parsed the err keyword, inside of the Parse impl for InstrumentArgs.

IMO, having a separate type for this is not really necessary any longer, but it's up to you...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right, I completely missed this. I think removing Error and doing err parsing into the InstrumentArgs Parse impl. 👍

@ciuncan
Copy link
Contributor Author

ciuncan commented Oct 14, 2021

Overall, this looks good to me --- I commented on a couple minor things. However, it does look like CI is failing because rustfmt needs to be run: https://github.com/tokio-rs/tracing/pull/1631/checks?check_run_id=3899421739 (this is actually my fault, since I wrote the suggestion for the test change in the GitHub suggestions UI and didn't run rustfmt on it...). Would you mind running rustfmt on this branch and committing the results?

Ah sorry about that, I didn't check style CI step. I was so used to my cargo-husky hooks on our repository, it runs it as pre-commit hook and prevents if there is any diff produced by cargo fmt, I guess I was spoiled by that. 😅

Applied the suggested changes 👍

Copy link
Member

@hawkw hawkw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! Thanks again for working on this --- I'm going to go ahead and merge it when CI finishes!

@hawkw hawkw changed the title [WIP] attributes: add err_dbg meta to use Debug impl attributes: add err(Debug) meta to use Debug impl Oct 14, 2021
@hawkw hawkw merged commit a1868ea into tokio-rs:master Oct 15, 2021
hawkw pushed a commit that referenced this pull request Oct 22, 2021
## 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`.
hawkw pushed a commit that referenced this pull request Oct 22, 2021
## 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`.
hawkw added a commit that referenced this pull request Feb 4, 2022
# 0.1.19 (February 3, 2022)

This release introduces a new `#[instrument(ret)]` argument to emit an
event with the return value of an instrumented function.

### Added

- `#[instrument(ret)]` to record the return value of a function
  ([#1716])
- added `err(Debug)` argument to cause `#[instrument(err)]` to record
  errors with `Debug` rather than `Display ([#1631])

### Fixed

- incorrect code generation for functions returning async blocks
  ([#1866])
- incorrect diagnostics when using `rust-analyzer` ([#1634])

Thanks to @Swatinem, @hkmatsumoto, @cynecx, and @ciuncan for
contributing to this release!

[#1716]: #1716
[#1631]: #1631
[#1634]: #1634
[#1866]: #1866
hawkw added a commit that referenced this pull request Feb 4, 2022
# 0.1.19 (February 3, 2022)

This release introduces a new `#[instrument(ret)]` argument to emit an
event with the return value of an instrumented function.

### Added

- `#[instrument(ret)]` to record the return value of a function
  ([#1716])
- added `err(Debug)` argument to cause `#[instrument(err)]` to record
  errors with `Debug` rather than `Display ([#1631])

### Fixed

- incorrect code generation for functions returning async blocks
  ([#1866])
- incorrect diagnostics when using `rust-analyzer` ([#1634])

Thanks to @Swatinem, @hkmatsumoto, @cynecx, and @ciuncan for
contributing to this release!

[#1716]: #1716
[#1631]: #1631
[#1634]: #1634
[#1866]: #1866
kaffarell pushed a commit to kaffarell/tracing that referenced this pull request May 22, 2024
# 0.1.19 (February 3, 2022)

This release introduces a new `#[instrument(ret)]` argument to emit an
event with the return value of an instrumented function.

### Added

- `#[instrument(ret)]` to record the return value of a function
  ([tokio-rs#1716])
- added `err(Debug)` argument to cause `#[instrument(err)]` to record
  errors with `Debug` rather than `Display ([tokio-rs#1631])

### Fixed

- incorrect code generation for functions returning async blocks
  ([tokio-rs#1866])
- incorrect diagnostics when using `rust-analyzer` ([tokio-rs#1634])

Thanks to @Swatinem, @hkmatsumoto, @cynecx, and @ciuncan for
contributing to this release!

[tokio-rs#1716]: tokio-rs#1716
[tokio-rs#1631]: tokio-rs#1631
[tokio-rs#1634]: tokio-rs#1634
[tokio-rs#1866]: tokio-rs#1866
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants