scuffle_metrics_derive/
metrics_impl.rs

1use darling::FromMeta;
2use darling::ast::NestedMeta;
3use quote::ToTokens;
4use syn::Token;
5use syn::parse::Parse;
6use syn::punctuated::Punctuated;
7use syn::spanned::Spanned;
8
9#[derive(Debug, FromMeta)]
10#[darling(default)]
11#[derive(Default)]
12struct ModuleOptions {
13    crate_path: Option<syn::Path>,
14    rename: Option<syn::LitStr>,
15}
16
17impl Parse for ModuleOptions {
18    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
19        if input.is_empty() {
20            Ok(ModuleOptions::default())
21        } else {
22            let meta_list = Punctuated::<NestedMeta, Token![,]>::parse_terminated(input)?
23                .into_iter()
24                .collect::<Vec<_>>();
25
26            Ok(ModuleOptions::from_list(&meta_list)?)
27        }
28    }
29}
30
31#[derive(Debug, FromMeta)]
32#[darling(default)]
33#[derive(Default)]
34struct Options {
35    crate_path: Option<syn::Path>,
36    builder: Option<syn::Expr>,
37    unit: Option<syn::LitStr>,
38    rename: Option<syn::LitStr>,
39}
40
41impl Parse for Options {
42    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
43        if input.is_empty() {
44            Ok(Options::default())
45        } else {
46            let meta_list = Punctuated::<NestedMeta, Token![,]>::parse_terminated(input)?
47                .into_iter()
48                .collect::<Vec<_>>();
49
50            Ok(Options::from_list(&meta_list)?)
51        }
52    }
53}
54
55enum ModuleItem {
56    Other(Box<syn::Item>),
57    Function(proc_macro2::TokenStream),
58}
59
60struct FunctionAttrs {
61    cfg_attrs: Vec<syn::Attribute>,
62    docs: Vec<syn::LitStr>,
63    options: Options,
64}
65
66impl FunctionAttrs {
67    fn from_attrs(attrs: Vec<syn::Attribute>) -> syn::Result<Self> {
68        let (cfg_attrs, others): (Vec<_>, Vec<_>) = attrs.into_iter().partition(|attr| attr.path().is_ident("cfg"));
69
70        let (doc_attrs, others): (Vec<_>, Vec<_>) = others.into_iter().partition(|attr| attr.path().is_ident("doc"));
71
72        Ok(FunctionAttrs {
73            cfg_attrs,
74            docs: doc_attrs
75                .into_iter()
76                .map(|attr| match attr.meta {
77                    syn::Meta::NameValue(syn::MetaNameValue {
78                        value:
79                            syn::Expr::Lit(syn::ExprLit {
80                                lit: syn::Lit::Str(lit), ..
81                            }),
82                        ..
83                    }) => Ok(lit),
84                    _ => Err(syn::Error::new_spanned(attr, "expected string literal")),
85                })
86                .collect::<Result<_, _>>()?,
87            options: {
88                let mut meta = Vec::new();
89                for attr in &others {
90                    if attr.path().is_ident("metrics") {
91                        match &attr.meta {
92                            syn::Meta::List(syn::MetaList { tokens, .. }) => {
93                                meta.extend(NestedMeta::parse_meta_list(tokens.clone())?);
94                            }
95                            _ => return Err(syn::Error::new_spanned(attr, "expected list")),
96                        }
97                    }
98                }
99
100                Options::from_list(&meta)?
101            },
102        })
103    }
104}
105
106struct Function {
107    vis: syn::Visibility,
108    fn_token: Token![fn],
109    ident: syn::Ident,
110    args: syn::punctuated::Punctuated<FnArg, Token![,]>,
111    arrow_token: Token![->],
112    ret: syn::Type,
113    attrs: FunctionAttrs,
114}
115
116impl Parse for Function {
117    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
118        let attrs = input.call(syn::Attribute::parse_outer)?;
119        let vis = input.parse()?;
120        let fn_token = input.parse()?;
121        let ident = input.parse()?;
122        let args_content;
123        let _paren = syn::parenthesized!(args_content in input);
124        let args = args_content.parse_terminated(FnArg::parse, Token![,])?;
125        let arrow_token = input.parse()?;
126        let ret = input.parse()?;
127        input.parse::<Token![;]>()?;
128
129        Ok(Function {
130            vis,
131            fn_token,
132            ident,
133            args,
134            arrow_token,
135            ret,
136            attrs: FunctionAttrs::from_attrs(attrs)?,
137        })
138    }
139}
140
141struct FnArg {
142    cfg_attrs: Vec<syn::Attribute>,
143    other_attrs: Vec<syn::Attribute>,
144    options: FnArgOptions,
145    ident: syn::Ident,
146    colon_token: Token![:],
147    ty: syn::Type,
148    struct_ty: StructTy,
149}
150
151#[derive(Debug, FromMeta)]
152#[darling(default)]
153#[derive(Default)]
154struct FnArgOptions {
155    rename: Option<syn::LitStr>,
156}
157
158impl Parse for FnArgOptions {
159    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
160        if input.is_empty() {
161            Ok(FnArgOptions::default())
162        } else {
163            let meta_list = Punctuated::<NestedMeta, Token![,]>::parse_terminated(input)?
164                .into_iter()
165                .collect::<Vec<_>>();
166
167            Ok(FnArgOptions::from_list(&meta_list)?)
168        }
169    }
170}
171
172enum StructTy {
173    Clone(syn::Type),
174    Into(syn::Type),
175    Raw(syn::Type),
176    Str(syn::Type),
177}
178
179impl StructTy {
180    fn ty(&self) -> &syn::Type {
181        match self {
182            StructTy::Clone(ty) => ty,
183            StructTy::Into(ty) => ty,
184            StructTy::Raw(ty) => ty,
185            StructTy::Str(ty) => ty,
186        }
187    }
188}
189
190fn type_to_struct_type(ty: syn::Type) -> syn::Result<StructTy> {
191    match ty.clone() {
192        syn::Type::Reference(syn::TypeReference { elem, lifetime, .. }) => {
193            if lifetime.is_some_and(|lifetime| lifetime.ident == "static") {
194                return Ok(StructTy::Raw(ty));
195            }
196
197            if let syn::Type::Path(syn::TypePath { path, .. }) = &*elem {
198                if path.is_ident("str") {
199                    return Ok(StructTy::Str(
200                        syn::parse_quote_spanned! { ty.span() => ::std::sync::Arc<#path> },
201                    ));
202                }
203            }
204
205            Ok(StructTy::Clone(*elem))
206        }
207        // Also support impl types
208        syn::Type::ImplTrait(impl_trait) => impl_trait
209            .bounds
210            .iter()
211            .find_map(|bound| match bound {
212                syn::TypeParamBound::Trait(syn::TraitBound {
213                    path: syn::Path { segments, .. },
214                    ..
215                }) => {
216                    let first_segment = segments.first()?;
217                    if first_segment.ident != "Into" {
218                        return None;
219                    }
220
221                    let args = match first_segment.arguments {
222                        syn::PathArguments::AngleBracketed(ref args) => args.args.clone(),
223                        _ => return None,
224                    };
225
226                    if args.len() != 1 {
227                        return None;
228                    }
229
230                    match &args[0] {
231                        syn::GenericArgument::Type(ty) => Some(StructTy::Into(ty.clone())),
232                        _ => None,
233                    }
234                }
235                _ => None,
236            })
237            .ok_or_else(|| syn::Error::new_spanned(impl_trait, "only impl Into<T> trait bounds are supported")),
238        _ => Ok(StructTy::Raw(ty)),
239    }
240}
241
242impl Parse for FnArg {
243    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
244        let attrs = input.call(syn::Attribute::parse_outer)?;
245        let ident = input.parse()?;
246        let colon_token = input.parse()?;
247        let ty: syn::Type = input.parse()?;
248        let struct_ty = type_to_struct_type(ty.clone())?;
249
250        let (cfg_attrs, other_attrs): (Vec<_>, Vec<_>) = attrs.into_iter().partition(|attr| attr.path().is_ident("cfg"));
251
252        let (metric_attrs, other_attrs): (Vec<_>, Vec<_>) =
253            other_attrs.into_iter().partition(|attr| attr.path().is_ident("metrics"));
254
255        let mut meta = Vec::new();
256        for attr in &metric_attrs {
257            match &attr.meta {
258                syn::Meta::List(syn::MetaList { tokens, .. }) => {
259                    meta.extend(NestedMeta::parse_meta_list(tokens.clone())?);
260                }
261                _ => return Err(syn::Error::new_spanned(attr, "expected list")),
262            }
263        }
264
265        let options = FnArgOptions::from_list(&meta)?;
266
267        Ok(FnArg {
268            ident,
269            cfg_attrs,
270            other_attrs,
271            options,
272            colon_token,
273            ty,
274            struct_ty,
275        })
276    }
277}
278
279impl ToTokens for FnArg {
280    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
281        for attr in &self.cfg_attrs {
282            attr.to_tokens(tokens);
283        }
284
285        for attr in &self.other_attrs {
286            attr.to_tokens(tokens);
287        }
288
289        self.ident.to_tokens(tokens);
290        self.colon_token.to_tokens(tokens);
291        self.ty.to_tokens(tokens);
292    }
293}
294
295fn metric_function(
296    input: proc_macro2::TokenStream,
297    module_name: Option<&str>,
298    module_options: &ModuleOptions,
299) -> syn::Result<proc_macro2::TokenStream> {
300    let item = syn::parse2::<Function>(input)?;
301
302    let crate_path = item
303        .attrs
304        .options
305        .crate_path
306        .clone()
307        .or(module_options.crate_path.clone())
308        .unwrap_or_else(|| syn::parse_quote!(::scuffle_metrics));
309
310    let ident = &item.ident;
311    let vis = &item.vis;
312    let ret = &item.ret;
313
314    let const_assert_ret = quote::quote_spanned! { ret.span() =>
315        __assert_impl_collector::<#ret>();
316    };
317
318    let options = &item.attrs.options;
319    let name = &options.rename;
320    let attrs = &item.attrs.cfg_attrs;
321    let docs = &item.attrs.docs;
322    let fn_token = &item.fn_token;
323    let arrow_token = &item.arrow_token;
324    let args = &item.args;
325
326    let collect_args = args
327        .iter()
328        .map(|arg| {
329            let ident = &arg.ident;
330            let ty = &arg.struct_ty.ty();
331
332            let arg_tokens = match &arg.struct_ty {
333                StructTy::Clone(_) => quote::quote! {
334                    ::core::clone::Clone::clone(#ident)
335                },
336                StructTy::Into(_) => quote::quote! {
337                    ::core::convert::Into::into(#ident)
338                },
339                StructTy::Raw(_) => quote::quote! {
340                    #ident
341                },
342                StructTy::Str(_) => quote::quote! {
343                    ::std::sync::Arc::from(#ident)
344                },
345            };
346
347            let name = if let Some(name) = &arg.options.rename {
348                name.value()
349            } else {
350                ident.to_string()
351            };
352
353            quote::quote! {
354                let #ident: #ty = #arg_tokens;
355                if let Some(#ident) = #crate_path::to_value!(#ident) {
356                    ___args.push(#crate_path::opentelemetry::KeyValue::new(
357                        #crate_path::opentelemetry::Key::from_static_str(#name),
358                        #ident,
359                    ));
360                }
361            }
362        })
363        .collect::<Vec<_>>();
364
365    let name = if let Some(name) = name {
366        name.value()
367    } else {
368        ident.to_string()
369    };
370
371    let name = if let Some(module_name) = module_name {
372        format!("{module_name}_{name}")
373    } else {
374        name
375    };
376
377    let make_metric = {
378        let help = docs.iter().map(|doc| doc.value()).collect::<Vec<_>>();
379        let help = help
380            .iter()
381            .map(|help| ::core::primitive::str::trim_end_matches(help.trim(), "."))
382            .filter(|help| !help.is_empty())
383            .collect::<Vec<_>>();
384
385        let help = if help.is_empty() {
386            quote::quote! {}
387        } else {
388            let help = help.join(" ");
389            quote::quote! {
390                builder = builder.with_description(#help);
391            }
392        };
393
394        let unit = if let Some(unit) = &options.unit {
395            quote::quote! {
396                builder = builder.with_unit(#unit);
397            }
398        } else {
399            quote::quote! {}
400        };
401
402        let builder = if let Some(expr) = &options.builder {
403            quote::quote! {
404                { #expr }
405            }
406        } else {
407            quote::quote! {
408                |builder| { builder }
409            }
410        };
411
412        quote::quote! {
413            let callback = #builder;
414
415            let meter = #crate_path::opentelemetry::global::meter_with_scope(
416                #crate_path::opentelemetry::InstrumentationScope::builder(env!("CARGO_PKG_NAME"))
417                    .with_version(env!("CARGO_PKG_VERSION"))
418                    .build()
419            );
420
421            #[allow(unused_mut)]
422            let mut builder = <#ret as #crate_path::collector::IsCollector>::builder(&meter, #name);
423
424            #help
425
426            #unit
427
428            callback(builder).build()
429        }
430    };
431
432    let assert_collector_fn = quote::quote! {
433        const fn __assert_impl_collector<T: #crate_path::collector::IsCollector>() {}
434    };
435
436    let fn_body = quote::quote! {
437        #assert_collector_fn
438        #const_assert_ret
439
440        #[allow(unused_mut)]
441        let mut ___args = Vec::new();
442
443        #(#collect_args)*
444
445        static __COLLECTOR: std::sync::OnceLock<#ret> = std::sync::OnceLock::new();
446
447        let collector = __COLLECTOR.get_or_init(|| { #make_metric });
448
449        #crate_path::collector::Collector::new(___args, collector)
450    };
451
452    Ok(quote::quote! {
453        #(#attrs)*
454        #(#[doc = #docs])*
455        #vis #fn_token #ident(#args) #arrow_token #crate_path::collector::Collector<'static, #ret> {
456            #fn_body
457        }
458    })
459}
460
461pub(crate) fn metrics_impl(
462    args: proc_macro::TokenStream,
463    input: proc_macro::TokenStream,
464) -> syn::Result<proc_macro2::TokenStream> {
465    let module = match syn::parse::<syn::Item>(input)? {
466        syn::Item::Mod(module) => module,
467        syn::Item::Verbatim(tokens) => return metric_function(tokens, None, &Default::default()),
468        item => return Err(syn::Error::new_spanned(item, "expected module or bare function")),
469    };
470
471    let args = syn::parse::<ModuleOptions>(args)?;
472
473    let ident = &module.ident;
474
475    let module_name = if let Some(rename) = args.rename.as_ref() {
476        rename.value()
477    } else {
478        ident.to_string()
479    };
480    let vis = &module.vis;
481
482    let items = module
483        .content
484        .into_iter()
485        .flat_map(|(_, item)| item)
486        .map(|item| match item {
487            syn::Item::Verbatim(verbatim) => metric_function(verbatim, Some(&module_name), &args).map(ModuleItem::Function),
488            item => Ok(ModuleItem::Other(Box::new(item))),
489        })
490        .collect::<syn::Result<Vec<_>>>()?;
491
492    let items = items.into_iter().map(|item| match item {
493        ModuleItem::Other(item) => *item,
494        ModuleItem::Function(item) => syn::Item::Verbatim(item),
495    });
496
497    Ok(quote::quote! {
498        #vis mod #ident {
499            #(#items)*
500        }
501    })
502}