1use proc_macro::TokenStream;
7use quote::quote;
8use syn::parse::{Parse, ParseStream};
9use syn::{ItemFn, Lit, ReturnType, Token, parse_macro_input};
10
11struct MetricsArgs {
13 name: String,
14 description: Option<String>,
15}
16
17impl Parse for MetricsArgs {
18 fn parse(input: ParseStream) -> syn::Result<Self> {
19 let name_lit: Lit = input.parse()?;
20 let name = match name_lit {
21 Lit::Str(s) => s.value(),
22 _ => {
23 return Err(syn::Error::new_spanned(
24 name_lit,
25 "Expected string literal for metric name",
26 ));
27 }
28 };
29
30 let description = if input.parse::<Token![,]>().is_ok() {
31 let desc_lit: Lit = input.parse()?;
32 match desc_lit {
33 Lit::Str(s) => Some(s.value()),
34 _ => {
35 return Err(syn::Error::new_spanned(
36 desc_lit,
37 "Expected string literal for description",
38 ));
39 }
40 }
41 } else {
42 None
43 };
44
45 Ok(MetricsArgs { name, description })
46 }
47}
48
49#[proc_macro_attribute]
89pub fn metrics(args: TokenStream, input: TokenStream) -> TokenStream {
90 let input_fn = parse_macro_input!(input as ItemFn);
91
92 let (metric_prefix, description) = if args.is_empty() {
94 return syn::Error::new_spanned(
95 &input_fn,
96 "Expected string literal for metric prefix, e.g., #[metrics(\"my.operation\")] or #[metrics(\"my.operation\", \"description\")]",
97 )
98 .to_compile_error()
99 .into();
100 } else {
101 match syn::parse::<MetricsArgs>(args) {
102 Ok(parsed_args) => (parsed_args.name, parsed_args.description),
103 Err(e) => {
104 return syn::Error::new(
105 e.span(),
106 "Expected string literal(s) for metric name and optional description",
107 )
108 .to_compile_error()
109 .into();
110 }
111 }
112 };
113
114 let return_type = match &input_fn.sig.output {
116 ReturnType::Type(_, ty) => ty,
117 ReturnType::Default => {
118 return syn::Error::new_spanned(
119 &input_fn.sig,
120 "Function must return a Result<T, E> to use #[metrics] attribute",
121 )
122 .to_compile_error()
123 .into();
124 }
125 };
126
127 let is_result = match return_type.as_ref() {
129 syn::Type::Path(type_path) => type_path
130 .path
131 .segments
132 .last()
133 .is_some_and(|seg| seg.ident == "Result"),
134 _ => false,
135 };
136
137 if !is_result {
138 return syn::Error::new_spanned(
139 return_type,
140 "Function must return a Result<T, E> to use #[metrics] attribute",
141 )
142 .to_compile_error()
143 .into();
144 }
145
146 let args = MetricsArgs {
147 name: metric_prefix,
148 description,
149 };
150
151 let expanded = generate_metrics_wrapper(&input_fn, &args);
152 TokenStream::from(expanded)
153}
154
155fn generate_metrics_wrapper(input_fn: &ItemFn, args: &MetricsArgs) -> proc_macro2::TokenStream {
156 let fn_vis = &input_fn.vis;
157 let fn_sig = &input_fn.sig;
158 let fn_block = &input_fn.block;
159 let fn_attrs = &input_fn.attrs;
160 let metric_prefix = &args.name;
161
162 let registration_code = if let Some(desc) = &args.description {
164 let count_desc = format!("Number of {desc} operations");
165 let timing_desc = format!("Timing of {desc} operations in milliseconds");
166 quote! {
167 static __METRICS_REGISTERED: std::sync::Once = std::sync::Once::new();
169 __METRICS_REGISTERED.call_once(|| {
170 metrics::describe_counter!(#metric_prefix, #count_desc);
171 metrics::describe_counter!(concat!(#metric_prefix, "_ms"), #timing_desc);
172 });
173 }
174 } else {
175 quote! {
176 static __METRICS_REGISTERED: std::sync::Once = std::sync::Once::new();
178 __METRICS_REGISTERED.call_once(|| {
179 metrics::describe_counter!(#metric_prefix, "Operation counter");
180 metrics::describe_counter!(concat!(#metric_prefix, "_ms"), "Operation timing in milliseconds");
181 });
182 }
183 };
184
185 quote! {
186 #(#fn_attrs)*
187 #fn_vis #fn_sig {
188 #registration_code
189
190 let __metrics_start = ::std::time::Instant::now();
191
192 let __metrics_result = { #fn_block };
193
194 static __METRICS_LABELS_SUCCESS: &[(&str, &str)] = &[("success", "true")];
196 static __METRICS_LABELS_ERROR: &[(&str, &str)] = &[("success", "false")];
197 let __metrics_labels = if __metrics_result.is_err() {
198 __METRICS_LABELS_ERROR
199 } else {
200 __METRICS_LABELS_SUCCESS
201 };
202
203 metrics::counter!(#metric_prefix, __metrics_labels).increment(1);
205
206 metrics::counter!(concat!(#metric_prefix, "_ms"), __metrics_labels)
208 .increment(__metrics_start.elapsed().as_millis() as u64);
209
210 __metrics_result
211 }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 #![expect(clippy::unwrap_used)]
218
219 use super::*;
220
221 #[test]
222 fn test_slow_proc_macro_compilation() {
223 let t = trybuild::TestCases::new();
225 t.pass("tests/compile_pass/*.rs");
226 t.compile_fail("tests/compile_fail/*.rs");
227 }
228
229 #[test]
230 fn test_metrics_args_parsing() {
231 let input = quote::quote! { "test.metric" };
233 let parsed: MetricsArgs = syn::parse2(input).unwrap();
234 assert_eq!(parsed.name, "test.metric");
235 assert_eq!(parsed.description, None);
236
237 let input = quote::quote! { "test.metric", "test description" };
239 let parsed: MetricsArgs = syn::parse2(input).unwrap();
240 assert_eq!(parsed.name, "test.metric");
241 assert_eq!(parsed.description, Some("test description".to_string()));
242 }
243
244 #[test]
245 fn test_invalid_args_parsing() {
246 let input = quote::quote! { 123 };
248 let result: syn::Result<MetricsArgs> = syn::parse2(input);
249 assert!(result.is_err());
250
251 let input = quote::quote! { "test.metric", "description", "extra" };
253 let result: syn::Result<MetricsArgs> = syn::parse2(input);
254 assert!(result.is_err());
255 }
256
257 #[test]
258 fn test_generated_code_structure() {
259 use syn::{ItemFn, parse_quote};
261
262 let input: ItemFn = parse_quote! {
263 fn test_function() -> Result<(), &'static str> {
264 Ok(())
265 }
266 };
267
268 let args = MetricsArgs {
269 name: "test.metric".to_string(),
270 description: Some("test description".to_string()),
271 };
272
273 let result = generate_metrics_wrapper(&input, &args);
274 let generated_code = result.to_string();
275
276 assert!(generated_code.contains("__METRICS_LABELS_SUCCESS"));
278 assert!(generated_code.contains("__METRICS_LABELS_ERROR"));
279 assert!(generated_code.contains("test.metric"));
280 assert!(
281 generated_code.contains("std")
282 && generated_code.contains("Instant")
283 && generated_code.contains("now")
284 );
285 assert!(generated_code.contains("metrics") && generated_code.contains("counter"));
286 assert!(generated_code.contains("Number of test description operations"));
287 assert!(generated_code.contains("Timing of test description operations"));
288
289 assert!(generated_code.contains("concat") && generated_code.contains('!'));
291 assert!(generated_code.contains("_ms"));
292 }
293}