Skip to content

Commit d1eccec

Browse files
hkmatsumotohawkw
andcommitted
attributes: implement #[instrument(ret)] (#1716)
* attributes: implement `#[instrument(ret)]` ## Motivation Currently, users have to explicitly call logging functions at the end of functions if they wanted to record values being returned. For example, ```rust fn increment(a: i32) -> i32 { let succ = a + 1; tracing::debug!(?succ); succ } ``` The code above will be significantly simpler if we provide the functionality to record returned values. ## Solution This PR adds the exact feature we're talking here, which enables users to write the code below instead. ```rust #[instrument(ret)] fn increment(a: i32) -> i32 { a + 1 } ``` Co-authored-by: Eliza Weisman <[email protected]>
1 parent 5e57734 commit d1eccec

File tree

4 files changed

+361
-33
lines changed

4 files changed

+361
-33
lines changed

tracing-attributes/src/attr.rs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ pub(crate) struct InstrumentArgs {
1414
pub(crate) skips: HashSet<Ident>,
1515
pub(crate) skip_all: bool,
1616
pub(crate) fields: Option<Fields>,
17-
pub(crate) err_mode: Option<ErrorMode>,
17+
pub(crate) err_mode: Option<FormatMode>,
18+
pub(crate) ret_mode: Option<FormatMode>,
1819
/// Errors describing any unrecognized parse inputs that we skipped.
1920
parse_warnings: Vec<syn::Error>,
2021
}
@@ -152,8 +153,12 @@ impl Parse for InstrumentArgs {
152153
args.fields = Some(input.parse()?);
153154
} else if lookahead.peek(kw::err) {
154155
let _ = input.parse::<kw::err>();
155-
let mode = ErrorMode::parse(input)?;
156+
let mode = FormatMode::parse(input)?;
156157
args.err_mode = Some(mode);
158+
} else if lookahead.peek(kw::ret) {
159+
let _ = input.parse::<kw::ret>()?;
160+
let mode = FormatMode::parse(input)?;
161+
args.ret_mode = Some(mode);
157162
} else if lookahead.peek(Token![,]) {
158163
let _ = input.parse::<Token![,]>()?;
159164
} else {
@@ -212,29 +217,30 @@ impl Parse for Skips {
212217
}
213218

214219
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
215-
pub(crate) enum ErrorMode {
220+
pub(crate) enum FormatMode {
221+
Default,
216222
Display,
217223
Debug,
218224
}
219225

220-
impl Default for ErrorMode {
226+
impl Default for FormatMode {
221227
fn default() -> Self {
222-
ErrorMode::Display
228+
FormatMode::Default
223229
}
224230
}
225231

226-
impl Parse for ErrorMode {
232+
impl Parse for FormatMode {
227233
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
228234
if !input.peek(syn::token::Paren) {
229-
return Ok(ErrorMode::default());
235+
return Ok(FormatMode::default());
230236
}
231237
let content;
232238
let _ = syn::parenthesized!(content in input);
233239
let maybe_mode: Option<Ident> = content.parse()?;
234-
maybe_mode.map_or(Ok(ErrorMode::default()), |ident| {
240+
maybe_mode.map_or(Ok(FormatMode::default()), |ident| {
235241
match ident.to_string().as_str() {
236-
"Debug" => Ok(ErrorMode::Debug),
237-
"Display" => Ok(ErrorMode::Display),
242+
"Debug" => Ok(FormatMode::Debug),
243+
"Display" => Ok(FormatMode::Display),
238244
_ => Err(syn::Error::new(
239245
ident.span(),
240246
"unknown error mode, must be Debug or Display",
@@ -370,4 +376,5 @@ mod kw {
370376
syn::custom_keyword!(target);
371377
syn::custom_keyword!(name);
372378
syn::custom_keyword!(err);
379+
syn::custom_keyword!(ret);
373380
}

tracing-attributes/src/expand.rs

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use syn::{
99
};
1010

1111
use crate::{
12-
attr::{ErrorMode, Field, Fields, InstrumentArgs},
12+
attr::{Field, Fields, FormatMode, InstrumentArgs},
1313
MaybeItemFnRef,
1414
};
1515

@@ -191,8 +191,18 @@ fn gen_block<B: ToTokens>(
191191
})();
192192

193193
let err_event = match args.err_mode {
194-
Some(ErrorMode::Display) => Some(quote!(tracing::error!(error = %e))),
195-
Some(ErrorMode::Debug) => Some(quote!(tracing::error!(error = ?e))),
194+
Some(FormatMode::Default) | Some(FormatMode::Display) => {
195+
Some(quote!(tracing::error!(error = %e)))
196+
}
197+
Some(FormatMode::Debug) => Some(quote!(tracing::error!(error = ?e))),
198+
_ => None,
199+
};
200+
201+
let ret_event = match args.ret_mode {
202+
Some(FormatMode::Display) => Some(quote!(tracing::event!(#level, return = %x))),
203+
Some(FormatMode::Default) | Some(FormatMode::Debug) => {
204+
Some(quote!(tracing::event!(#level, return = ?x)))
205+
}
196206
_ => None,
197207
};
198208

@@ -201,9 +211,26 @@ fn gen_block<B: ToTokens>(
201211
// which is `instrument`ed using `tracing-futures`. Otherwise, this will
202212
// enter the span and then perform the rest of the body.
203213
// If `err` is in args, instrument any resulting `Err`s.
214+
// If `ret` is in args, instrument any resulting `Ok`s when the function
215+
// returns `Result`s, otherwise instrument any resulting values.
204216
if async_context {
205-
let mk_fut = match err_event {
206-
Some(err_event) => quote_spanned!(block.span()=>
217+
let mk_fut = match (err_event, ret_event) {
218+
(Some(err_event), Some(ret_event)) => quote_spanned!(block.span()=>
219+
async move {
220+
match async move { #block }.await {
221+
#[allow(clippy::unit_arg)]
222+
Ok(x) => {
223+
#ret_event;
224+
Ok(x)
225+
},
226+
Err(e) => {
227+
#err_event;
228+
Err(e)
229+
}
230+
}
231+
}
232+
),
233+
(Some(err_event), None) => quote_spanned!(block.span()=>
207234
async move {
208235
match async move { #block }.await {
209236
#[allow(clippy::unit_arg)]
@@ -215,7 +242,14 @@ fn gen_block<B: ToTokens>(
215242
}
216243
}
217244
),
218-
None => quote_spanned!(block.span()=>
245+
(None, Some(ret_event)) => quote_spanned!(block.span()=>
246+
async move {
247+
let x = async move { #block }.await;
248+
#ret_event;
249+
x
250+
}
251+
),
252+
(None, None) => quote_spanned!(block.span()=>
219253
async move { #block }
220254
),
221255
};
@@ -254,8 +288,23 @@ fn gen_block<B: ToTokens>(
254288
}
255289
);
256290

257-
if let Some(err_event) = err_event {
258-
return quote_spanned!(block.span()=>
291+
match (err_event, ret_event) {
292+
(Some(err_event), Some(ret_event)) => quote_spanned! {block.span()=>
293+
#span
294+
#[allow(clippy::redundant_closure_call)]
295+
match (move || #block)() {
296+
#[allow(clippy::unit_arg)]
297+
Ok(x) => {
298+
#ret_event;
299+
Ok(x)
300+
},
301+
Err(e) => {
302+
#err_event;
303+
Err(e)
304+
}
305+
}
306+
},
307+
(Some(err_event), None) => quote_spanned!(block.span()=>
259308
#span
260309
#[allow(clippy::redundant_closure_call)]
261310
match (move || #block)() {
@@ -266,22 +315,28 @@ fn gen_block<B: ToTokens>(
266315
Err(e)
267316
}
268317
}
269-
);
270-
}
271-
272-
quote_spanned!(block.span() =>
273-
// Because `quote` produces a stream of tokens _without_ whitespace, the
274-
// `if` and the block will appear directly next to each other. This
275-
// generates a clippy lint about suspicious `if/else` formatting.
276-
// Therefore, suppress the lint inside the generated code...
277-
#[allow(clippy::suspicious_else_formatting)]
278-
{
318+
),
319+
(None, Some(ret_event)) => quote_spanned!(block.span()=>
279320
#span
280-
// ...but turn the lint back on inside the function body.
281-
#[warn(clippy::suspicious_else_formatting)]
282-
#block
283-
}
284-
)
321+
#[allow(clippy::redundant_closure_call)]
322+
let x = (move || #block)();
323+
#ret_event;
324+
x
325+
),
326+
(None, None) => quote_spanned!(block.span() =>
327+
// Because `quote` produces a stream of tokens _without_ whitespace, the
328+
// `if` and the block will appear directly next to each other. This
329+
// generates a clippy lint about suspicious `if/else` formatting.
330+
// Therefore, suppress the lint inside the generated code...
331+
#[allow(clippy::suspicious_else_formatting)]
332+
{
333+
#span
334+
// ...but turn the lint back on inside the function body.
335+
#[warn(clippy::suspicious_else_formatting)]
336+
#block
337+
}
338+
),
339+
}
285340
}
286341

287342
/// Indicates whether a field should be recorded as `Value` or `Debug`.

tracing-attributes/src/lib.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,35 @@ mod expand;
368368
/// }
369369
/// ```
370370
///
371+
/// Adding the `ret` argument to `#[instrument]` will emit an event with the function's
372+
/// return value when the function returns:
373+
///
374+
/// ```
375+
/// # use tracing_attributes::instrument;
376+
/// #[instrument(ret)]
377+
/// fn my_function() -> i32 {
378+
/// 42
379+
/// }
380+
/// ```
381+
/// The return value event will have the same level as the span generated by `#[instrument]`.
382+
/// By default, this will be `TRACE`, but if the level is overridden, the event will be at the same
383+
/// level.
384+
///
385+
/// **Note**: if the function returns a `Result<T, E>`, `ret` will record returned values if and
386+
/// only if the function returns [`Result::Ok`].
387+
///
388+
/// By default, returned values will be recorded using their [`std::fmt::Debug`] implementations.
389+
/// If a returned value implements [`std::fmt::Display`], it can be recorded using its `Display`
390+
/// implementation instead, by writing `ret(Display)`:
391+
///
392+
/// ```
393+
/// # use tracing_attributes::instrument;
394+
/// #[instrument(ret(Display))]
395+
/// fn my_function() -> i32 {
396+
/// 42
397+
/// }
398+
/// ```
399+
///
371400
/// If the function returns a `Result<T, E>` and `E` implements `std::fmt::Display`, you can add
372401
/// `err` or `err(Display)` to emit error events when the function returns `Err`:
373402
///
@@ -391,6 +420,17 @@ mod expand;
391420
/// }
392421
/// ```
393422
///
423+
/// The `ret` and `err` arguments can be combined in order to record an event if a
424+
/// function returns [`Result::Ok`] or [`Result::Err`]:
425+
///
426+
/// ```
427+
/// # use tracing_attributes::instrument;
428+
/// #[instrument(err, ret)]
429+
/// fn my_function(arg: usize) -> Result<(), std::io::Error> {
430+
/// Ok(())
431+
/// }
432+
/// ```
433+
///
394434
/// `async fn`s may also be instrumented:
395435
///
396436
/// ```

0 commit comments

Comments
 (0)