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