Skip to main content

opentelemetry_traceable_macros/
lib.rs

1//! Proc-macro implementation of `#[traceable]`.
2//!
3//! Instruments a function and makes it "traceable". Traceable
4//! functions (aka trace sites) can be dynamically enabled or
5//! disabled to turn tracing on and off on a per-function basis.
6//!
7//! Span creation is controlled via a per-function bitmask that enables
8//! multiple instrumentations to coexist and produce different trace
9//! shapes/hierarchies (a function may produce a span for a given
10//! instrumentation and not for another). This way, tracing can be
11//! controller per-function and per-instrumentation, at runtime.
12//!
13//! The `traceable` macro allows configuring certain parameters of
14//! the trace site, such as the span name and attributes.
15
16use proc_macro::TokenStream;
17use proc_macro2::TokenStream as TokenStream2;
18use quote::{ToTokens, quote};
19use syn::{
20    Expr, Ident, ItemFn, LitStr, Token,
21    parse::{Parse, ParseStream},
22    parse_macro_input,
23    punctuated::Punctuated,
24};
25
26enum FieldKey {
27    Lit(LitStr),
28    Path(syn::Path),
29}
30
31impl ToTokens for FieldKey {
32    fn to_tokens(&self, tokens: &mut TokenStream2) {
33        match self {
34            FieldKey::Lit(lit) => lit.to_tokens(tokens),
35            FieldKey::Path(path) => path.to_tokens(tokens),
36        }
37    }
38}
39
40struct Field {
41    key: FieldKey,
42    value: Expr,
43}
44
45impl Parse for Field {
46    fn parse(input: ParseStream) -> syn::Result<Self> {
47        let key = if input.peek(LitStr) {
48            FieldKey::Lit(input.parse()?)
49        } else {
50            FieldKey::Path(input.parse()?)
51        };
52        input.parse::<Token![=]>()?;
53        let value: Expr = input.parse()?;
54        Ok(Field { key, value })
55    }
56}
57
58#[derive(Default)]
59struct TraceableArgs {
60    name: Option<LitStr>,
61    fields: Option<Vec<Field>>,
62}
63
64impl Parse for TraceableArgs {
65    fn parse(input: ParseStream) -> syn::Result<Self> {
66        let mut args = TraceableArgs::default();
67        while !input.is_empty() {
68            let ident: Ident = input.parse()?;
69            match ident.to_string().as_str() {
70                "name" => {
71                    input.parse::<Token![=]>()?;
72                    let lit: LitStr = input.parse()?;
73                    if args.name.replace(lit).is_some() {
74                        return Err(syn::Error::new(ident.span(), "duplicate `name` argument"));
75                    }
76                }
77                "fields" => {
78                    let content;
79                    syn::parenthesized!(content in input);
80                    let parsed: Punctuated<Field, Token![,]> =
81                        content.parse_terminated(Field::parse, Token![,])?;
82                    if args.fields.replace(parsed.into_iter().collect()).is_some() {
83                        return Err(syn::Error::new(ident.span(), "duplicate `fields` argument"));
84                    }
85                }
86                other => {
87                    return Err(syn::Error::new(
88                        ident.span(),
89                        format!("unknown argument `{other}`"),
90                    ));
91                }
92            }
93            if !input.is_empty() {
94                input.parse::<Token![,]>()?;
95            }
96        }
97        Ok(args)
98    }
99}
100
101/// Marks a `fn` or `async fn` as a candidate for tracing.
102///
103/// Every call checks a per-function bitmask before creating any span.
104/// A disabled trace site has the cost of one atomic load.
105///
106/// Enable it at runtime through a
107/// `opentelemetry_traceable::instrumentation::Instrumentation`.
108///
109/// The registry key used to enable a function defaults to
110/// `module_path!() + "::" + fn_name`, or the `name` argument if given.
111/// Note: two methods with the same name in the same module share a key
112/// unless `name` is used to distinguish them.
113///
114/// ```ignore
115/// #[traceable]
116/// fn process() { }
117///
118/// #[traceable(name = "kafka.fetch")]
119/// async fn fetch() { }
120///
121/// #[traceable(fields("component" = "proxy", request_id = id))]
122/// async fn handle(id: String) { }
123/// ```
124#[proc_macro_attribute]
125pub fn traceable(attr: TokenStream, item: TokenStream) -> TokenStream {
126    let args = parse_macro_input!(attr as TraceableArgs);
127    let func = parse_macro_input!(item as ItemFn);
128    expand(args, func).into()
129}
130
131fn expand(args: TraceableArgs, func: ItemFn) -> TokenStream2 {
132    let ItemFn {
133        attrs,
134        vis,
135        sig,
136        block,
137    } = func;
138    let fn_ident_str = sig.ident.to_string();
139    let is_async = sig.asyncness.is_some();
140
141    let span_name = match &args.name {
142        Some(lit) => quote! { #lit },
143        None => quote! { #fn_ident_str },
144    };
145
146    let registry_key = match &args.name {
147        Some(lit) => quote! { #lit },
148        None => quote! { ::std::concat!(::std::module_path!(), "::", #fn_ident_str) },
149    };
150
151    let kvs: Vec<TokenStream2> = args
152        .fields
153        .iter()
154        .flatten()
155        .map(|f| {
156            let (key, value) = (&f.key, &f.value);
157            quote! { ::opentelemetry::KeyValue::new(#key, #value) }
158        })
159        .collect();
160
161    // `start_spans` builds one child span per active slot (with that slot's own
162    // tracer and parent) and hands back the single context to attach, or `None`
163    // if no span was created.
164    let traced = if is_async {
165        quote! {
166            ::opentelemetry::trace::FutureExt::with_context(async #block, __traceable_cx).await
167        }
168    } else {
169        quote! {
170            let __traceable_guard = __traceable_cx.attach();
171            let __traceable_ret = #block;
172            ::std::mem::drop(__traceable_guard);
173            __traceable_ret
174        }
175    };
176
177    // Check the bitmask to add zero overhead where possible:
178    //   0 -> no instrumentation is tracing this function: run the raw body,
179    //        having paid a single atomic load.
180    //   _ -> at least one is; build a span per active slot.
181    quote! {
182        #(#attrs)*
183        #vis #sig {
184            #[::opentelemetry_traceable::__private::linkme::distributed_slice(::opentelemetry_traceable::registry::REGISTRY)]
185            #[linkme(crate = ::opentelemetry_traceable::__private::linkme)]
186            static __TRACEABLE_SITE: ::opentelemetry_traceable::registry::TraceSite =
187                ::opentelemetry_traceable::registry::TraceSite::new(#registry_key);
188
189            let __traceable_mask =
190                __TRACEABLE_SITE.enabled_slots.load(::std::sync::atomic::Ordering::Relaxed);
191            if __traceable_mask == 0u64 {
192                #block
193            } else {
194                match ::opentelemetry_traceable::instrumentation::start_spans(
195                    __traceable_mask,
196                    #span_name,
197                    ::std::vec![#(#kvs),*],
198                ) {
199                    ::std::option::Option::None => #block,
200                    ::std::option::Option::Some(__traceable_cx) => { #traced }
201                }
202            }
203        }
204    }
205}