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 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}