1use proc_macro::TokenStream;
9use quote::{format_ident, quote};
10use syn::spanned::Spanned;
11use syn::{
12 Data, DeriveInput, Expr, Fields, GenericArgument, Lit, Meta, PathArguments, Type,
13 parse_macro_input,
14};
15
16enum MetricKind {
17 Counter,
18 Distribution,
19 DynamicCounter,
20 DynamicDistribution,
21 DynamicGauge,
22 DynamicGaugeI64,
23 DynamicHistogram,
24 Gauge,
25 GaugeF64,
26 Histogram,
27 SampledTimer,
28 MaxGauge,
29 MaxGaugeF64,
30 MinGauge,
31 MinGaugeF64,
32 LabeledCounter(Type),
33 LabeledGauge,
34 LabeledHistogram(Type),
35 LabeledSampledTimer(Type),
36}
37
38const PROM_BASE_FIELD_OVERHEAD_BYTES: usize = 48;
40const PROM_COMPLEX_METRIC_OVERHEAD_BYTES: usize = 128;
41const DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES: usize = 24;
42const DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES: usize = 30;
43const DOGSTATSD_HISTOGRAM_LINES: usize = 2;
44const DOGSTATSD_SAMPLED_TIMER_LINES: usize = 3;
45const DOGSTATSD_TAG_PREFIX_BYTES: usize = 2; const DOGSTATSD_TAG_PAIR_OVERHEAD_BYTES: usize = 2; const DYNAMIC_LABELS_PER_SERIES_ESTIMATE: usize = 10;
48const DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES: usize = 16;
49const PROM_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES: usize = 64;
50const PROM_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES: usize = 160;
51const DOGSTATSD_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES: usize = 64;
52const DOGSTATSD_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES: usize = 160;
53
54fn metric_kind(ty: &Type) -> Option<MetricKind> {
55 let Type::Path(type_path) = ty else {
56 return None;
57 };
58 let segment = type_path.path.segments.last()?;
59 match segment.ident.to_string().as_str() {
60 "Counter" => Some(MetricKind::Counter),
61 "Distribution" => Some(MetricKind::Distribution),
62 "DynamicCounter" => Some(MetricKind::DynamicCounter),
63 "DynamicDistribution" => Some(MetricKind::DynamicDistribution),
64 "DynamicGauge" => Some(MetricKind::DynamicGauge),
65 "DynamicGaugeI64" => Some(MetricKind::DynamicGaugeI64),
66 "DynamicHistogram" => Some(MetricKind::DynamicHistogram),
67 "Gauge" => Some(MetricKind::Gauge),
68 "GaugeF64" => Some(MetricKind::GaugeF64),
69 "Histogram" => Some(MetricKind::Histogram),
70 "SampledTimer" => Some(MetricKind::SampledTimer),
71 "MaxGauge" => Some(MetricKind::MaxGauge),
72 "MaxGaugeF64" => Some(MetricKind::MaxGaugeF64),
73 "MinGauge" => Some(MetricKind::MinGauge),
74 "MinGaugeF64" => Some(MetricKind::MinGaugeF64),
75 "LabeledCounter" => {
76 let PathArguments::AngleBracketed(args) = &segment.arguments else {
77 return None;
78 };
79 let arg = args.args.first()?;
80 let GenericArgument::Type(label_ty) = arg else {
81 return None;
82 };
83 Some(MetricKind::LabeledCounter(label_ty.clone()))
84 }
85 "LabeledGauge" => {
86 let PathArguments::AngleBracketed(args) = &segment.arguments else {
87 return None;
88 };
89 let arg = args.args.first()?;
90 let GenericArgument::Type(_label_ty) = arg else {
91 return None;
92 };
93 Some(MetricKind::LabeledGauge)
94 }
95 "LabeledHistogram" => {
96 let PathArguments::AngleBracketed(args) = &segment.arguments else {
97 return None;
98 };
99 let arg = args.args.first()?;
100 let GenericArgument::Type(label_ty) = arg else {
101 return None;
102 };
103 Some(MetricKind::LabeledHistogram(label_ty.clone()))
104 }
105 "LabeledSampledTimer" => {
106 let PathArguments::AngleBracketed(args) = &segment.arguments else {
107 return None;
108 };
109 let arg = args.args.first()?;
110 let GenericArgument::Type(label_ty) = arg else {
111 return None;
112 };
113 Some(MetricKind::LabeledSampledTimer(label_ty.clone()))
114 }
115 _ => None,
116 }
117}
118
119#[proc_macro_derive(ExportMetrics, attributes(metric_prefix, help, otlp))]
170pub fn derive_export_metrics(input: TokenStream) -> TokenStream {
171 let input = parse_macro_input!(input as DeriveInput);
172 match derive_export_metrics_impl(input) {
173 Ok(ts) => ts,
174 Err(err) => err.to_compile_error().into(),
175 }
176}
177
178fn derive_export_metrics_impl(input: DeriveInput) -> syn::Result<TokenStream> {
179 let name = &input.ident;
180 let vis = &input.vis;
181 let state_name = format_ident!("{}DogStatsDState", name);
182
183 let prefix = extract_metric_prefix(&input.attrs).unwrap_or_default();
185
186 let enable_otlp = input.attrs.iter().any(|attr| attr.path().is_ident("otlp"));
188
189 let fields = match &input.data {
191 Data::Struct(data) => match &data.fields {
192 Fields::Named(fields) => &fields.named,
193 _ => {
194 return Err(syn::Error::new_spanned(
195 &data.fields,
196 "ExportMetrics only supports structs with named fields",
197 ));
198 }
199 },
200 _ => {
201 return Err(syn::Error::new_spanned(
202 &input,
203 "ExportMetrics only supports structs",
204 ));
205 }
206 };
207
208 let mut prometheus_exports = Vec::new();
209 let mut dogstatsd_exports = Vec::new();
210 let mut delta_exports = Vec::new();
211 let mut otlp_exports = Vec::new();
212 let mut state_fields = Vec::new();
213 let mut state_inits = Vec::new();
214 let mut state_label_count_exprs = Vec::new();
215 let mut prom_reserve_hint = 0usize;
216 let mut dogstatsd_reserve_hint = 0usize;
217 let mut dogstatsd_delta_reserve_hint = 0usize;
218 let mut dogstatsd_tag_line_hint = 0usize;
219 let mut dogstatsd_delta_tag_line_hint = 0usize;
220 let mut prom_dynamic_reserve_exprs = Vec::new();
221 let mut dogstatsd_dynamic_reserve_exprs = Vec::new();
222 let mut dogstatsd_delta_dynamic_reserve_exprs = Vec::new();
223 let mut dogstatsd_dynamic_tag_line_exprs = Vec::new();
224 let mut dogstatsd_delta_dynamic_tag_line_exprs = Vec::new();
225
226 for field in fields.iter() {
227 let field_name = field.ident.as_ref().ok_or_else(|| {
228 syn::Error::new(field.span(), "ExportMetrics only supports named fields")
229 })?;
230 let field_name_str = field_name.to_string();
231 let prom_metric_name = if prefix.is_empty() {
232 field_name_str.clone()
233 } else {
234 format!("{}_{}", prefix, field_name_str)
235 };
236 let statsd_metric_name = if prefix.is_empty() {
237 field_name_str.clone()
238 } else {
239 format!("{}.{}", prefix, field_name_str)
240 };
241 let help = extract_help(&field.attrs).unwrap_or_else(|| field_name_str.clone());
242
243 prometheus_exports.push(quote! {
244 fast_telemetry::PrometheusExport::export_prometheus(&self.#field_name, output, #prom_metric_name, #help);
245 });
246
247 dogstatsd_exports.push(quote! {
248 fast_telemetry::DogStatsDExport::export_dogstatsd(&self.#field_name, output, #statsd_metric_name, tags);
249 });
250
251 otlp_exports.push(quote! {
252 fast_telemetry::OtlpExport::export_otlp(&self.#field_name, metrics, #prom_metric_name, #help, time_unix_nano);
253 });
254
255 let metric_kind = metric_kind(&field.ty).ok_or_else(|| {
256 syn::Error::new_spanned(
257 &field.ty,
258 format!(
259 "ExportMetrics does not support field '{}' with this type",
260 field_name_str
261 ),
262 )
263 })?;
264
265 prom_reserve_hint += prom_metric_name.len() + help.len() + PROM_BASE_FIELD_OVERHEAD_BYTES;
266 match &metric_kind {
267 MetricKind::Counter
268 | MetricKind::Gauge
269 | MetricKind::GaugeF64
270 | MetricKind::MaxGauge
271 | MetricKind::MaxGaugeF64
272 | MetricKind::MinGauge
273 | MetricKind::MinGaugeF64
274 | MetricKind::Distribution
275 | MetricKind::DynamicCounter
276 | MetricKind::DynamicGauge
277 | MetricKind::DynamicGaugeI64
278 | MetricKind::LabeledCounter(_)
279 | MetricKind::LabeledGauge => {
280 prom_reserve_hint += PROM_BASE_FIELD_OVERHEAD_BYTES;
281 }
282 MetricKind::Histogram
283 | MetricKind::SampledTimer
284 | MetricKind::DynamicHistogram
285 | MetricKind::DynamicDistribution
286 | MetricKind::LabeledHistogram(_)
287 | MetricKind::LabeledSampledTimer(_) => {
288 prom_reserve_hint += PROM_COMPLEX_METRIC_OVERHEAD_BYTES;
289 }
290 }
291
292 match &metric_kind {
293 MetricKind::Counter
294 | MetricKind::Gauge
295 | MetricKind::GaugeF64
296 | MetricKind::MaxGauge
297 | MetricKind::MaxGaugeF64
298 | MetricKind::MinGauge
299 | MetricKind::MinGaugeF64 => {
300 dogstatsd_reserve_hint +=
301 statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
302 dogstatsd_delta_reserve_hint +=
303 statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
304 dogstatsd_tag_line_hint += 1;
305 dogstatsd_delta_tag_line_hint += 1;
306 }
307 MetricKind::Histogram => {
308 dogstatsd_reserve_hint += (statsd_metric_name.len()
309 + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
310 * DOGSTATSD_HISTOGRAM_LINES;
311 dogstatsd_delta_reserve_hint += (statsd_metric_name.len()
312 + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
313 * DOGSTATSD_HISTOGRAM_LINES;
314 dogstatsd_tag_line_hint += DOGSTATSD_HISTOGRAM_LINES;
315 dogstatsd_delta_tag_line_hint += DOGSTATSD_HISTOGRAM_LINES;
316 }
317 MetricKind::SampledTimer => {
318 dogstatsd_reserve_hint += (statsd_metric_name.len()
319 + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
320 * DOGSTATSD_SAMPLED_TIMER_LINES;
321 dogstatsd_delta_reserve_hint += (statsd_metric_name.len()
322 + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
323 * DOGSTATSD_SAMPLED_TIMER_LINES;
324 dogstatsd_tag_line_hint += DOGSTATSD_SAMPLED_TIMER_LINES;
325 dogstatsd_delta_tag_line_hint += DOGSTATSD_SAMPLED_TIMER_LINES;
326 }
327 MetricKind::Distribution => {
328 dogstatsd_reserve_hint +=
329 statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
330 dogstatsd_delta_reserve_hint +=
331 statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
332 dogstatsd_tag_line_hint += 1;
333 dogstatsd_delta_tag_line_hint += 1;
334 }
335 MetricKind::DynamicCounter
336 | MetricKind::DynamicGauge
337 | MetricKind::DynamicGaugeI64
338 | MetricKind::DynamicDistribution => {
339 dogstatsd_reserve_hint +=
340 statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
341 dogstatsd_delta_reserve_hint +=
342 statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
343 }
344 MetricKind::DynamicHistogram => {
345 dogstatsd_reserve_hint += (statsd_metric_name.len()
346 + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
347 * DOGSTATSD_HISTOGRAM_LINES;
348 dogstatsd_delta_reserve_hint += (statsd_metric_name.len()
349 + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
350 * DOGSTATSD_HISTOGRAM_LINES;
351 }
352 MetricKind::LabeledCounter(_) | MetricKind::LabeledGauge => {
353 dogstatsd_reserve_hint +=
354 statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
355 dogstatsd_delta_reserve_hint +=
356 statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
357 }
358 MetricKind::LabeledHistogram(_) => {
359 dogstatsd_reserve_hint += (statsd_metric_name.len()
360 + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
361 * DOGSTATSD_HISTOGRAM_LINES;
362 dogstatsd_delta_reserve_hint += (statsd_metric_name.len()
363 + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
364 * DOGSTATSD_HISTOGRAM_LINES;
365 }
366 MetricKind::LabeledSampledTimer(_) => {
367 dogstatsd_reserve_hint += (statsd_metric_name.len()
368 + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
369 * DOGSTATSD_SAMPLED_TIMER_LINES;
370 dogstatsd_delta_reserve_hint += (statsd_metric_name.len()
371 + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
372 * DOGSTATSD_SAMPLED_TIMER_LINES;
373 }
374 }
375
376 match metric_kind {
377 MetricKind::Counter => {
378 state_label_count_exprs.push(quote! { 0usize });
379 state_fields.push(quote! { #field_name: isize, });
380 state_inits.push(quote! { #field_name: 0, });
381 delta_exports.push(quote! {
382 let current = self.#field_name.sum();
383 let delta = current - state.#field_name;
384 state.#field_name = current;
385 fast_telemetry::__macro_support::__write_dogstatsd(output, #statsd_metric_name, delta, "c", tags);
387 });
388 }
389 MetricKind::Distribution => {
390 let buckets_state_field = format_ident!("{}_buckets", field_name);
391 state_label_count_exprs.push(quote! { 0usize });
392 state_fields.push(quote! { #buckets_state_field: [u64; 65], });
393 state_inits.push(quote! { #buckets_state_field: [0u64; 65], });
394 delta_exports.push(quote! {
395 let snap = self.#field_name.buckets_snapshot();
396 fast_telemetry::__macro_support::__write_dogstatsd_distribution_delta(
397 output, #statsd_metric_name, &snap, &mut state.#buckets_state_field, tags
398 );
399 });
400 }
401 MetricKind::DynamicCounter => {
402 prom_dynamic_reserve_exprs.push(quote! {
403 self.#field_name.cardinality().saturating_mul(
404 #prom_metric_name.len()
405 + #PROM_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
406 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
407 )
408 });
409 dogstatsd_dynamic_reserve_exprs.push(quote! {
410 self.#field_name.cardinality().saturating_mul(
411 #statsd_metric_name.len()
412 + #DOGSTATSD_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
413 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
414 )
415 });
416 dogstatsd_delta_dynamic_reserve_exprs.push(quote! {
417 self.#field_name.cardinality().saturating_mul(
418 #statsd_metric_name.len()
419 + #DOGSTATSD_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
420 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
421 )
422 });
423 dogstatsd_dynamic_tag_line_exprs.push(quote! { self.#field_name.cardinality() });
424 dogstatsd_delta_dynamic_tag_line_exprs
425 .push(quote! { self.#field_name.cardinality() });
426 state_label_count_exprs.push(quote! { self.#field_name.len() });
427 state_fields.push(quote! { #field_name: std::collections::HashMap<fast_telemetry::DynamicLabelSet, isize>, });
428 state_inits.push(quote! { #field_name: std::collections::HashMap::new(), });
429 delta_exports.push(quote! {
430 let overflow = self.#field_name.overflow_count();
431 if overflow > 0 {
432 log::warn!(
433 "fast-telemetry: {} hit cardinality cap, {} records routed to overflow",
434 #statsd_metric_name,
435 overflow
436 );
437 }
438 let mut current_keys = std::collections::HashSet::new();
439 self.#field_name.visit_series(|labels, current| {
440 let key = fast_telemetry::DynamicLabelSet::from_canonical_pairs(labels);
441 current_keys.insert(key.clone());
442 let previous = state.#field_name.get(&key).copied().unwrap_or(0);
443 let delta = current - previous;
444 state.#field_name.insert(key, current);
445 fast_telemetry::__macro_support::__write_dogstatsd_dynamic_pairs(
446 output,
447 #statsd_metric_name,
448 delta,
449 "c",
450 labels,
451 tags,
452 );
453 });
454 state.#field_name.retain(|k, _| current_keys.contains(k));
456 });
457 }
458 MetricKind::DynamicDistribution => {
459 let buckets_state_field = format_ident!("{}_buckets", field_name);
460 prom_dynamic_reserve_exprs.push(quote! {
461 self.#field_name.cardinality().saturating_mul(
462 #prom_metric_name.len()
463 + #PROM_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES
464 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
465 )
466 });
467 dogstatsd_dynamic_reserve_exprs.push(quote! {
468 self.#field_name.cardinality().saturating_mul(
469 #statsd_metric_name.len()
470 + #DOGSTATSD_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES
471 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
472 )
473 });
474 dogstatsd_delta_dynamic_reserve_exprs.push(quote! {
475 self.#field_name.cardinality().saturating_mul(
476 #statsd_metric_name.len()
477 + #DOGSTATSD_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES
478 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
479 )
480 });
481 dogstatsd_dynamic_tag_line_exprs.push(quote! { self.#field_name.cardinality() });
482 dogstatsd_delta_dynamic_tag_line_exprs
483 .push(quote! { self.#field_name.cardinality() });
484 state_label_count_exprs.push(quote! {
485 self.#buckets_state_field.len()
486 });
487 state_fields.push(quote! { #buckets_state_field: std::collections::HashMap<fast_telemetry::DynamicLabelSet, [u64; 65]>, });
488 state_inits
489 .push(quote! { #buckets_state_field: std::collections::HashMap::new(), });
490 delta_exports.push(quote! {
491 let overflow = self.#field_name.overflow_count();
492 if overflow > 0 {
493 log::warn!(
494 "fast-telemetry: {} hit cardinality cap, {} records routed to overflow",
495 #statsd_metric_name,
496 overflow
497 );
498 }
499 let mut current_keys = std::collections::HashSet::new();
500 self.#field_name.visit_series(|labels, _count, _sum, snap| {
501 let key = fast_telemetry::DynamicLabelSet::from_canonical_pairs(labels);
502 current_keys.insert(key.clone());
503 let prev = state.#buckets_state_field.entry(key).or_insert([0u64; 65]);
504 fast_telemetry::__macro_support::__write_dogstatsd_distribution_delta_dynamic_pairs(
505 output, #statsd_metric_name, &snap, prev, labels, tags
506 );
507 });
508 state.#buckets_state_field.retain(|k, _| current_keys.contains(k));
510 });
511 }
512 MetricKind::DynamicGauge => {
513 prom_dynamic_reserve_exprs.push(quote! {
514 self.#field_name.cardinality().saturating_mul(
515 #prom_metric_name.len()
516 + #PROM_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
517 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
518 )
519 });
520 dogstatsd_dynamic_reserve_exprs.push(quote! {
521 self.#field_name.cardinality().saturating_mul(
522 #statsd_metric_name.len()
523 + #DOGSTATSD_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
524 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
525 )
526 });
527 dogstatsd_delta_dynamic_reserve_exprs.push(quote! {
528 self.#field_name.cardinality().saturating_mul(
529 #statsd_metric_name.len()
530 + #DOGSTATSD_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
531 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
532 )
533 });
534 dogstatsd_dynamic_tag_line_exprs.push(quote! { self.#field_name.cardinality() });
535 dogstatsd_delta_dynamic_tag_line_exprs
536 .push(quote! { self.#field_name.cardinality() });
537 state_label_count_exprs.push(quote! { 0usize });
538 delta_exports.push(quote! {
540 let overflow = self.#field_name.overflow_count();
541 if overflow > 0 {
542 log::warn!(
543 "fast-telemetry: {} hit cardinality cap, {} records routed to overflow",
544 #statsd_metric_name,
545 overflow
546 );
547 }
548 fast_telemetry::DogStatsDExport::export_dogstatsd(&self.#field_name, output, #statsd_metric_name, tags);
549 });
550 }
551 MetricKind::DynamicGaugeI64 => {
552 prom_dynamic_reserve_exprs.push(quote! {
553 self.#field_name.cardinality().saturating_mul(
554 #prom_metric_name.len()
555 + #PROM_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
556 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
557 )
558 });
559 dogstatsd_dynamic_reserve_exprs.push(quote! {
560 self.#field_name.cardinality().saturating_mul(
561 #statsd_metric_name.len()
562 + #DOGSTATSD_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
563 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
564 )
565 });
566 dogstatsd_delta_dynamic_reserve_exprs.push(quote! {
567 self.#field_name.cardinality().saturating_mul(
568 #statsd_metric_name.len()
569 + #DOGSTATSD_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
570 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
571 )
572 });
573 dogstatsd_dynamic_tag_line_exprs.push(quote! { self.#field_name.cardinality() });
574 dogstatsd_delta_dynamic_tag_line_exprs
575 .push(quote! { self.#field_name.cardinality() });
576 state_label_count_exprs.push(quote! { 0usize });
577 delta_exports.push(quote! {
579 let overflow = self.#field_name.overflow_count();
580 if overflow > 0 {
581 log::warn!(
582 "fast-telemetry: {} hit cardinality cap, {} records routed to overflow",
583 #statsd_metric_name,
584 overflow
585 );
586 }
587 fast_telemetry::DogStatsDExport::export_dogstatsd(&self.#field_name, output, #statsd_metric_name, tags);
588 });
589 }
590 MetricKind::DynamicHistogram => {
591 let count_state_field = format_ident!("{}_count", field_name);
592 let sum_state_field = format_ident!("{}_sum", field_name);
593 let count_metric_name = format!("{}.count", statsd_metric_name);
594 let sum_metric_name = format!("{}.sum", statsd_metric_name);
595 prom_dynamic_reserve_exprs.push(quote! {
596 self.#field_name.cardinality().saturating_mul(
597 #prom_metric_name.len()
598 + #PROM_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES
599 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
600 )
601 });
602 dogstatsd_dynamic_reserve_exprs.push(quote! {
603 self.#field_name.cardinality().saturating_mul(
604 #statsd_metric_name.len()
605 + #DOGSTATSD_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES
606 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
607 )
608 });
609 dogstatsd_delta_dynamic_reserve_exprs.push(quote! {
610 self.#field_name.cardinality().saturating_mul(
611 #statsd_metric_name.len()
612 + #DOGSTATSD_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES
613 + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
614 )
615 });
616 dogstatsd_dynamic_tag_line_exprs.push(quote! { self.#field_name.cardinality() });
617 dogstatsd_delta_dynamic_tag_line_exprs
618 .push(quote! { self.#field_name.cardinality() });
619 state_label_count_exprs.push(quote! {
620 core::cmp::max(self.#count_state_field.len(), self.#sum_state_field.len())
621 });
622 state_fields.push(quote! { #count_state_field: std::collections::HashMap<fast_telemetry::DynamicLabelSet, u64>, });
623 state_fields.push(quote! { #sum_state_field: std::collections::HashMap<fast_telemetry::DynamicLabelSet, u64>, });
624 state_inits.push(quote! { #count_state_field: std::collections::HashMap::new(), });
625 state_inits.push(quote! { #sum_state_field: std::collections::HashMap::new(), });
626 delta_exports.push(quote! {
627 let overflow = self.#field_name.overflow_count();
628 if overflow > 0 {
629 log::warn!(
630 "fast-telemetry: {} hit cardinality cap, {} records routed to overflow",
631 #statsd_metric_name,
632 overflow
633 );
634 }
635 let mut current_keys = std::collections::HashSet::new();
636 self.#field_name.visit_series(|labels, series| {
637 let key = fast_telemetry::DynamicLabelSet::from_canonical_pairs(labels);
638 current_keys.insert(key.clone());
639 let current_count = series.count();
640 let current_sum = series.sum();
641 let previous_count = state.#count_state_field.get(&key).copied().unwrap_or(0);
642 let previous_sum = state.#sum_state_field.get(&key).copied().unwrap_or(0);
643 let delta_count = if current_count >= previous_count {
644 current_count - previous_count
645 } else {
646 current_count
647 };
648 let delta_sum = if current_sum >= previous_sum {
649 current_sum - previous_sum
650 } else {
651 current_sum
652 };
653 state.#count_state_field.insert(key.clone(), current_count);
654 state.#sum_state_field.insert(key, current_sum);
655 fast_telemetry::__macro_support::__write_dogstatsd_dynamic_pairs(
656 output,
657 #count_metric_name,
658 delta_count,
659 "c",
660 labels,
661 tags,
662 );
663 fast_telemetry::__macro_support::__write_dogstatsd_dynamic_pairs(
664 output,
665 #sum_metric_name,
666 delta_sum,
667 "c",
668 labels,
669 tags,
670 );
671 });
672 state.#count_state_field.retain(|k, _| current_keys.contains(k));
674 state.#sum_state_field.retain(|k, _| current_keys.contains(k));
675 });
676 }
677 MetricKind::Gauge
678 | MetricKind::GaugeF64
679 | MetricKind::MaxGauge
680 | MetricKind::MaxGaugeF64
681 | MetricKind::MinGauge
682 | MetricKind::MinGaugeF64 => {
683 state_label_count_exprs.push(quote! { 0usize });
684 delta_exports.push(quote! {
686 fast_telemetry::DogStatsDExport::export_dogstatsd(&self.#field_name, output, #statsd_metric_name, tags);
687 });
688 }
689 MetricKind::Histogram => {
690 let count_state_field = format_ident!("{}_count", field_name);
691 let sum_state_field = format_ident!("{}_sum", field_name);
692 let count_metric_name = format!("{}.count", statsd_metric_name);
693 let sum_metric_name = format!("{}.sum", statsd_metric_name);
694 state_label_count_exprs.push(quote! { 0usize });
695 state_fields.push(quote! { #count_state_field: u64, });
696 state_fields.push(quote! { #sum_state_field: u64, });
697 state_inits.push(quote! { #count_state_field: 0, });
698 state_inits.push(quote! { #sum_state_field: 0, });
699 delta_exports.push(quote! {
700 let current_count = self.#field_name.count();
701 let current_sum = self.#field_name.sum();
702 let delta_count = if current_count >= state.#count_state_field {
703 current_count - state.#count_state_field
704 } else {
705 current_count
706 };
707 let delta_sum = if current_sum >= state.#sum_state_field {
708 current_sum - state.#sum_state_field
709 } else {
710 current_sum
711 };
712 state.#count_state_field = current_count;
713 state.#sum_state_field = current_sum;
714 fast_telemetry::__macro_support::__write_dogstatsd(output, #count_metric_name, delta_count, "c", tags);
715 fast_telemetry::__macro_support::__write_dogstatsd(output, #sum_metric_name, delta_sum, "c", tags);
716 });
717 }
718 MetricKind::SampledTimer => {
719 let calls_state_field = format_ident!("{}_calls", field_name);
720 let count_state_field = format_ident!("{}_sample_count", field_name);
721 let sum_state_field = format_ident!("{}_sample_sum", field_name);
722 let calls_metric_name = format!("{}.calls", statsd_metric_name);
723 let count_metric_name = format!("{}.samples.count", statsd_metric_name);
724 let sum_metric_name = format!("{}.samples.sum", statsd_metric_name);
725 state_label_count_exprs.push(quote! { 0usize });
726 state_fields.push(quote! { #calls_state_field: u64, });
727 state_fields.push(quote! { #count_state_field: u64, });
728 state_fields.push(quote! { #sum_state_field: u64, });
729 state_inits.push(quote! { #calls_state_field: 0, });
730 state_inits.push(quote! { #count_state_field: 0, });
731 state_inits.push(quote! { #sum_state_field: 0, });
732 delta_exports.push(quote! {
733 let current_calls = self.#field_name.calls();
734 let current_count = self.#field_name.sample_count();
735 let current_sum = self.#field_name.sample_sum_nanos();
736 let delta_calls = if current_calls >= state.#calls_state_field {
737 current_calls - state.#calls_state_field
738 } else {
739 current_calls
740 };
741 let delta_count = if current_count >= state.#count_state_field {
742 current_count - state.#count_state_field
743 } else {
744 current_count
745 };
746 let delta_sum = if current_sum >= state.#sum_state_field {
747 current_sum - state.#sum_state_field
748 } else {
749 current_sum
750 };
751 state.#calls_state_field = current_calls;
752 state.#count_state_field = current_count;
753 state.#sum_state_field = current_sum;
754 fast_telemetry::__macro_support::__write_dogstatsd(output, #calls_metric_name, delta_calls, "c", tags);
755 fast_telemetry::__macro_support::__write_dogstatsd(output, #count_metric_name, delta_count, "c", tags);
756 fast_telemetry::__macro_support::__write_dogstatsd(output, #sum_metric_name, delta_sum, "c", tags);
757 });
758 }
759 MetricKind::LabeledCounter(label_ty) => {
760 state_label_count_exprs.push(quote! { 0usize });
761 state_fields.push(quote! { #field_name: Vec<isize>, });
762 state_inits.push(quote! {
763 #field_name: vec![0; <#label_ty as fast_telemetry::LabelEnum>::CARDINALITY],
764 });
765 delta_exports.push(quote! {
766 for idx in 0..<#label_ty as fast_telemetry::LabelEnum>::CARDINALITY {
767 let label = <#label_ty as fast_telemetry::LabelEnum>::from_index(idx);
768 let current = self.#field_name.get(label);
769 let delta = current - state.#field_name[idx];
770 state.#field_name[idx] = current;
771 fast_telemetry::__macro_support::__write_dogstatsd_with_label(
772 output,
773 #statsd_metric_name,
774 delta,
775 "c",
776 <#label_ty as fast_telemetry::LabelEnum>::LABEL_NAME,
777 <#label_ty as fast_telemetry::LabelEnum>::variant_name(label),
778 tags,
779 );
780 }
781 });
782 }
783 MetricKind::LabeledGauge => {
784 state_label_count_exprs.push(quote! { 0usize });
785 delta_exports.push(quote! {
786 fast_telemetry::DogStatsDExport::export_dogstatsd(&self.#field_name, output, #statsd_metric_name, tags);
787 });
788 }
789 MetricKind::LabeledHistogram(label_ty) => {
790 let count_state_field = format_ident!("{}_count", field_name);
791 let sum_state_field = format_ident!("{}_sum", field_name);
792 let count_metric_name = format!("{}.count", statsd_metric_name);
793 let sum_metric_name = format!("{}.sum", statsd_metric_name);
794 state_label_count_exprs.push(quote! { 0usize });
795 state_fields.push(quote! { #count_state_field: Vec<u64>, });
796 state_fields.push(quote! { #sum_state_field: Vec<u64>, });
797 state_inits.push(quote! {
798 #count_state_field: vec![0; <#label_ty as fast_telemetry::LabelEnum>::CARDINALITY],
799 });
800 state_inits.push(quote! {
801 #sum_state_field: vec![0; <#label_ty as fast_telemetry::LabelEnum>::CARDINALITY],
802 });
803 delta_exports.push(quote! {
804 for idx in 0..<#label_ty as fast_telemetry::LabelEnum>::CARDINALITY {
805 let label = <#label_ty as fast_telemetry::LabelEnum>::from_index(idx);
806 let current_count = self.#field_name.get(label).count();
807 let current_sum = self.#field_name.get(label).sum();
808 let delta_count = if current_count >= state.#count_state_field[idx] {
809 current_count - state.#count_state_field[idx]
810 } else {
811 current_count
812 };
813 let delta_sum = if current_sum >= state.#sum_state_field[idx] {
814 current_sum - state.#sum_state_field[idx]
815 } else {
816 current_sum
817 };
818 state.#count_state_field[idx] = current_count;
819 state.#sum_state_field[idx] = current_sum;
820 fast_telemetry::__macro_support::__write_dogstatsd_with_label(
821 output,
822 #count_metric_name,
823 delta_count,
824 "c",
825 <#label_ty as fast_telemetry::LabelEnum>::LABEL_NAME,
826 <#label_ty as fast_telemetry::LabelEnum>::variant_name(label),
827 tags,
828 );
829 fast_telemetry::__macro_support::__write_dogstatsd_with_label(
830 output,
831 #sum_metric_name,
832 delta_sum,
833 "c",
834 <#label_ty as fast_telemetry::LabelEnum>::LABEL_NAME,
835 <#label_ty as fast_telemetry::LabelEnum>::variant_name(label),
836 tags,
837 );
838 }
839 });
840 }
841 MetricKind::LabeledSampledTimer(label_ty) => {
842 let calls_state_field = format_ident!("{}_calls", field_name);
843 let count_state_field = format_ident!("{}_sample_count", field_name);
844 let sum_state_field = format_ident!("{}_sample_sum", field_name);
845 let calls_metric_name = format!("{}.calls", statsd_metric_name);
846 let count_metric_name = format!("{}.samples.count", statsd_metric_name);
847 let sum_metric_name = format!("{}.samples.sum", statsd_metric_name);
848 state_label_count_exprs.push(quote! { 0usize });
849 state_fields.push(quote! { #calls_state_field: Vec<u64>, });
850 state_fields.push(quote! { #count_state_field: Vec<u64>, });
851 state_fields.push(quote! { #sum_state_field: Vec<u64>, });
852 state_inits.push(quote! {
853 #calls_state_field: vec![0; <#label_ty as fast_telemetry::LabelEnum>::CARDINALITY],
854 });
855 state_inits.push(quote! {
856 #count_state_field: vec![0; <#label_ty as fast_telemetry::LabelEnum>::CARDINALITY],
857 });
858 state_inits.push(quote! {
859 #sum_state_field: vec![0; <#label_ty as fast_telemetry::LabelEnum>::CARDINALITY],
860 });
861 delta_exports.push(quote! {
862 for idx in 0..<#label_ty as fast_telemetry::LabelEnum>::CARDINALITY {
863 let label = <#label_ty as fast_telemetry::LabelEnum>::from_index(idx);
864 let current_calls = self.#field_name.calls(label);
865 let current_count = self.#field_name.sample_count(label);
866 let current_sum = self.#field_name.sample_sum_nanos(label);
867 let delta_calls = if current_calls >= state.#calls_state_field[idx] {
868 current_calls - state.#calls_state_field[idx]
869 } else {
870 current_calls
871 };
872 let delta_count = if current_count >= state.#count_state_field[idx] {
873 current_count - state.#count_state_field[idx]
874 } else {
875 current_count
876 };
877 let delta_sum = if current_sum >= state.#sum_state_field[idx] {
878 current_sum - state.#sum_state_field[idx]
879 } else {
880 current_sum
881 };
882 state.#calls_state_field[idx] = current_calls;
883 state.#count_state_field[idx] = current_count;
884 state.#sum_state_field[idx] = current_sum;
885 fast_telemetry::__macro_support::__write_dogstatsd_with_label(
886 output,
887 #calls_metric_name,
888 delta_calls,
889 "c",
890 <#label_ty as fast_telemetry::LabelEnum>::LABEL_NAME,
891 <#label_ty as fast_telemetry::LabelEnum>::variant_name(label),
892 tags,
893 );
894 fast_telemetry::__macro_support::__write_dogstatsd_with_label(
895 output,
896 #count_metric_name,
897 delta_count,
898 "c",
899 <#label_ty as fast_telemetry::LabelEnum>::LABEL_NAME,
900 <#label_ty as fast_telemetry::LabelEnum>::variant_name(label),
901 tags,
902 );
903 fast_telemetry::__macro_support::__write_dogstatsd_with_label(
904 output,
905 #sum_metric_name,
906 delta_sum,
907 "c",
908 <#label_ty as fast_telemetry::LabelEnum>::LABEL_NAME,
909 <#label_ty as fast_telemetry::LabelEnum>::variant_name(label),
910 tags,
911 );
912 }
913 });
914 }
915 }
916 }
917
918 let otlp_method = if enable_otlp {
919 quote! {
920 pub fn export_otlp(&self, metrics: &mut Vec<fast_telemetry::otlp::pb::Metric>, time_unix_nano: u64) {
927 #(#otlp_exports)*
928 }
929 }
930 } else {
931 quote! {}
932 };
933
934 let expanded = quote! {
935 #vis struct #state_name {
937 #(#state_fields)*
938 }
939
940 impl #state_name {
941 pub fn new() -> Self {
942 Self {
943 #(#state_inits)*
944 }
945 }
946
947 pub fn tracked_label_sets(&self) -> usize {
949 0usize #(+ #state_label_count_exprs)*
950 }
951 }
952
953 impl Default for #state_name {
954 fn default() -> Self {
955 Self::new()
956 }
957 }
958
959 impl #name {
960 pub fn export_prometheus(&self, output: &mut String) {
962 let __ft_prom_dynamic_reserve = 0usize #(+ #prom_dynamic_reserve_exprs)*;
963 output.reserve(#prom_reserve_hint + __ft_prom_dynamic_reserve);
964 #(#prometheus_exports)*
965 }
966
967 pub fn export_dogstatsd(&self, output: &mut String, tags: &[(&str, &str)]) {
972 let __ft_tag_bytes = if tags.is_empty() {
973 0usize
974 } else {
975 #DOGSTATSD_TAG_PREFIX_BYTES
976 + tags.iter().map(|(k, v)| k.len() + v.len() + #DOGSTATSD_TAG_PAIR_OVERHEAD_BYTES).sum::<usize>()
977 };
978 let __ft_dynamic_reserve = 0usize #(+ #dogstatsd_dynamic_reserve_exprs)*;
979 let __ft_dynamic_tag_lines = 0usize #(+ #dogstatsd_dynamic_tag_line_exprs)*;
980 output.reserve(
981 #dogstatsd_reserve_hint
982 + __ft_dynamic_reserve
983 + __ft_tag_bytes.saturating_mul(#dogstatsd_tag_line_hint + __ft_dynamic_tag_lines)
984 );
985 #(#dogstatsd_exports)*
986 }
987
988 pub fn export_dogstatsd_delta(
992 &self,
993 output: &mut String,
994 tags: &[(&str, &str)],
995 state: &mut #state_name,
996 ) {
997 let __ft_tag_bytes = if tags.is_empty() {
998 0usize
999 } else {
1000 #DOGSTATSD_TAG_PREFIX_BYTES
1001 + tags.iter().map(|(k, v)| k.len() + v.len() + #DOGSTATSD_TAG_PAIR_OVERHEAD_BYTES).sum::<usize>()
1002 };
1003 let __ft_dynamic_reserve = 0usize #(+ #dogstatsd_delta_dynamic_reserve_exprs)*;
1004 let __ft_dynamic_tag_lines = 0usize #(+ #dogstatsd_delta_dynamic_tag_line_exprs)*;
1005 output.reserve(
1006 #dogstatsd_delta_reserve_hint
1007 + __ft_dynamic_reserve
1008 + __ft_tag_bytes.saturating_mul(#dogstatsd_delta_tag_line_hint + __ft_dynamic_tag_lines)
1009 );
1010 #(#delta_exports)*
1011 }
1012
1013 pub fn export_dogstatsd_with_temporality(
1015 &self,
1016 output: &mut String,
1017 tags: &[(&str, &str)],
1018 temporality: fast_telemetry::Temporality,
1019 state: &mut #state_name,
1020 ) {
1021 match temporality {
1022 fast_telemetry::Temporality::Cumulative => self.export_dogstatsd(output, tags),
1023 fast_telemetry::Temporality::Delta => self.export_dogstatsd_delta(output, tags, state),
1024 }
1025 }
1026
1027 #otlp_method
1028 }
1029 };
1030
1031 Ok(TokenStream::from(expanded))
1032}
1033
1034#[proc_macro_derive(LabelEnum, attributes(label_name, label))]
1068pub fn derive_label_enum(input: TokenStream) -> TokenStream {
1069 let input = parse_macro_input!(input as DeriveInput);
1070 match derive_label_enum_impl(input) {
1071 Ok(ts) => ts,
1072 Err(err) => err.to_compile_error().into(),
1073 }
1074}
1075
1076fn derive_label_enum_impl(input: DeriveInput) -> syn::Result<TokenStream> {
1077 let name = &input.ident;
1078
1079 let label_name = extract_label_name(&input.attrs).ok_or_else(|| {
1081 syn::Error::new_spanned(
1082 name,
1083 "LabelEnum requires #[label_name = \"...\"] attribute on the enum",
1084 )
1085 })?;
1086
1087 let variants = match &input.data {
1089 Data::Enum(data) => &data.variants,
1090 _ => {
1091 return Err(syn::Error::new_spanned(
1092 &input,
1093 "LabelEnum can only be derived for enums",
1094 ));
1095 }
1096 };
1097 if variants.is_empty() {
1098 return Err(syn::Error::new_spanned(
1099 name,
1100 "LabelEnum requires at least one variant",
1101 ));
1102 }
1103
1104 let cardinality = variants.len();
1105
1106 let as_index_arms: Vec<_> = variants
1108 .iter()
1109 .enumerate()
1110 .map(|(idx, variant)| {
1111 let variant_ident = &variant.ident;
1112 quote! { Self::#variant_ident => #idx, }
1113 })
1114 .collect();
1115
1116 let from_index_arms: Vec<_> = variants
1118 .iter()
1119 .enumerate()
1120 .map(|(idx, variant)| {
1121 let variant_ident = &variant.ident;
1122 quote! { #idx => Self::#variant_ident, }
1123 })
1124 .collect();
1125
1126 let last_variant = &variants[variants.len() - 1].ident;
1128
1129 let variant_name_arms: Vec<_> = variants
1131 .iter()
1132 .map(|variant| {
1133 let variant_ident = &variant.ident;
1134 let label_value = extract_label_override(&variant.attrs)
1135 .unwrap_or_else(|| to_snake_case(&variant_ident.to_string()));
1136 quote! { Self::#variant_ident => #label_value, }
1137 })
1138 .collect();
1139
1140 let expanded = quote! {
1141 impl fast_telemetry::LabelEnum for #name {
1142 const CARDINALITY: usize = #cardinality;
1143 const LABEL_NAME: &'static str = #label_name;
1144
1145 fn as_index(self) -> usize {
1146 match self {
1147 #(#as_index_arms)*
1148 }
1149 }
1150
1151 fn from_index(index: usize) -> Self {
1152 match index {
1153 #(#from_index_arms)*
1154 _ => Self::#last_variant,
1155 }
1156 }
1157
1158 fn variant_name(self) -> &'static str {
1159 match self {
1160 #(#variant_name_arms)*
1161 }
1162 }
1163 }
1164 };
1165
1166 Ok(TokenStream::from(expanded))
1167}
1168
1169fn extract_label_name(attrs: &[syn::Attribute]) -> Option<String> {
1171 for attr in attrs {
1172 if attr.path().is_ident("label_name")
1173 && let Meta::NameValue(nv) = &attr.meta
1174 && let Expr::Lit(expr_lit) = &nv.value
1175 && let Lit::Str(lit) = &expr_lit.lit
1176 {
1177 return Some(lit.value());
1178 }
1179 }
1180 None
1181}
1182
1183fn extract_label_override(attrs: &[syn::Attribute]) -> Option<String> {
1185 for attr in attrs {
1186 if attr.path().is_ident("label")
1187 && let Meta::NameValue(nv) = &attr.meta
1188 && let Expr::Lit(expr_lit) = &nv.value
1189 && let Lit::Str(lit) = &expr_lit.lit
1190 {
1191 return Some(lit.value());
1192 }
1193 }
1194 None
1195}
1196
1197fn to_snake_case(s: &str) -> String {
1199 let mut result = String::new();
1200 for (i, c) in s.chars().enumerate() {
1201 if c.is_uppercase() {
1202 if i > 0 {
1203 result.push('_');
1204 }
1205 for lower in c.to_lowercase() {
1206 result.push(lower);
1207 }
1208 } else {
1209 result.push(c);
1210 }
1211 }
1212 result
1213}
1214
1215fn extract_metric_prefix(attrs: &[syn::Attribute]) -> Option<String> {
1216 for attr in attrs {
1217 if attr.path().is_ident("metric_prefix")
1218 && let Meta::NameValue(nv) = &attr.meta
1219 && let Expr::Lit(expr_lit) = &nv.value
1220 && let Lit::Str(lit) = &expr_lit.lit
1221 {
1222 return Some(lit.value());
1223 }
1224 }
1225 None
1226}
1227
1228fn extract_help(attrs: &[syn::Attribute]) -> Option<String> {
1229 for attr in attrs {
1230 if attr.path().is_ident("help")
1231 && let Meta::NameValue(nv) = &attr.meta
1232 && let Expr::Lit(expr_lit) = &nv.value
1233 && let Lit::Str(lit) = &expr_lit.lit
1234 {
1235 return Some(lit.value());
1236 }
1237 }
1238 None
1239}