Skip to content

Commit 6433f7a

Browse files
authored
support instrument attribute macros (#32)
1 parent 9ca4598 commit 6433f7a

File tree

11 files changed

+577
-1
lines changed

11 files changed

+577
-1
lines changed

.github/workflows/publish.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ jobs:
1717
- uses: actions/checkout@v3
1818

1919
- name: Publish to crates.io
20-
run: cargo publish
20+
run: |
21+
cargo publish -p await-tree-attributes
22+
cargo publish -p await-tree
2123
env:
2224
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## Unreleased
9+
10+
- Added `#[instrument("<fmt>", args)]` attribute macro for automatic instrumentation of async functions ([#16](https://github.com/risingwavelabs/await-tree/pull/32))
11+
812
## [0.3.0] - 2025-04-09
913

1014
### Added

Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
[workspace]
2+
members = [".", "await-tree-attributes"]
3+
14
[package]
25
name = "await-tree"
36
version = "0.3.1"
@@ -12,8 +15,10 @@ license = "Apache-2.0"
1215
[features]
1316
serde = ["dep:serde", "flexstr/serde"]
1417
tokio = ["dep:tokio"]
18+
attributes = ["dep:await-tree-attributes"]
1519

1620
[dependencies]
21+
await-tree-attributes = { path = "await-tree-attributes", version = "0.1", optional = true }
1722
coarsetime = "0.1"
1823
derive_builder = "0.20"
1924
easy-ext = "1"
@@ -59,3 +64,7 @@ required-features = ["tokio"]
5964
[[example]]
6065
name = "global"
6166
required-features = ["tokio"]
67+
68+
[[example]]
69+
name = "instrument"
70+
required-features = ["tokio", "attributes"]

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,22 @@ println!("{tree}");
7979
});
8080
```
8181

82+
- `attributes`: Enables the `#[instrument]` attribute macro for automatic instrumentation of async functions. This provides a convenient way to add await-tree spans to functions without manual instrumentation.
83+
84+
```rust
85+
// Enable the attributes feature in Cargo.toml
86+
// await-tree = { version = "<version>", features = ["attributes"] }
87+
88+
// Then you can use the instrument attribute
89+
use await_tree::instrument;
90+
91+
#[instrument("my_function({})", arg)]
92+
async fn my_function(arg: i32) {
93+
// Your async code here
94+
work().await;
95+
}
96+
```
97+
8298
## Compared to `async-backtrace`
8399

84100
[`tokio-rs/async-backtrace`](https://github.com/tokio-rs/async-backtrace) is a similar crate that also provides the ability to dump the execution tree of async tasks. Here are some differences between `await-tree` and `async-backtrace`:

await-tree-attributes/Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "await-tree-attributes"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "Procedural attributes for await-tree instrumentation"
6+
repository = "https://github.com/risingwavelabs/await-tree"
7+
keywords = ["async", "tokio", "backtrace", "actor", "attributes"]
8+
categories = ["development-tools::debugging"]
9+
license = "Apache-2.0"
10+
11+
[lib]
12+
proc-macro = true
13+
14+
[dependencies]
15+
proc-macro2 = "1.0"
16+
quote = "1.0"
17+
syn = { version = "2.0", features = ["full", "extra-traits"] }
18+
19+
[dev-dependencies]
20+
tokio = { version = "1", features = ["full"] }
21+
await-tree = { path = ".." }

await-tree-attributes/README.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# await-tree-attributes
2+
3+
Procedural attributes for the [`await-tree`](https://crates.io/crates/await-tree) crate.
4+
5+
## Overview
6+
7+
This crate provides the `#[instrument]` attribute macro that automatically instruments async functions with await-tree spans, similar to how `tracing::instrument` works but specifically designed for await-tree.
8+
9+
## Usage
10+
11+
Add this to your `Cargo.toml`:
12+
13+
```toml
14+
[dependencies]
15+
await-tree = { version = "0.3", features = ["attributes"] }
16+
```
17+
18+
Then use the `#[instrument]` attribute on your async functions:
19+
20+
```rust
21+
use await_tree::{instrument, InstrumentAwait};
22+
23+
#[instrument("fetch_data({})", id)]
24+
async fn fetch_data(id: u32) -> String {
25+
// Your async code here
26+
format!("data_{}", id)
27+
}
28+
29+
#[instrument(long_running, verbose, "complex_task({}, {})", name, value)]
30+
async fn complex_task(name: &str, value: i32) -> String {
31+
format!("{}: {}", name, value)
32+
}
33+
34+
#[instrument]
35+
async fn simple_function() -> String {
36+
"hello".to_string()
37+
}
38+
```
39+
40+
## Attribute Expansion
41+
42+
The `#[instrument]` macro transforms your async function by:
43+
44+
1. Creating an await-tree span with the provided format arguments
45+
2. Wrapping the original function body in an async block
46+
3. Instrumenting the async block with the span
47+
48+
For example:
49+
50+
```rust
51+
#[instrument("span_name({})", arg1)]
52+
async fn foo(arg1: i32, arg2: String) {
53+
// original function body
54+
}
55+
```
56+
57+
Expands to:
58+
59+
```rust
60+
async fn foo(arg1: i32, arg2: String) {
61+
let span = await_tree::span!("span_name({})", arg1);
62+
let fut = async move {
63+
// original function body
64+
};
65+
fut.instrument_await(span).await
66+
}
67+
```
68+
69+
## Features
70+
71+
- **Format arguments**: Pass format strings and arguments just like `format!()` or `println!()`
72+
- **No argument parsing**: Format arguments are passed directly to `await_tree::span!()` without modification
73+
- **Function name fallback**: If no arguments are provided, uses the function name as the span name
74+
- **Preserves function attributes**: All function attributes and visibility modifiers are preserved
75+
- **Method chaining**: Support for chaining any method calls on the span
76+
77+
### Method Chaining
78+
79+
You can chain method calls on the span by including identifiers before the format arguments:
80+
81+
```rust
82+
// Chain span methods
83+
#[instrument(long_running, "slow_task")]
84+
async fn slow_task() { /* ... */ }
85+
86+
// Chain multiple methods
87+
#[instrument(long_running, verbose, "complex_task({})", id)]
88+
async fn complex_task(id: u32) { /* ... */ }
89+
90+
// Method calls without format args
91+
#[instrument(long_running, verbose)]
92+
async fn keywords_only() { /* ... */ }
93+
94+
// Any method name works (will fail at compile time if method doesn't exist)
95+
#[instrument(custom_attribute, "task")]
96+
async fn custom_task() { /* ... */ }
97+
```
98+
99+
The identifiers are processed in order and result in method calls on the span:
100+
- `long_running``.long_running()`
101+
- `verbose``.verbose()`
102+
- `custom_attribute``.custom_attribute()`
103+
104+
If a method doesn't exist on the `Span` type, the code will fail to compile with a clear error message.
105+
106+
## Requirements
107+
108+
- The macro can only be applied to `async` functions
109+
- You must import `InstrumentAwait` trait to use the generated code
110+
- The `attributes` feature must be enabled in the `await-tree` dependency
111+
112+
## License
113+
114+
Licensed under the Apache License, Version 2.0.

await-tree-attributes/src/lib.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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+
17+
use proc_macro::TokenStream;
18+
use quote::quote;
19+
use syn::{parse_macro_input, Ident, ItemFn, Token};
20+
21+
/// Parse the attribute arguments to extract method calls and format args
22+
struct InstrumentArgs {
23+
method_calls: Vec<Ident>,
24+
format_args: Option<proc_macro2::TokenStream>,
25+
}
26+
27+
impl syn::parse::Parse for InstrumentArgs {
28+
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
29+
let mut method_calls = Vec::new();
30+
let mut format_args = None;
31+
32+
// Parse identifiers first (these will become method calls)
33+
while input.peek(Ident) {
34+
// Look ahead to see if this looks like a method call identifier
35+
let fork = input.fork();
36+
let ident: Ident = fork.parse()?;
37+
38+
// Check if the next token after the identifier is a comma or end
39+
// If it's something else (like a parenthesis or string), treat as format args
40+
if fork.peek(Token![,]) || fork.is_empty() {
41+
// This is a method call identifier
42+
input.parse::<Ident>()?; // consume the identifier
43+
method_calls.push(ident);
44+
if input.peek(Token![,]) {
45+
input.parse::<Token![,]>()?;
46+
}
47+
} else {
48+
// This looks like the start of format arguments
49+
break;
50+
}
51+
}
52+
53+
// Parse remaining tokens as format arguments
54+
if !input.is_empty() {
55+
let remaining: proc_macro2::TokenStream = input.parse()?;
56+
format_args = Some(remaining);
57+
}
58+
59+
Ok(InstrumentArgs {
60+
method_calls,
61+
format_args,
62+
})
63+
}
64+
}
65+
66+
/// Instruments an async function with await-tree spans.
67+
///
68+
/// This attribute macro transforms an async function to automatically create
69+
/// an await-tree span and instrument the function's execution.
70+
///
71+
/// # Usage
72+
///
73+
/// ```rust,ignore
74+
/// #[await_tree::instrument("span_name({})", arg1)]
75+
/// async fn foo(arg1: i32, arg2: String) {
76+
/// // function body
77+
/// }
78+
/// ```
79+
///
80+
/// With keywords:
81+
///
82+
/// ```rust,ignore
83+
/// #[await_tree::instrument(long_running, verbose, "span_name({})", arg1)]
84+
/// async fn foo(arg1: i32, arg2: String) {
85+
/// // function body
86+
/// }
87+
/// ```
88+
///
89+
/// The above will be expanded to:
90+
///
91+
/// ```rust,ignore
92+
/// async fn foo(arg1: i32, arg2: String) {
93+
/// let span = await_tree::span!("span_name({})", arg1).long_running().verbose();
94+
/// let fut = async move {
95+
/// // original function body
96+
/// };
97+
/// fut.instrument_await(span).await
98+
/// }
99+
/// ```
100+
///
101+
/// # Arguments
102+
///
103+
/// The macro accepts format arguments similar to `format!` or `println!`:
104+
/// - The first argument is the format string
105+
/// - Subsequent arguments are the values to be formatted
106+
///
107+
/// The format arguments are passed directly to the `await_tree::span!` macro
108+
/// without any parsing or modification.
109+
#[proc_macro_attribute]
110+
pub fn instrument(args: TokenStream, input: TokenStream) -> TokenStream {
111+
let input_fn = parse_macro_input!(input as ItemFn);
112+
113+
// Validate that this is an async function
114+
if input_fn.sig.asyncness.is_none() {
115+
return syn::Error::new_spanned(
116+
&input_fn.sig.fn_token,
117+
"the `instrument` attribute can only be applied to async functions",
118+
)
119+
.to_compile_error()
120+
.into();
121+
}
122+
123+
// Parse the arguments
124+
let parsed_args = if args.is_empty() {
125+
InstrumentArgs {
126+
method_calls: Vec::new(),
127+
format_args: None,
128+
}
129+
} else {
130+
match syn::parse::<InstrumentArgs>(args) {
131+
Ok(args) => args,
132+
Err(e) => return e.to_compile_error().into(),
133+
}
134+
};
135+
136+
// Extract the span format arguments
137+
let span_args = if let Some(format_args) = parsed_args.format_args {
138+
quote! { #format_args }
139+
} else {
140+
// If no format arguments provided, use the function name as span
141+
let fn_name = &input_fn.sig.ident;
142+
quote! { stringify!(#fn_name) }
143+
};
144+
145+
// Build span creation with method calls
146+
let mut span_creation = quote! { ::await_tree::span!(#span_args) };
147+
148+
// Chain all method calls
149+
for method_name in parsed_args.method_calls {
150+
span_creation = quote! { #span_creation.#method_name() };
151+
}
152+
153+
// Extract function components
154+
let fn_vis = &input_fn.vis;
155+
let fn_sig = &input_fn.sig;
156+
let fn_block = &input_fn.block;
157+
let fn_attrs = &input_fn.attrs;
158+
159+
// Generate the instrumented function
160+
let result = quote! {
161+
#(#fn_attrs)*
162+
#fn_vis #fn_sig {
163+
use ::await_tree::SpanExt as _;
164+
let __at_span: ::await_tree::Span = #span_creation;
165+
let __at_fut = async move #fn_block;
166+
::await_tree::InstrumentAwait::instrument_await(__at_fut, __at_span).await
167+
}
168+
};
169+
170+
result.into()
171+
}

0 commit comments

Comments
 (0)