Skip to main content

veecle_telemetry_macros/
lib.rs

1// Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0.
2// Copyright 2025 Veecle GmbH.
3//
4// This file has been modified from the original TiKV implementation.
5
6//! An attribute macro designed to eliminate boilerplate code for [`veecle_telemetry`](https://crates.io/crates/veecle_telemetry).
7
8#![recursion_limit = "256"]
9#![cfg_attr(not(feature = "enable"), allow(dead_code))]
10#![cfg_attr(not(feature = "enable"), allow(unreachable_code))]
11
12use std::collections::HashMap;
13
14use proc_macro2::{Ident, Span};
15use quote::{quote, quote_spanned};
16use syn::parse::{Parse, ParseStream};
17use syn::punctuated::Punctuated;
18use syn::*;
19
20struct Arguments {
21    name: Option<LitStr>,
22    short_name: bool,
23    properties: Vec<Property>,
24    span: Span,
25}
26
27struct Property {
28    key: LitStr,
29    value: Lit,
30    span: Span,
31}
32
33impl Parse for Property {
34    fn parse(input: ParseStream) -> Result<Self> {
35        let key: LitStr = input.parse()?;
36        input.parse::<Token![:]>()?;
37        let value: Lit = input.parse()?;
38
39        // For some reason, `join` fails in doc macros.
40        let span = key.span().join(value.span()).unwrap_or_else(|| key.span());
41        Ok(Property { key, value, span })
42    }
43}
44
45impl Parse for Arguments {
46    fn parse(input: ParseStream) -> Result<Self> {
47        let mut name = None;
48        let mut short_name = false;
49        let mut properties = Vec::<Property>::new();
50        let mut seen = HashMap::new();
51
52        while !input.is_empty() {
53            let ident: Ident = input.parse()?;
54            if seen.contains_key(&ident.to_string()) {
55                return Err(Error::new(ident.span(), "duplicate argument"));
56            }
57            seen.insert(ident.to_string(), ());
58            input.parse::<Token![=]>()?;
59            match ident.to_string().as_str() {
60                "name" => {
61                    let parsed_name: LitStr = input.parse()?;
62                    name = Some(parsed_name);
63                }
64                "short_name" => {
65                    let parsed_short_name: LitBool = input.parse()?;
66                    short_name = parsed_short_name.value;
67                }
68                "properties" => {
69                    let content;
70                    let _brace_token = braced!(content in input);
71                    let property_list = content.parse_terminated(Property::parse, Token![,])?;
72                    for property in property_list {
73                        if properties
74                            .iter()
75                            .any(|existing| existing.key == property.key)
76                        {
77                            return Err(Error::new(Span::call_site(), "duplicate property key"));
78                        }
79                        properties.push(property);
80                    }
81                }
82                _ => return Err(Error::new(Span::call_site(), "unexpected identifier")),
83            }
84            if !input.is_empty() {
85                let _ = input.parse::<Token![,]>();
86            }
87        }
88
89        Ok(Arguments {
90            name,
91            short_name,
92            properties,
93            span: input.span(),
94        })
95    }
96}
97
98/// An attribute macro designed to eliminate boilerplate code.
99///
100/// This macro automatically creates a span for the annotated function. The span name defaults to
101/// the function name but can be customized by passing a string literal as an argument using the
102/// `name` parameter.
103///
104/// The `#[trace]` attribute requires a local parent context to function correctly. Ensure that
105/// the function annotated with `#[trace]` is called within __a local context of a `Span`__, which
106/// is established by invoking the `Span::set_local_parent()` method.
107///
108/// ## Arguments
109///
110/// * `name` - The name of the span. Defaults to the full path of the function.
111/// * `short_name` - Whether to use the function name without path as the span name. Defaults to `false`.
112/// * `properties` - A list of key-value pairs to be added as properties to the span. The value can be a format string,
113///   where the function arguments are accessible. Defaults to `{}`.
114///
115/// # Examples
116///
117/// ```
118/// use veecle_telemetry::instrument;
119///
120/// #[veecle_telemetry::instrument]
121/// fn simple() {
122///     // ...
123/// }
124///
125/// #[veecle_telemetry::instrument(short_name = true)]
126/// async fn simple_async() {
127///     // ...
128/// }
129///
130/// #[veecle_telemetry::instrument(properties = { "k1": "v1", "a": 2 })]
131/// async fn properties(a: u64) {
132///     // ...
133/// }
134/// ```
135///
136/// The code snippets above will be expanded to:
137///
138/// ```
139/// # extern crate alloc;
140/// # use veecle_telemetry::Span;
141/// # use veecle_telemetry::value::KeyValue;
142///
143/// fn simple() {
144///     let __guard__ = Span::new("example::simple", &[]).entered();
145///     // ...
146/// }
147///
148/// async fn simple_async() {
149///     veecle_telemetry::future::FutureExt::with_span(
150///         async move {
151///             // ...
152///         },
153///         veecle_telemetry::Span::new("simple_async", &[]),
154///     )
155///     .await
156/// }
157///
158/// async fn properties(a: u64) {
159///     veecle_telemetry::future::FutureExt::with_span(
160///         async move {
161///             // ...
162///         },
163///         veecle_telemetry::Span::new("example::properties", &[
164///             KeyValue::new("k1", "v1"),
165///             KeyValue::new("a", 2),
166///         ]),
167///     )
168///     .await
169/// }
170/// ```
171#[proc_macro_attribute]
172pub fn instrument(
173    arguments: proc_macro::TokenStream,
174    item: proc_macro::TokenStream,
175) -> proc_macro::TokenStream {
176    #[cfg(not(feature = "enable"))]
177    {
178        let _ = parse_macro_input!(arguments as Arguments);
179        return item;
180    }
181
182    let arguments = parse_macro_input!(arguments as Arguments);
183    let input = parse_macro_input!(item as ItemFn);
184
185    let function_name = &input.sig.ident;
186
187    // Check for async_trait-like patterns in the block, and instrument the future instead of the wrapper.
188    let function_body = match generate_block(
189        function_name,
190        &input.block,
191        input.sig.asyncness.is_some(),
192        &arguments,
193    ) {
194        Ok(body) => body,
195        Err(error) => return error.to_compile_error().into(),
196    };
197
198    let ItemFn {
199        attrs, vis, sig, ..
200    } = input;
201
202    let Signature {
203        output: return_type,
204        inputs: params,
205        unsafety,
206        constness,
207        abi,
208        ident,
209        asyncness,
210        generics:
211            Generics {
212                params: gen_params,
213                where_clause,
214                ..
215            },
216        ..
217    } = sig;
218
219    quote::quote!(
220        #(#attrs) *
221        #vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type
222        #where_clause
223        {
224            #function_body
225        }
226    )
227    .into()
228}
229
230fn generate_name(
231    function_name: &Ident,
232    arguments: &Arguments,
233    async_closure: bool,
234) -> syn::Result<proc_macro2::TokenStream> {
235    let span = function_name.span();
236    if let Some(name) = &arguments.name {
237        if name.value().is_empty() {
238            return Err(Error::new(span, "`name` can not be empty"));
239        }
240
241        if arguments.short_name {
242            return Err(Error::new(
243                Span::call_site(),
244                "`name` and `short_name` can not be used together",
245            ));
246        }
247
248        Ok(quote_spanned!(span=>
249            #name
250        ))
251    } else if arguments.short_name {
252        let function_name = function_name.to_string();
253        Ok(quote_spanned!(span=>
254            #function_name
255        ))
256    } else {
257        Ok(quote_spanned!(span=>
258            veecle_telemetry::macro_helpers::strip_closure_suffix(core::any::type_name_of_val(&|| {}), #async_closure)
259        ))
260    }
261}
262
263fn generate_properties(arguments: &Arguments) -> proc_macro2::TokenStream {
264    if arguments.properties.is_empty() {
265        return quote::quote!(&[]);
266    }
267
268    let span = arguments.span;
269    let properties = arguments
270        .properties
271        .iter()
272        .map(|Property { key, value, span }| {
273            quote_spanned!(*span=>
274                veecle_telemetry::value::KeyValue::new(#key, #value)
275            )
276        });
277    let properties = Punctuated::<_, Token![,]>::from_iter(properties);
278    quote_spanned!(span=>
279        &[ #properties ]
280    )
281}
282
283/// Instrument a block
284fn generate_block(
285    func_name: &Ident,
286    block: &Block,
287    async_context: bool,
288    arguments: &Arguments,
289) -> syn::Result<proc_macro2::TokenStream> {
290    let name = generate_name(func_name, arguments, async_context)?;
291    let properties = generate_properties(arguments);
292
293    // Generate the instrumented function body.
294    // If the function is an `async fn`, this will wrap it in an async block.
295    // Otherwise, this will enter the span and then perform the rest of the body.
296    if async_context {
297        Ok(quote!(
298            veecle_telemetry::future::FutureExt::with_span(
299                async move { #block },
300                veecle_telemetry::Span::new(#name, #properties),
301            ).await
302        ))
303    } else {
304        Ok(quote!(
305            let __guard__= veecle_telemetry::Span::new(#name, #properties).entered();
306            #block
307        ))
308    }
309}