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}