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}