micromegas_tracing_proc_macros/lib.rs
1//! Procedural macros for instrumenting Rust functions with tracing spans.
2//!
3//! This crate provides `#[span_fn]` and `#[log_fn]` attribute macros that automatically
4//! inject instrumentation into functions. These macros are the primary way to instrument
5//! async code in micromegas.
6//!
7//! # Quick Start
8//!
9//! ```rust,ignore
10//! use micromegas_tracing::prelude::*;
11//!
12//! #[span_fn]
13//! async fn fetch_user(id: u64) -> User {
14//! // This async function is automatically instrumented
15//! database.get_user(id).await
16//! }
17//!
18//! #[span_fn]
19//! fn compute_hash(data: &[u8]) -> Hash {
20//! // Sync functions work too
21//! hasher.hash(data)
22//! }
23//! ```
24//!
25//! # Why use `#[span_fn]`?
26//!
27//! The `#[span_fn]` macro solves a fundamental challenge in async Rust: tracking execution
28//! time across `.await` points. When an async function awaits, it yields control and may
29//! resume on a different thread. `#[span_fn]` wraps your async code in an `InstrumentedFuture`
30//! that correctly tracks wall-clock time even across these suspension points.
31//!
32//! For sync functions, it creates a scope-based span that measures the function's execution.
33//!
34//! # Import
35//!
36//! These macros are re-exported through the tracing prelude:
37//!
38//! ```rust,ignore
39//! use micromegas_tracing::prelude::*;
40//! ```
41
42// crate-specific lint exceptions:
43//#![allow()]
44
45use proc_macro::TokenStream;
46use proc_macro2::Literal;
47use quote::quote;
48use syn::{
49 ItemFn, ReturnType, Type, TypePath,
50 parse::{Parse, ParseStream, Result},
51 parse_macro_input, parse_quote,
52};
53
54struct TraceArgs {
55 alternative_name: Option<Literal>,
56}
57
58impl Parse for TraceArgs {
59 fn parse(input: ParseStream<'_>) -> Result<Self> {
60 if input.is_empty() {
61 Ok(Self {
62 alternative_name: None,
63 })
64 } else {
65 Ok(Self {
66 alternative_name: Some(Literal::parse(input)?),
67 })
68 }
69 }
70}
71
72/// Check if the function returns a Future (indicating it's an async trait method)
73fn returns_future(function: &ItemFn) -> bool {
74 match &function.sig.output {
75 ReturnType::Type(_, ty) => is_future_type(ty),
76 ReturnType::Default => false,
77 }
78}
79
80/// Check if a type is a Future type (Pin<Box<dyn Future>> or impl Future)
81fn is_future_type(ty: &Type) -> bool {
82 match ty {
83 // Check for Pin<Box<dyn Future<...>>>
84 Type::Path(TypePath { path, .. }) => {
85 if let Some(last_segment) = path.segments.last()
86 && last_segment.ident == "Pin"
87 {
88 // This is the pattern async-trait generates
89 return true;
90 }
91 false
92 }
93 // Check for impl Future<...>
94 Type::ImplTrait(impl_trait) => impl_trait.bounds.iter().any(|bound| {
95 if let syn::TypeParamBound::Trait(trait_bound) = bound
96 && let Some(segment) = trait_bound.path.segments.last()
97 {
98 return segment.ident == "Future";
99 }
100 false
101 }),
102 _ => false,
103 }
104}
105
106/// Instruments a function with automatic span tracing.
107///
108/// This is the primary macro for instrumenting both sync and async functions in micromegas.
109/// It automatically detects the function type and applies the appropriate instrumentation.
110///
111/// # Supported Function Types
112///
113/// - **Sync functions**: Wrapped with `span_scope!` for scope-based timing
114/// - **Async functions**: Wrapped with `InstrumentedFuture` for accurate async timing
115/// - **Async trait methods**: Works with `#[async_trait]` - place `#[span_fn]` after `#[async_trait]`
116///
117/// # Basic Usage
118///
119/// ```rust,ignore
120/// use micromegas_tracing::prelude::*;
121///
122/// // Async function - tracks time across .await points
123/// #[span_fn]
124/// async fn process_request(req: Request) -> Response {
125/// let data = fetch_data(req.id).await;
126/// transform(data).await
127/// }
128///
129/// // Sync function - tracks wall-clock execution time
130/// #[span_fn]
131/// fn calculate_checksum(data: &[u8]) -> u32 {
132/// data.iter().fold(0u32, |acc, &b| acc.wrapping_add(b as u32))
133/// }
134/// ```
135///
136/// # Custom Span Names
137///
138/// By default, the span name is the function name prefixed with the module path.
139/// You can override this with a custom name:
140///
141/// ```rust,ignore
142/// #[span_fn("custom_operation_name")]
143/// async fn internal_impl() {
144/// // Span will be named "module::path::custom_operation_name"
145/// }
146/// ```
147///
148/// # With Async Traits
149///
150/// When using `#[async_trait]`, place `#[span_fn]` on the method *after* the
151/// `#[async_trait]` attribute on the impl block:
152///
153/// ```rust,ignore
154/// use async_trait::async_trait;
155/// use micromegas_tracing::prelude::*;
156///
157/// #[async_trait]
158/// trait DataService {
159/// async fn fetch(&self, id: u64) -> Data;
160/// }
161///
162/// #[async_trait]
163/// impl DataService for MyService {
164/// #[span_fn]
165/// async fn fetch(&self, id: u64) -> Data {
166/// self.db.query(id).await
167/// }
168/// }
169/// ```
170///
171/// # How It Works
172///
173/// For **async functions**, the macro:
174/// 1. Removes the `async` keyword
175/// 2. Changes the return type to `impl Future<Output = T>`
176/// 3. Wraps the body in an `InstrumentedFuture` that tracks timing
177///
178/// For **sync functions**, the macro:
179/// 1. Inserts a `span_scope!` call at the start of the function
180/// 2. The span automatically closes when the function returns
181///
182/// # Performance
183///
184/// The overhead is approximately 40ns per span (20ns per event, with a span
185/// recording both begin and end). This makes it suitable for high-frequency
186/// instrumentation. Spans are collected in thread-local storage and batched
187/// for efficient transmission.
188///
189/// # See Also
190///
191/// - [`log_fn`] - For simple function entry logging without timing
192/// - `span_scope!` - For manual scope-based spans within a function
193/// - `span_async_named!` - For manual async spans with dynamic names
194#[proc_macro_attribute]
195pub fn span_fn(args: TokenStream, input: TokenStream) -> TokenStream {
196 let args = parse_macro_input!(args as TraceArgs);
197 let mut function = parse_macro_input!(input as ItemFn);
198
199 let function_name = args
200 .alternative_name
201 .map_or(function.sig.ident.to_string(), |n| n.to_string());
202
203 if returns_future(&function) {
204 // Case 1: Async trait method (after #[async_trait] transformation)
205 // Function returns Pin<Box<dyn Future<Output = T>>> and has no async keyword
206 let stmts = &function.block.stmts;
207
208 // Extract and instrument the async block from Box::pin(async move { ... })
209 if stmts.len() == 1
210 && let syn::Stmt::Expr(syn::Expr::Call(call_expr)) = &stmts[0]
211 && call_expr.args.len() == 1
212 {
213 let async_block = &call_expr.args[0];
214
215 // Replace the function body with instrumented version
216 function.block = parse_quote! {
217 {
218 static_span_desc!(_SCOPE_DESC, concat!(module_path!(), "::", #function_name));
219 Box::pin(InstrumentedFuture::new(
220 #async_block,
221 &_SCOPE_DESC
222 ))
223 }
224 };
225 } else {
226 // For complex async functions that don't match the simple Box::pin pattern,
227 // wrap the entire body in an async block and instrument it
228 let original_block = &function.block;
229 function.block = parse_quote! {
230 {
231 static_span_desc!(_SCOPE_DESC, concat!(module_path!(), "::", #function_name));
232 Box::pin(InstrumentedFuture::new(
233 async move #original_block,
234 &_SCOPE_DESC
235 ))
236 }
237 };
238 }
239 } else if function.sig.asyncness.is_some() {
240 // Case 2: Regular async function
241 let original_block = &function.block;
242 let output_type = match &function.sig.output {
243 syn::ReturnType::Type(_, ty) => quote! { #ty },
244 syn::ReturnType::Default => quote! { () },
245 };
246
247 // Remove async and change return type to impl Future
248 function.sig.asyncness = None;
249 function.sig.output = parse_quote! { -> impl std::future::Future<Output = #output_type> };
250 function.block = parse_quote! {
251 {
252 static_span_desc!(_SCOPE_DESC, concat!(module_path!(), "::", #function_name));
253 let fut = async move #original_block;
254 InstrumentedFuture::new(fut, &_SCOPE_DESC)
255 }
256 };
257 } else {
258 // Case 3: Regular sync function
259 function.block.stmts.insert(
260 0,
261 parse_quote! {
262 span_scope!(_METADATA_FUNC, concat!(module_path!(), "::", #function_name));
263 },
264 );
265 }
266
267 TokenStream::from(quote! {
268 #function
269 })
270}
271
272/// Logs function entry with the function name.
273///
274/// This macro injects a `trace!` call at the start of the function, logging
275/// the function name. Unlike [`span_fn`], it does not measure execution time
276/// or track function exit.
277///
278/// # Usage
279///
280/// ```rust,ignore
281/// use micromegas_tracing::prelude::*;
282///
283/// #[log_fn]
284/// fn handle_event(event: Event) {
285/// // Logs "handle_event" at trace level when called
286/// process(event);
287/// }
288/// ```
289///
290/// # When to Use
291///
292/// Use `log_fn` when you only need to know that a function was called, without
293/// timing data. Note that log entries are typically more expensive than span
294/// events, so for performance instrumentation prefer [`span_fn`].
295///
296/// `log_fn` is useful when you want function calls to appear in the log stream
297/// rather than the spans/traces stream.
298#[proc_macro_attribute]
299pub fn log_fn(args: TokenStream, input: TokenStream) -> TokenStream {
300 assert!(args.is_empty());
301 let mut function = parse_macro_input!(input as ItemFn);
302 let function_name = function.sig.ident.to_string();
303
304 function.block.stmts.insert(
305 0,
306 parse_quote! {
307 trace!(#function_name);
308 },
309 );
310 TokenStream::from(quote! {
311 #function
312 })
313}