bity_ic_canister_tracing_macros/lib.rs
1//! Module for procedural macros that add tracing capabilities to canister functions.
2//!
3//! This module provides macros that automatically add tracing instrumentation to functions,
4//! making it easier to debug and monitor canister behavior. It wraps functions with tracing
5//! capabilities while preserving their original functionality.
6//!
7//! # Example
8//! ```
9//! use bity_ic_canister_tracing_macros::trace;
10//!
11//! #[trace]
12//! async fn my_function(arg1: u64, arg2: String) -> Result<(), String> {
13//! // Function implementation
14//! Ok(())
15//! }
16//! ```
17
18use proc_macro::TokenStream;
19use proc_macro2::Ident;
20use quote::{format_ident, quote};
21use syn::{parse_macro_input, FnArg, ItemFn, Pat, PatIdent, PatType, Signature};
22
23/// A procedural macro attribute that adds tracing capabilities to a function.
24///
25/// This macro wraps the target function with tracing instrumentation, automatically
26/// logging function entry, arguments, and return values at the trace level.
27///
28/// # Usage
29/// Add the `#[trace]` attribute above any function you want to trace:
30/// ```rust
31/// #[trace]
32/// fn my_function(arg: u64) -> u64 {
33/// arg * 2
34/// }
35/// ```
36///
37/// # Features
38/// * Automatically traces function entry and exit
39/// * Logs all function arguments
40/// * Logs the return value
41/// * Works with both synchronous and asynchronous functions
42/// * Preserves the original function signature
43#[proc_macro_attribute]
44pub fn trace(_: TokenStream, item: TokenStream) -> TokenStream {
45 let mut inner = parse_macro_input!(item as ItemFn);
46
47 // We will wrap the original fn in a new fn whose signature matches the original fn
48 #[allow(clippy::redundant_clone)] // clippy doesn't realise that this is used in the macro
49 let wrapper_sig = inner.sig.clone();
50
51 // Change the name of the inner fn so that it doesn't clash with the wrapper fn
52 let inner_method_name = format_ident!("{}_inner_", inner.sig.ident);
53 inner.sig.ident = inner_method_name.clone();
54
55 let is_async = inner.sig.asyncness.is_some();
56 let arg_names = get_arg_names(&inner.sig);
57
58 let function_call = if is_async {
59 quote! { #inner_method_name ( #(#arg_names),* ) .await }
60 } else {
61 quote! { #inner_method_name ( #(#arg_names),* ) }
62 };
63
64 let expanded = quote! {
65 #[allow(unused_mut)]
66 #[tracing::instrument(level = "trace")]
67 #wrapper_sig {
68 let result = #function_call;
69 tracing::trace!(?result);
70 result
71 }
72 #inner
73 };
74
75 TokenStream::from(expanded)
76}
77
78/// Extracts argument names from a function signature.
79///
80/// This helper function processes a function signature to extract the names of all arguments,
81/// including the receiver (self) if present.
82///
83/// # Arguments
84/// * `signature` - The function signature to process
85///
86/// # Returns
87/// A vector of identifiers representing the argument names
88///
89/// # Panics
90/// Panics if it encounters an argument pattern that it cannot process
91fn get_arg_names(signature: &Signature) -> Vec<Ident> {
92 signature
93 .inputs
94 .iter()
95 .map(|arg| match arg {
96 FnArg::Receiver(r) => r.self_token.into(),
97 FnArg::Typed(PatType { pat, .. }) => {
98 if let Pat::Ident(PatIdent { ident, .. }) = pat.as_ref() {
99 ident.clone()
100 } else {
101 panic!("Unable to determine arg name");
102 }
103 }
104 })
105 .collect()
106}