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