minitrace_macro/
lib.rs

1// Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0.
2
3//! An attribute macro designed to eliminate boilerplate code for [`minitrace`](https://crates.io/crates/minitrace).
4
5#![recursion_limit = "256"]
6// Instrumenting the async fn is not as straight forward as expected because `async_trait` rewrites
7// `async fn` into a normal fn which returns `Box<impl Future>`, and this stops the macro from
8// distinguishing `async fn` from `fn`. The following code reused the `async_trait` probes from [tokio-tracing](https://github.com/tokio-rs/tracing/blob/6a61897a5e834988ad9ac709e28c93c4dbf29116/tracing-attributes/src/expand.rs).
9
10extern crate proc_macro;
11
12#[macro_use]
13extern crate proc_macro_error;
14
15use std::collections::HashMap;
16
17use proc_macro2::Span;
18use quote::quote_spanned;
19use syn::parse::Parse;
20use syn::parse::ParseStream;
21use syn::punctuated::Punctuated;
22use syn::spanned::Spanned;
23use syn::*;
24
25struct Args {
26    name: Option<String>,
27    short_name: bool,
28    enter_on_poll: bool,
29    properties: Vec<(String, String)>,
30}
31
32struct Property {
33    key: String,
34    value: String,
35}
36
37impl Parse for Property {
38    fn parse(input: ParseStream) -> Result<Self> {
39        let key: LitStr = input.parse()?;
40        input.parse::<Token![:]>()?;
41        let value: LitStr = input.parse()?;
42        Ok(Property {
43            key: key.value(),
44            value: value.value(),
45        })
46    }
47}
48
49impl Parse for Args {
50    fn parse(input: ParseStream) -> Result<Self> {
51        let mut name = None;
52        let mut short_name = false;
53        let mut enter_on_poll = false;
54        let mut properties = Vec::new();
55        let mut seen = HashMap::new();
56
57        while !input.is_empty() {
58            let ident: Ident = input.parse()?;
59            if seen.contains_key(&ident.to_string()) {
60                return Err(syn::Error::new(ident.span(), "duplicate argument"));
61            }
62            seen.insert(ident.to_string(), ());
63            input.parse::<Token![=]>()?;
64            match ident.to_string().as_str() {
65                "name" => {
66                    let parsed_name: LitStr = input.parse()?;
67                    name = Some(parsed_name.value());
68                }
69                "short_name" => {
70                    let parsed_short_name: LitBool = input.parse()?;
71                    short_name = parsed_short_name.value;
72                }
73                "enter_on_poll" => {
74                    let parsed_enter_on_poll: LitBool = input.parse()?;
75                    enter_on_poll = parsed_enter_on_poll.value;
76                }
77                "properties" => {
78                    let content;
79                    let _brace_token = syn::braced!(content in input);
80                    let property_list: Punctuated<Property, Token![,]> =
81                        content.parse_terminated(Property::parse)?;
82                    for property in property_list {
83                        if properties.iter().any(|(k, _)| k == &property.key) {
84                            return Err(syn::Error::new(
85                                Span::call_site(),
86                                "duplicate property key",
87                            ));
88                        }
89                        properties.push((property.key, property.value));
90                    }
91                }
92                _ => return Err(syn::Error::new(Span::call_site(), "unexpected identifier")),
93            }
94            if !input.is_empty() {
95                let _ = input.parse::<Token![,]>();
96            }
97        }
98
99        Ok(Args {
100            name,
101            short_name,
102            enter_on_poll,
103            properties,
104        })
105    }
106}
107
108/// An attribute macro designed to eliminate boilerplate code.
109///
110/// This macro automatically creates a span for the annotated function. The span name defaults to
111/// the function name but can be customized by passing a string literal as an argument using the
112/// `name` parameter.
113///
114/// The `#[trace]` attribute requires a local parent context to function correctly. Ensure that
115/// the function annotated with `#[trace]` is called within __a local context of a `Span`__, which
116/// is established by invoking the `Span::set_local_parent()` method.
117///
118/// ## Arguments
119///
120/// * `name` - The name of the span. Defaults to the full path of the function.
121/// * `short_name` - Whether to use the function name without path as the span name. Defaults to
122///   `false`.
123/// * `enter_on_poll` - Whether to enter the span on poll. If set to `false`, `in_span` will be
124///   used. Only available for `async fn`. Defaults to `false`.
125/// * `properties` - A list of key-value pairs to be added as properties to the span. The value can
126///   be a format string, where the function arguments are accessible. Defaults to `{}`.
127///
128/// # Examples
129///
130/// ```
131/// use minitrace::prelude::*;
132///
133/// #[trace]
134/// fn simple() {
135///     // ...
136/// }
137///
138/// #[trace(short_name = true)]
139/// async fn simple_async() {
140///     // ...
141/// }
142///
143/// #[trace(name = "qux", enter_on_poll = true)]
144/// async fn baz() {
145///     // ...
146/// }
147///
148/// #[trace(properties = { "k1": "v1", "a": "argument `a` is {a:?}" })]
149/// async fn properties(a: u64) {
150///     // ...
151/// }
152/// ```
153///
154/// The code snippets above will be expanded to:
155///
156/// ```
157/// # use minitrace::prelude::*;
158/// # use minitrace::local::LocalSpan;
159/// fn simple() {
160///     let __guard__ = LocalSpan::enter_with_local_parent("example::simple");
161///     // ...
162/// }
163///
164/// async fn simple_async() {
165///     let __span__ = Span::enter_with_local_parent("simple_async");
166///     async {
167///         // ...
168///     }
169///     .in_span(__span__)
170///     .await
171/// }
172///
173/// async fn baz() {
174///     async {
175///         // ...
176///     }
177///     .enter_on_poll("qux")
178///     .await
179/// }
180///
181/// async fn properties(a: u64) {
182///     let __span__ = Span::enter_with_local_parent("example::properties").with_properties(|| {
183///         [
184///             (std::borrow::Cow::from("k1"), std::borrow::Cow::from("v1")),
185///             (
186///                 std::borrow::Cow::from("a"),
187///                 std::borrow::Cow::from(format!("argument `a` is {a:?}")),
188///             ),
189///         ]
190///     });
191///     async {
192///         // ...
193///     }
194///     .in_span(__span__)
195///     .await
196/// }
197/// ```
198#[proc_macro_attribute]
199#[proc_macro_error]
200pub fn trace(
201    args: proc_macro::TokenStream,
202    item: proc_macro::TokenStream,
203) -> proc_macro::TokenStream {
204    let args = parse_macro_input!(args as Args);
205    let input = syn::parse_macro_input!(item as ItemFn);
206
207    let func_name = input.sig.ident.to_string();
208    // check for async_trait-like patterns in the block, and instrument
209    // the future instead of the wrapper
210    let func_body = if let Some(internal_fun) =
211        get_async_trait_info(&input.block, input.sig.asyncness.is_some())
212    {
213        // let's rewrite some statements!
214        match internal_fun.kind {
215            // async-trait <= 0.1.43
216            AsyncTraitKind::Function => {
217                unimplemented!(
218                    "Please upgrade the crate `async-trait` to a version higher than 0.1.44"
219                )
220            }
221            // async-trait >= 0.1.44
222            AsyncTraitKind::Async(async_expr) => {
223                // fallback if we couldn't find the '__async_trait' binding, might be
224                // useful for crates exhibiting the same behaviors as async-trait
225                let instrumented_block =
226                    gen_block(&func_name, &async_expr.block, true, false, &args);
227                let async_attrs = &async_expr.attrs;
228                quote::quote! {
229                    Box::pin(#(#async_attrs) * #instrumented_block)
230                }
231            }
232        }
233    } else {
234        gen_block(
235            &func_name,
236            &input.block,
237            input.sig.asyncness.is_some(),
238            input.sig.asyncness.is_some(),
239            &args,
240        )
241    };
242
243    let ItemFn {
244        attrs, vis, sig, ..
245    } = input;
246
247    let Signature {
248        output: return_type,
249        inputs: params,
250        unsafety,
251        constness,
252        abi,
253        ident,
254        asyncness,
255        generics:
256            Generics {
257                params: gen_params,
258                where_clause,
259                ..
260            },
261        ..
262    } = sig;
263
264    quote::quote!(
265        #(#attrs) *
266        #vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type
267        #where_clause
268        {
269            #func_body
270        }
271    )
272    .into()
273}
274
275fn gen_name(span: proc_macro2::Span, func_name: &str, args: &Args) -> proc_macro2::TokenStream {
276    match &args.name {
277        Some(name) if name.is_empty() => {
278            abort_call_site!("`name` can not be empty")
279        }
280        Some(_) if args.short_name => {
281            abort_call_site!("`name` and `short_name` can not be used together")
282        }
283        Some(name) => {
284            quote_spanned!(span=>
285                #name
286            )
287        }
288        None if args.short_name => {
289            quote_spanned!(span=>
290                #func_name
291            )
292        }
293        None => {
294            quote_spanned!(span=>
295                minitrace::full_name!()
296            )
297        }
298    }
299}
300
301fn gen_properties(span: proc_macro2::Span, args: &Args) -> proc_macro2::TokenStream {
302    if args.properties.is_empty() {
303        return quote::quote!();
304    }
305
306    if args.enter_on_poll {
307        abort_call_site!("`enter_on_poll` can not be used with `properties`")
308    }
309
310    let properties = args.properties.iter().map(|(k, v)| {
311        let k = k.as_str();
312        let v = v.as_str();
313
314        let (v, need_format) = unescape_format_string(v);
315
316        if need_format {
317            quote_spanned!(span=>
318                (std::borrow::Cow::from(#k), std::borrow::Cow::from(format!(#v)))
319            )
320        } else {
321            quote_spanned!(span=>
322                (std::borrow::Cow::from(#k), std::borrow::Cow::from(#v))
323            )
324        }
325    });
326    let properties = Punctuated::<_, Token![,]>::from_iter(properties);
327    quote_spanned!(span=>
328        .with_properties(|| [ #properties ])
329    )
330}
331
332fn unescape_format_string(s: &str) -> (String, bool) {
333    let unescaped_delete = s.replace("{{", "").replace("}}", "");
334    let contains_valid_format_string =
335        unescaped_delete.contains('{') || unescaped_delete.contains('}');
336    if contains_valid_format_string {
337        (s.to_string(), true)
338    } else {
339        let unescaped_replace = s.replace("{{", "{").replace("}}", "}");
340        (unescaped_replace, false)
341    }
342}
343
344/// Instrument a block
345fn gen_block(
346    func_name: &str,
347    block: &Block,
348    async_context: bool,
349    async_keyword: bool,
350    args: &Args,
351) -> proc_macro2::TokenStream {
352    let name = gen_name(block.span(), func_name, args);
353    let properties = gen_properties(block.span(), args);
354
355    // Generate the instrumented function body.
356    // If the function is an `async fn`, this will wrap it in an async block.
357    // Otherwise, this will enter the span and then perform the rest of the body.
358    if async_context {
359        let block = if args.enter_on_poll {
360            quote_spanned!(block.span()=>
361                minitrace::future::FutureExt::enter_on_poll(
362                    async move { #block },
363                    #name
364                )
365            )
366        } else {
367            quote_spanned!(block.span()=>
368                {
369                    let __span__ = minitrace::Span::enter_with_local_parent( #name ) #properties;
370                    minitrace::future::FutureExt::in_span(
371                        async move { #block },
372                        __span__,
373                    )
374                }
375            )
376        };
377
378        if async_keyword {
379            quote_spanned!(block.span()=>
380                #block.await
381            )
382        } else {
383            block
384        }
385    } else {
386        if args.enter_on_poll {
387            abort_call_site!("`enter_on_poll` can not be applied on non-async function");
388        }
389
390        quote_spanned!(block.span()=>
391            let __guard__ = minitrace::local::LocalSpan::enter_with_local_parent( #name ) #properties;
392            #block
393        )
394    }
395}
396
397enum AsyncTraitKind<'a> {
398    // old construction. Contains the function
399    Function,
400    // new construction. Contains a reference to the async block
401    Async(&'a ExprAsync),
402}
403
404struct AsyncTraitInfo<'a> {
405    // statement that must be patched
406    _source_stmt: &'a Stmt,
407    kind: AsyncTraitKind<'a>,
408}
409
410// Get the AST of the inner function we need to hook, if it was generated
411// by async-trait.
412// When we are given a function annotated by async-trait, that function
413// is only a placeholder that returns a pinned future containing the
414// user logic, and it is that pinned future that needs to be instrumented.
415// Were we to instrument its parent, we would only collect information
416// regarding the allocation of that future, and not its own span of execution.
417// Depending on the version of async-trait, we inspect the block of the function
418// to find if it matches the pattern
419// `async fn foo<...>(...) {...}; Box::pin(foo<...>(...))` (<=0.1.43), or if
420// it matches `Box::pin(async move { ... }) (>=0.1.44). We the return the
421// statement that must be instrumented, along with some other information.
422// 'gen_body' will then be able to use that information to instrument the
423// proper function/future.
424// (this follows the approach suggested in
425// https://github.com/dtolnay/async-trait/issues/45#issuecomment-571245673)
426fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option<AsyncTraitInfo<'_>> {
427    // are we in an async context? If yes, this isn't a async_trait-like pattern
428    if block_is_async {
429        return None;
430    }
431
432    // list of async functions declared inside the block
433    let inside_funs = block.stmts.iter().filter_map(|stmt| {
434        if let Stmt::Item(Item::Fn(fun)) = &stmt {
435            // If the function is async, this is a candidate
436            if fun.sig.asyncness.is_some() {
437                return Some((stmt, fun));
438            }
439        }
440        None
441    });
442
443    // last expression of the block (it determines the return value
444    // of the block, so that if we are working on a function whose
445    // `trait` or `impl` declaration is annotated by async_trait,
446    // this is quite likely the point where the future is pinned)
447    let (last_expr_stmt, last_expr) = block.stmts.iter().rev().find_map(|stmt| {
448        if let Stmt::Expr(expr) = stmt {
449            Some((stmt, expr))
450        } else {
451            None
452        }
453    })?;
454
455    // is the last expression a function call?
456    let (outside_func, outside_args) = match last_expr {
457        Expr::Call(ExprCall { func, args, .. }) => (func, args),
458        _ => return None,
459    };
460
461    // is it a call to `Box::pin()`?
462    let path = match outside_func.as_ref() {
463        Expr::Path(path) => &path.path,
464        _ => return None,
465    };
466    if !path_to_string(path).ends_with("Box::pin") {
467        return None;
468    }
469
470    // Does the call take an argument? If it doesn't,
471    // it's not gonna compile anyway, but that's no reason
472    // to (try to) perform an out of bounds access
473    if outside_args.is_empty() {
474        return None;
475    }
476
477    // Is the argument to Box::pin an async block that
478    // captures its arguments?
479    if let Expr::Async(async_expr) = &outside_args[0] {
480        // check that the move 'keyword' is present
481        async_expr.capture?;
482
483        return Some(AsyncTraitInfo {
484            _source_stmt: last_expr_stmt,
485            kind: AsyncTraitKind::Async(async_expr),
486        });
487    }
488
489    // Is the argument to Box::pin a function call itself?
490    let func = match &outside_args[0] {
491        Expr::Call(ExprCall { func, .. }) => func,
492        _ => return None,
493    };
494
495    // "stringify" the path of the function called
496    let func_name = match **func {
497        Expr::Path(ref func_path) => path_to_string(&func_path.path),
498        _ => return None,
499    };
500
501    // Was that function defined inside of the current block?
502    // If so, retrieve the statement where it was declared and the function itself
503    let (stmt_func_declaration, _) = inside_funs
504        .into_iter()
505        .find(|(_, fun)| fun.sig.ident == func_name)?;
506
507    Some(AsyncTraitInfo {
508        _source_stmt: stmt_func_declaration,
509        kind: AsyncTraitKind::Function,
510    })
511}
512
513// Return a path as a String
514fn path_to_string(path: &Path) -> String {
515    use std::fmt::Write;
516    // some heuristic to prevent too many allocations
517    let mut res = String::with_capacity(path.segments.len() * 5);
518    for i in 0..path.segments.len() {
519        write!(res, "{}", path.segments[i].ident).expect("writing to a String should never fail");
520        if i < path.segments.len() - 1 {
521            res.push_str("::");
522        }
523    }
524    res
525}