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

[tracing-attributes] Support for using #[instrument] with async-trait #711

Merged
merged 4 commits into from
May 13, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,6 @@ async fn write(stream: &mut TcpStream) -> io::Result<usize> {
Under the hood, the `#[instrument]` macro performs same the explicit span
attachment that `Future::instrument` does.

Note: the [`#[tracing::instrument]`](https://github.com/tokio-rs/tracing/issues/399)` macro does not work correctly with the [async-trait](https://github.com/dtolnay/async-trait) crate. This bug is tracked in [#399](https://github.com/tokio-rs/tracing/issues/399).

## Getting Help

First, see if the answer to your question can be found in the API documentation.
Expand Down
3 changes: 3 additions & 0 deletions tracing-attributes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ async-await = []
[dependencies]
syn = { version = "1", features = ["full", "extra-traits"] }
quote = "1"
proc-macro2 = "1"


[dev-dependencies]
tracing = { path = "../tracing", version = "0.1" }
tracing-futures = { path = "../tracing-futures", version = "0.2" }
tokio-test = { version = "0.2.0" }
tracing-core = { path = "../tracing-core", version = "0.1"}
# I'm not a huge fan of adding such a dependency only for tests, any idea ?
Copy link
Member

Choose a reason for hiding this comment

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

/md I think taking a dependency is fine. This PR is meant to address a poor interaction between the two crates, so it makes sense to depend on the crate that previously caused a poor interaction.

async-trait = "0.1"
Copy link
Member

Choose a reason for hiding this comment

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

I believe dev dependencies will only be downloaded when running tests, so I don't think this is a huge deal. If it is an issue, we can also move the tests for compatibility with async-trait to a separate crate that is used only for testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, "Dev-dependencies are not used when compiling a package for building, but are used for compiling tests, examples, and benchmarks.

These dependencies are not propagated to other packages which depend on this package."

So it's finally not that big of an issue, as you said. Alas, I never took the time to "read" (instead of looking at it when in need) the cargo book and it looks like I'm still learning the basics of cargo sometimes ^^

Copy link
Member

Choose a reason for hiding this comment

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

Great, let's just add the dev dependency, then. Since this isn't an issue, we can remove the comment.


[badges]
maintenance = { status = "experimental" }
209 changes: 185 additions & 24 deletions tracing-attributes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ use std::iter;
use proc_macro::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::{
spanned::Spanned, AttributeArgs, FieldPat, FnArg, Ident, ItemFn, Lit, LitInt, Meta, MetaList,
MetaNameValue, NestedMeta, Pat, PatIdent, PatReference, PatStruct, PatTuple, PatTupleStruct,
PatType, Path, Signature,
spanned::Spanned, AttributeArgs, Block, Expr, ExprCall, FieldPat, FnArg, Ident, Item, ItemFn,
Lit, LitInt, Meta, MetaList, MetaNameValue, NestedMeta, Pat, PatIdent, PatReference, PatStruct,
PatTuple, PatTupleStruct, PatType, Path, Signature, Stmt,
};

/// Instruments a function to create and enter a `tracing` [span] every time
Expand Down Expand Up @@ -168,6 +168,28 @@ use syn::{
/// # Ok(())
/// }
/// ```
///
/// It also works with async-trait (and hopefully most of the libraries that does similar things):
Copy link
Member

Choose a reason for hiding this comment

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

Can we add a link to the async-trait crate here?

///
/// ```
/// # use tracing::instrument;
/// use async_trait::async_trait;
///
/// #[async_trait]
/// pub trait Foo {
/// async fn foo(&self, v: usize) -> ();
/// }
///
/// #[derive(Debug)]
/// struct FooImpl;
///
/// #[async_trait]
/// impl Foo for FooImpl {
/// #[instrument(skip(self))]
/// async fn foo(&self, v: usize) {}
/// }
/// ```

///
/// [span]: https://docs.rs/tracing/latest/tracing/span/index.html
/// [`tracing`]: https://github.com/tokio-rs/tracing
Expand All @@ -177,6 +199,41 @@ pub fn instrument(args: TokenStream, item: TokenStream) -> TokenStream {
let input: ItemFn = syn::parse_macro_input!(item as ItemFn);
let args = syn::parse_macro_input!(args as AttributeArgs);

// check for async_trait-like patterns in the block and wrap the internal function with Instrument
// instead of wrapping the async_trait generated wrapper
if let Some(internal_fun_name) = is_async_trait(&input.block, input.sig.asyncness.is_some()) {
// let's rewrite some statements !
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
// let's rewrite some statements !
// let's rewrite some statements!

let mut stmts: Vec<Stmt> = input.block.stmts.to_vec();
for stmt in &mut stmts {
if let Stmt::Item(Item::Fn(fun)) = stmt {
// instrument the function if considered as the function we truly want to trace
if fun.sig.ident == internal_fun_name {
*stmt = syn::parse2(gen_body(fun, args, Some(input.sig.ident.to_string())))
.unwrap();
break;
}
}
}

let sig = &input.sig;
let attrs = &input.attrs;
quote!(
#(#attrs) *
#sig {
#(#stmts) *
}
)
.into()
} else {
gen_body(&input, args, None).into()
}
}

fn gen_body(
input: &ItemFn,
args: AttributeArgs,
fun_name: Option<String>,
) -> proc_macro2::TokenStream {
// these are needed ahead of time, as ItemFn contains the function body _and_
// isn't representable inside a quote!/quote_spanned! macro
// (Syn's ToTokens isn't implemented for ItemFn)
Expand Down Expand Up @@ -206,7 +263,11 @@ pub fn instrument(args: TokenStream, item: TokenStream) -> TokenStream {
} = sig;

// function name
let ident_str = ident.to_string();
let ident_str = if let Some(x) = &fun_name {
x.clone()
} else {
ident.to_string()
};

// generate this inside a closure, so we can return early on errors.
let span = (|| {
Expand All @@ -216,42 +277,63 @@ pub fn instrument(args: TokenStream, item: TokenStream) -> TokenStream {
Err(err) => return quote!(#err),
};

let param_names: Vec<Ident> = params
let param_names: Vec<(Ident, Ident)> = params
.clone()
.into_iter()
.flat_map(|param| match param {
FnArg::Typed(PatType { pat, .. }) => param_names(*pat),
FnArg::Receiver(_) => Box::new(iter::once(Ident::new("self", param.span()))),
})
// if we are inside a function generated by async-trait, we should take care to rewrite
// "_self" as "self" for 'user convenience'
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 the presence of a _self identifier is another likely marker that async-trait is in use...should that be part of our heuristic for detecting async-trait as well?

Copy link
Contributor Author

@nightmared nightmared May 13, 2020

Choose a reason for hiding this comment

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

You're right that we could do that to assert with more certainty that we are facing a aync_trait-like situation (aka: it is more probable that async_trait is in use if you see the the both pattern fn f(...) Box::pin(f(...)) and _self) , but this is not necessarily true will all traits, so its absence cannot rule out the use of async_trait. Consider this for example:

#[async_trait]
pub trait Foo {
    async fn foo(v: usize) -> ();
}

This foo method on this trait doesn't take self, so async_trait won't give us a _self parameter for that function either.

In the end, if we want to use this variable renaming to be more precise, maybe we can consider that functions with the fn f(...) Box::pin(f(...)) pattern but with self as an argument are not cases of async_trait, as self should have been rewritten to ``self. But I wonder if there are other crates that exists and that does similar things without renaming self` to `_self` or with a different name, and that we don't want to exclude simply because they don't apply the exact same mechanisms as async_trait.

Copy link
Member

Choose a reason for hiding this comment

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

That's fair, I think the pattern we're detecting is much more reliable.

.map(|x| {
if fun_name.is_some() && x == "_self" {
(Ident::new("self", x.span()), x)
} else {
(x.clone(), x)
}
})
.collect();

// TODO: allow the user to rename fields at will (all the machinery should be here)
// Little dance with new (user-exposed) names and old (internal) names of identifiers.
// That way, you can do the following even though async_trait rewrite "self" as "_self":
// ```
// #[async_trait]
// impl Foo for FooImpl {
// #[instrument(skip(self))]
// async fn foo(&self, v: usize) {}
// }
// ```

for skip in &skips {
if !param_names.contains(skip) {
if !param_names.iter().map(|(user, _)| user).any(|y| y == skip) {
return quote_spanned! {skip.span()=>
compile_error!("attempting to skip non-existent parameter")
};
}
}

let param_names: Vec<Ident> = param_names
// filter out skipped fields
let param_names: Vec<(Ident, Ident)> = param_names
.into_iter()
.filter(|ident| !skips.contains(ident))
.filter(|(ident, _)| !skips.contains(ident))
.collect();

let fields = match fields(&args, &param_names) {
let new_param_names: Vec<&Ident> = param_names.iter().map(|x| &x.0).collect();

let fields = match fields(&args, &new_param_names) {
Ok(fields) => fields,
Err(err) => return quote!(#err),
};

let param_names_clone = param_names.clone();

let level = level(&args);
let target = target(&args);
let span_name = name(&args, ident_str);

let mut quoted_fields: Vec<_> = param_names
.into_iter()
.map(|i| quote!(#i = tracing::field::debug(&#i)))
.iter()
.map(|(user_name, real_name)| quote!(#user_name = tracing::field::debug(&#real_name)))
.collect();
quoted_fields.extend(fields.into_iter().map(|(key, value)| {
let value = match value {
Expand All @@ -276,7 +358,8 @@ pub fn instrument(args: TokenStream, item: TokenStream) -> TokenStream {
// If `err` is in args, instrument any resulting `Err`s.
let body = if asyncness.is_some() {
if instrument_err(&args) {
quote_spanned! {block.span()=>
quote_spanned!(block.span()=>
let __tracing_attr_span = #span;
tracing_futures::Instrument::instrument(async move {
match async move { #block }.await {
Ok(x) => Ok(x),
Expand All @@ -286,18 +369,20 @@ pub fn instrument(args: TokenStream, item: TokenStream) -> TokenStream {
}
}
}, __tracing_attr_span).await
}
)
} else {
quote_spanned! {block.span()=>
tracing_futures::Instrument::instrument(
async move { #block },
__tracing_attr_span
)
quote_spanned!(block.span()=>
let __tracing_attr_span = #span;
tracing_futures::Instrument::instrument(
async move { #block },
__tracing_attr_span
)
.await
}
)
}
} else if instrument_err(&args) {
quote_spanned!(block.span()=>
let __tracing_attr_span = #span;
let __tracing_attr_guard = __tracing_attr_span.enter();
match { #block } {
Ok(x) => Ok(x),
Expand All @@ -309,6 +394,7 @@ pub fn instrument(args: TokenStream, item: TokenStream) -> TokenStream {
)
} else {
quote_spanned!(block.span()=>
let __tracing_attr_span = #span;
let __tracing_attr_guard = __tracing_attr_span.enter();
#block
)
Expand All @@ -319,11 +405,9 @@ pub fn instrument(args: TokenStream, item: TokenStream) -> TokenStream {
#vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type
#where_clause
{
let __tracing_attr_span = #span;
#body
}
)
.into()
}

fn param_names(pat: Pat) -> Box<dyn Iterator<Item = Ident>> {
Expand Down Expand Up @@ -462,7 +546,7 @@ fn target(args: &[NestedMeta]) -> impl ToTokens {

fn fields(
args: &[NestedMeta],
param_names: &[Ident],
param_names: &[&Ident],
) -> Result<(Vec<(Ident, Option<Lit>)>), impl ToTokens> {
let mut fields = args.iter().filter_map(|arg| match arg {
NestedMeta::Meta(Meta::List(MetaList {
Expand Down Expand Up @@ -579,3 +663,80 @@ fn instrument_err(args: &[NestedMeta]) -> bool {
_ => false,
})
}

// terrible machinery to make async_trait-like cases works
hawkw marked this conversation as resolved.
Show resolved Hide resolved
// (following the approach suggested in
// https://github.com/dtolnay/async-trait/issues/45#issuecomment-571245673)
fn is_async_trait(block: &Block, block_is_async: bool) -> Option<String> {
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: In general, I try to reserve is_ for functions that return a boolean. This is returning the name of the async-trait-generated inner function, if it exists...

// are we in an async context ? If yes, this isn't a async_trait-like pattern
if block_is_async {
return None;
}

// list of async functions declared inside the block
let mut inside_funs = Vec::new();
// last expression declared in the block (it determines the return value of the block, so
// that if we are working on a function whose `trait` or `impl` declaration is annotated by
// async_trait, this is quite likely the point where the future is pinned)
let mut last_expr = None;

// obtain the list of direct internal functions and the last expression of the block
for stmt in &block.stmts {
if let Stmt::Item(Item::Fn(fun)) = &stmt {
// is the function declared as async ? If so, this is a good candidate, let's keep it
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
// is the function declared as async ? If so, this is a good candidate, let's keep it
// is the function declared as async? If so, this is a good candidate, let's keep it

// in hand
if fun.sig.asyncness.is_some() {
inside_funs.push(fun.sig.ident.to_string());
}
} else if let Stmt::Expr(e) = &stmt {
last_expr = Some(e);
}
}

// let's play with (too much) pattern matching
// is the last expression a function call ?
if let Some(Expr::Call(ExprCall {
func: outside_func,
args: outside_args,
..
})) = last_expr
{
if let Expr::Path(path) = outside_func.as_ref() {
// is it a call to `Box::pin()` ?
Copy link
Member

Choose a reason for hiding this comment

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

nit

Suggested change
// is it a call to `Box::pin()` ?
// is it a call to `Box::pin()`?

if "Box::pin"
== path
.path
.segments
.iter()
.map(|x| x.ident.to_string())
.collect::<Vec<String>>()
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 could avoid a couple of allocations if we did this by creating a string and used push_str to append the string slices from the path.segments iterator, rather than creating a String from each string slice and collecting them into a Vec. I don't think this is hot enough that this is terribly important though, so it's fine as-is.

.join("::")
Copy link
Member

Choose a reason for hiding this comment

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

nit: can we maybe move this long expression to a let-binding outside the if condition? this is a little hard to follow IMO.

{
// does it takes at least an argument ? (if it doesn't, it's not gonna compile anyway,
Copy link
Member

Choose a reason for hiding this comment

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

nnit

Suggested change
// does it takes at least an argument ? (if it doesn't, it's not gonna compile anyway,
// does it takes at least an argument? (if it doesn't, it's not gonna compile anyway,

// but that's no reason to (try to) perform an out of bounds access)
if outside_args.is_empty() {
return None;
}
// is the argument to Box::pin a function call itself ?
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
// is the argument to Box::pin a function call itself ?
// is the argument to Box::pin a function call itself?

if let Expr::Call(ExprCall { func, args, .. }) = &outside_args[0] {
if let Expr::Path(inside_path) = func.as_ref() {
// "stringify" the path of the function called
let func_name = inside_path
.path
.segments
.iter()
.map(|x| x.ident.to_string())
.collect::<Vec<String>>()
.join("::");
// is this function directly defined insided the current block ?
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
// is this function directly defined insided the current block ?
// is this function directly defined insided the current block?

if inside_funs.contains(&func_name) {
// we must hook this function now
return Some(func_name);
}
}
}
}
}
}
None
}
Loading