Skip to main content

await_tree_attributes/
lib.rs

1// Copyright 2025 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Procedural attributes for await-tree instrumentation.
16
17use proc_macro::TokenStream;
18use quote::quote;
19use syn::{parse_macro_input, Ident, ItemFn, Token};
20
21/// Parse the attribute arguments to extract method calls and format args
22#[derive(Default)]
23struct InstrumentArgs {
24    method_calls: Vec<Ident>,
25    format_args: Option<proc_macro2::TokenStream>,
26    boxed: bool,
27}
28
29impl syn::parse::Parse for InstrumentArgs {
30    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
31        let mut method_calls = Vec::new();
32        let mut format_args = None;
33        let mut boxed = false;
34
35        // Parse identifiers first (these will become method calls or special keywords)
36        while input.peek(Ident) {
37            // Look ahead to see if this looks like a method call identifier
38            let fork = input.fork();
39            let ident: Ident = fork.parse()?;
40
41            // Check if the next token after the identifier is a comma or end
42            // If it's something else (like a parenthesis or string), treat as format args
43            if fork.peek(Token![,]) || fork.is_empty() {
44                // This is a method call identifier or special keyword
45                input.parse::<Ident>()?; // consume the identifier
46
47                // Check for special "boxed" keyword
48                if ident == "boxed" {
49                    boxed = true;
50                } else {
51                    method_calls.push(ident);
52                }
53
54                if input.peek(Token![,]) {
55                    input.parse::<Token![,]>()?;
56                }
57            } else {
58                // This looks like the start of format arguments
59                break;
60            }
61        }
62
63        // Parse remaining tokens as format arguments
64        if !input.is_empty() {
65            let remaining: proc_macro2::TokenStream = input.parse()?;
66            format_args = Some(remaining);
67        }
68
69        Ok(InstrumentArgs {
70            method_calls,
71            format_args,
72            boxed,
73        })
74    }
75}
76
77/// Instruments an async function with await-tree spans.
78///
79/// This attribute macro transforms an async function to automatically create
80/// an await-tree span and instrument the function's execution.
81///
82/// # Usage
83///
84/// ```rust,ignore
85/// #[await_tree::instrument("span_name({})", arg1)]
86/// async fn foo(arg1: i32, arg2: String) {
87///     // function body
88/// }
89/// ```
90///
91/// With attributes on the span:
92///
93/// ```rust,ignore
94/// #[await_tree::instrument(long_running, verbose, "span_name({})", arg1)]
95/// async fn foo(arg1: i32, arg2: String) {
96///     // function body
97/// }
98/// ```
99///
100/// With the `boxed` keyword to `Box::pin` the function body before calling `instrument_await`,
101/// which can help reducing the stack usage if you encounter stack overflow:
102///
103/// ```rust,ignore
104/// #[await_tree::instrument(boxed, "span_name({})", arg1)]
105/// async fn foo(arg1: i32, arg2: String) {
106///     // function body
107/// }
108/// ```
109///
110/// The above will be expanded to:
111///
112/// ```rust,ignore
113/// async fn foo(arg1: i32, arg2: String) {
114///     let span = await_tree::span!("span_name({})", arg1).long_running().verbose();
115///     let fut = async move {
116///         // original function body
117///     };
118///     let fut = Box::pin(fut); // if `boxed` is specified
119///     fut.instrument_await(span).await
120/// }
121/// ```
122///
123/// # Arguments
124///
125/// The macro accepts format arguments similar to `format!` or `println!`:
126/// - The first argument is the format string
127/// - Subsequent arguments are the values to be formatted
128///
129/// The format arguments are passed directly to the `await_tree::span!` macro
130/// without any parsing or modification.
131#[proc_macro_attribute]
132pub fn instrument(args: TokenStream, input: TokenStream) -> TokenStream {
133    let input_fn = parse_macro_input!(input as ItemFn);
134
135    // Validate that this is an async function
136    if input_fn.sig.asyncness.is_none() {
137        return syn::Error::new_spanned(
138            &input_fn.sig.fn_token,
139            "the `instrument` attribute can only be applied to async functions",
140        )
141        .to_compile_error()
142        .into();
143    }
144
145    // Parse the arguments
146    let parsed_args = if args.is_empty() {
147        InstrumentArgs::default()
148    } else {
149        match syn::parse::<InstrumentArgs>(args) {
150            Ok(args) => args,
151            Err(e) => return e.to_compile_error().into(),
152        }
153    };
154
155    // Extract the span format arguments
156    let span_args = if let Some(format_args) = parsed_args.format_args {
157        quote! { #format_args }
158    } else {
159        // If no format arguments provided, use the function name as span
160        let fn_name = &input_fn.sig.ident;
161        quote! { stringify!(#fn_name) }
162    };
163
164    // Build span creation with method calls
165    let mut span_creation = quote! { ::await_tree::span!(#span_args) };
166
167    // Chain all method calls
168    for method_name in parsed_args.method_calls {
169        span_creation = quote! { #span_creation.#method_name() };
170    }
171
172    // Extract function components
173    let fn_vis = &input_fn.vis;
174    let fn_sig = &input_fn.sig;
175    let fn_block = &input_fn.block;
176    let fn_attrs = &input_fn.attrs;
177
178    // Generate the instrumented function
179    let boxed =
180        (parsed_args.boxed).then(|| quote! { let __at_fut = ::std::boxed::Box::pin(__at_fut); });
181
182    let result = quote! {
183        #(#fn_attrs)*
184        #fn_vis #fn_sig {
185            use ::await_tree::SpanExt as _;
186            let __at_span: ::await_tree::Span = #span_creation;
187            let __at_fut = async move #fn_block;
188            #boxed
189            ::await_tree::InstrumentAwait::instrument_await(__at_fut, __at_span).await
190        }
191    };
192
193    result.into()
194}