Skip to main content

fast_telemetry_macros/
lib.rs

1//! Derive macros for fast-telemetry.
2//!
3//! Provides:
4//! - `#[derive(ExportMetrics)]` to auto-generate Prometheus, DogStatsD, and
5//!   optional OTLP export code
6//! - `#[derive(LabelEnum)]` to auto-generate `LabelEnum` trait implementations
7
8use 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
38// Output reserve heuristics for derive-generated exporters.
39const 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; // "|#"
46const DOGSTATSD_TAG_PAIR_OVERHEAD_BYTES: usize = 2; // ":" plus separator/comma budget
47const 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
119fn visitor_meta(
120    name: &str,
121    help: &str,
122    kind: proc_macro2::TokenStream,
123) -> proc_macro2::TokenStream {
124    quote! {
125        fast_telemetry::MetricMeta {
126            name: #name,
127            help: #help,
128            kind: #kind,
129            unit: None,
130        }
131    }
132}
133
134fn visitor_label(label: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
135    quote! {
136        fast_telemetry::MetricLabel {
137            name: fast_telemetry::LabelEnum::label_name(&#label),
138            value: fast_telemetry::LabelEnum::variant_name(#label),
139        }
140    }
141}
142
143/// Derive macro for exporting metrics in Prometheus, DogStatsD, and OTLP formats.
144///
145/// Generates methods:
146/// - `export_prometheus(&self, output: &mut String)` — Prometheus text format
147/// - `export_dogstatsd(&self, output: &mut String, tags: &[(&str, &str)])` — DogStatsD format
148/// - `export_dogstatsd_delta(...)` — DogStatsD with per-sink delta temporality
149/// - `export_dogstatsd_with_temporality(...)` — runtime-selectable cumulative or delta export
150/// - `export_otlp(...)` — OTLP protobuf (only when `#[otlp]` attribute is present)
151/// - `export_clickhouse(...)` — ClickHouse rows (only when `#[clickhouse]` attribute is present)
152///
153/// Supports unlabeled metrics (`Counter`, `Gauge`, `GaugeF64`, `Histogram`,
154/// `Distribution`, `SampledTimer`), compile-time labeled metrics
155/// (`LabeledCounter<L>`, `LabeledGauge<L>`, `LabeledHistogram<L>`,
156/// `LabeledSampledTimer<L>`), and runtime-labeled metrics (`DynamicCounter`,
157/// `DynamicGauge`, `DynamicGaugeI64`, `DynamicHistogram`, `DynamicDistribution`).
158///
159/// # Example
160///
161/// ```ignore
162/// use fast_telemetry::{Counter, Histogram, Gauge, LabeledCounter, DeriveLabel};
163///
164/// #[derive(Copy, Clone, Debug, DeriveLabel)]
165/// #[label_name = "method"]
166/// enum HttpMethod { Get, Post, Put, Delete }
167///
168/// #[derive(ExportMetrics)]
169/// #[metric_prefix = "proxy"]
170/// pub struct ProxyMetrics {
171///     #[help = "Total requests proxied"]
172///     pub requests: Counter,
173///
174///     #[help = "Requests by HTTP method"]
175///     pub requests_by_method: LabeledCounter<HttpMethod>,
176///
177///     #[help = "Request latency in microseconds"]
178///     pub latency: Histogram,
179///
180///     #[help = "Current memory usage"]
181///     pub memory_mb: Gauge,
182/// }
183///
184/// let metrics = ProxyMetrics::new();
185///
186/// // Prometheus export
187/// let mut prom_output = String::new();
188/// metrics.export_prometheus(&mut prom_output);
189///
190/// // DogStatsD export (with optional tags)
191/// let mut statsd_output = String::new();
192/// metrics.export_dogstatsd(&mut statsd_output, &[("env", "prod")]);
193/// ```
194#[proc_macro_derive(ExportMetrics, attributes(metric_prefix, help, otlp, clickhouse))]
195pub fn derive_export_metrics(input: TokenStream) -> TokenStream {
196    let input = parse_macro_input!(input as DeriveInput);
197    match derive_export_metrics_impl(input) {
198        Ok(ts) => ts,
199        Err(err) => err.to_compile_error().into(),
200    }
201}
202
203fn derive_export_metrics_impl(input: DeriveInput) -> syn::Result<TokenStream> {
204    let name = &input.ident;
205    let vis = &input.vis;
206    let state_name = format_ident!("{}DogStatsDState", name);
207
208    // Extract metric_prefix from struct attributes
209    let prefix = extract_metric_prefix(&input.attrs).unwrap_or_default();
210
211    // Check for #[otlp] attribute to enable OTLP export generation
212    let enable_otlp = input.attrs.iter().any(|attr| attr.path().is_ident("otlp"));
213    let enable_clickhouse = input
214        .attrs
215        .iter()
216        .any(|attr| attr.path().is_ident("clickhouse"));
217
218    // Get struct fields
219    let fields = match &input.data {
220        Data::Struct(data) => match &data.fields {
221            Fields::Named(fields) => &fields.named,
222            _ => {
223                return Err(syn::Error::new_spanned(
224                    &data.fields,
225                    "ExportMetrics only supports structs with named fields",
226                ));
227            }
228        },
229        _ => {
230            return Err(syn::Error::new_spanned(
231                &input,
232                "ExportMetrics only supports structs",
233            ));
234        }
235    };
236
237    let mut prometheus_exports = Vec::new();
238    let mut dogstatsd_exports = Vec::new();
239    let mut delta_exports = Vec::new();
240    let mut visitor_exports = Vec::new();
241    let mut otlp_exports = Vec::new();
242    let mut clickhouse_exports = Vec::new();
243    let mut state_fields = Vec::new();
244    let mut state_inits = Vec::new();
245    let mut state_label_count_exprs = Vec::new();
246    let mut prom_reserve_hint = 0usize;
247    let mut dogstatsd_reserve_hint = 0usize;
248    let mut dogstatsd_delta_reserve_hint = 0usize;
249    let mut dogstatsd_tag_line_hint = 0usize;
250    let mut dogstatsd_delta_tag_line_hint = 0usize;
251    let mut prom_dynamic_reserve_exprs = Vec::new();
252    let mut dogstatsd_dynamic_reserve_exprs = Vec::new();
253    let mut dogstatsd_delta_dynamic_reserve_exprs = Vec::new();
254    let mut dogstatsd_dynamic_tag_line_exprs = Vec::new();
255    let mut dogstatsd_delta_dynamic_tag_line_exprs = Vec::new();
256
257    for field in fields.iter() {
258        let field_name = field.ident.as_ref().ok_or_else(|| {
259            syn::Error::new(field.span(), "ExportMetrics only supports named fields")
260        })?;
261        let field_name_str = field_name.to_string();
262        let prom_metric_name = if prefix.is_empty() {
263            field_name_str.clone()
264        } else {
265            format!("{}_{}", prefix, field_name_str)
266        };
267        let statsd_metric_name = if prefix.is_empty() {
268            field_name_str.clone()
269        } else {
270            format!("{}.{}", prefix, field_name_str)
271        };
272        let help = extract_help(&field.attrs).unwrap_or_else(|| field_name_str.clone());
273
274        prometheus_exports.push(quote! {
275            fast_telemetry::PrometheusExport::export_prometheus(&self.#field_name, output, #prom_metric_name, #help);
276        });
277
278        dogstatsd_exports.push(quote! {
279            fast_telemetry::DogStatsDExport::export_dogstatsd(&self.#field_name, output, #statsd_metric_name, tags);
280        });
281
282        otlp_exports.push(quote! {
283            fast_telemetry::OtlpExport::export_otlp(&self.#field_name, metrics, #prom_metric_name, #help, time_unix_nano);
284        });
285        clickhouse_exports.push(quote! {
286            fast_telemetry::ClickHouseExport::export_clickhouse(&self.#field_name, batch, #prom_metric_name, #help, time_unix_nano);
287        });
288
289        let metric_kind = metric_kind(&field.ty).ok_or_else(|| {
290            syn::Error::new_spanned(
291                &field.ty,
292                format!(
293                    "ExportMetrics does not support field '{}' with this type",
294                    field_name_str
295                ),
296            )
297        })?;
298
299        prom_reserve_hint += prom_metric_name.len() + help.len() + PROM_BASE_FIELD_OVERHEAD_BYTES;
300        match &metric_kind {
301            MetricKind::Counter
302            | MetricKind::Gauge
303            | MetricKind::GaugeF64
304            | MetricKind::MaxGauge
305            | MetricKind::MaxGaugeF64
306            | MetricKind::MinGauge
307            | MetricKind::MinGaugeF64
308            | MetricKind::Distribution
309            | MetricKind::DynamicCounter
310            | MetricKind::DynamicGauge
311            | MetricKind::DynamicGaugeI64
312            | MetricKind::LabeledCounter(_)
313            | MetricKind::LabeledGauge => {
314                prom_reserve_hint += PROM_BASE_FIELD_OVERHEAD_BYTES;
315            }
316            MetricKind::Histogram
317            | MetricKind::SampledTimer
318            | MetricKind::DynamicHistogram
319            | MetricKind::DynamicDistribution
320            | MetricKind::LabeledHistogram(_)
321            | MetricKind::LabeledSampledTimer(_) => {
322                prom_reserve_hint += PROM_COMPLEX_METRIC_OVERHEAD_BYTES;
323            }
324        }
325
326        match &metric_kind {
327            MetricKind::Counter
328            | MetricKind::Gauge
329            | MetricKind::GaugeF64
330            | MetricKind::MaxGauge
331            | MetricKind::MaxGaugeF64
332            | MetricKind::MinGauge
333            | MetricKind::MinGaugeF64 => {
334                dogstatsd_reserve_hint +=
335                    statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
336                dogstatsd_delta_reserve_hint +=
337                    statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
338                dogstatsd_tag_line_hint += 1;
339                dogstatsd_delta_tag_line_hint += 1;
340            }
341            MetricKind::Histogram => {
342                dogstatsd_reserve_hint += (statsd_metric_name.len()
343                    + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
344                    * DOGSTATSD_HISTOGRAM_LINES;
345                dogstatsd_delta_reserve_hint += (statsd_metric_name.len()
346                    + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
347                    * DOGSTATSD_HISTOGRAM_LINES;
348                dogstatsd_tag_line_hint += DOGSTATSD_HISTOGRAM_LINES;
349                dogstatsd_delta_tag_line_hint += DOGSTATSD_HISTOGRAM_LINES;
350            }
351            MetricKind::SampledTimer => {
352                dogstatsd_reserve_hint += (statsd_metric_name.len()
353                    + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
354                    * DOGSTATSD_SAMPLED_TIMER_LINES;
355                dogstatsd_delta_reserve_hint += (statsd_metric_name.len()
356                    + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
357                    * DOGSTATSD_SAMPLED_TIMER_LINES;
358                dogstatsd_tag_line_hint += DOGSTATSD_SAMPLED_TIMER_LINES;
359                dogstatsd_delta_tag_line_hint += DOGSTATSD_SAMPLED_TIMER_LINES;
360            }
361            MetricKind::Distribution => {
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                dogstatsd_tag_line_hint += 1;
367                dogstatsd_delta_tag_line_hint += 1;
368            }
369            MetricKind::DynamicCounter
370            | MetricKind::DynamicGauge
371            | MetricKind::DynamicGaugeI64
372            | MetricKind::DynamicDistribution => {
373                dogstatsd_reserve_hint +=
374                    statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
375                dogstatsd_delta_reserve_hint +=
376                    statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
377            }
378            MetricKind::DynamicHistogram => {
379                dogstatsd_reserve_hint += (statsd_metric_name.len()
380                    + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
381                    * DOGSTATSD_HISTOGRAM_LINES;
382                dogstatsd_delta_reserve_hint += (statsd_metric_name.len()
383                    + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
384                    * DOGSTATSD_HISTOGRAM_LINES;
385            }
386            MetricKind::LabeledCounter(_) | MetricKind::LabeledGauge => {
387                dogstatsd_reserve_hint +=
388                    statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
389                dogstatsd_delta_reserve_hint +=
390                    statsd_metric_name.len() + DOGSTATSD_SIMPLE_LINE_OVERHEAD_BYTES;
391            }
392            MetricKind::LabeledHistogram(_) => {
393                dogstatsd_reserve_hint += (statsd_metric_name.len()
394                    + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
395                    * DOGSTATSD_HISTOGRAM_LINES;
396                dogstatsd_delta_reserve_hint += (statsd_metric_name.len()
397                    + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
398                    * DOGSTATSD_HISTOGRAM_LINES;
399            }
400            MetricKind::LabeledSampledTimer(_) => {
401                dogstatsd_reserve_hint += (statsd_metric_name.len()
402                    + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
403                    * DOGSTATSD_SAMPLED_TIMER_LINES;
404                dogstatsd_delta_reserve_hint += (statsd_metric_name.len()
405                    + DOGSTATSD_HISTOGRAM_LINE_OVERHEAD_BYTES)
406                    * DOGSTATSD_SAMPLED_TIMER_LINES;
407            }
408        }
409
410        match metric_kind {
411            MetricKind::Counter => {
412                let meta = visitor_meta(
413                    &prom_metric_name,
414                    &help,
415                    quote! { fast_telemetry::MetricKind::Counter },
416                );
417                visitor_exports.push(quote! {
418                    visitor.counter(#meta, fast_telemetry::MetricLabels::none(), self.#field_name.sum() as i64);
419                });
420                state_label_count_exprs.push(quote! { 0usize });
421                state_fields.push(quote! { #field_name: isize, });
422                state_inits.push(quote! { #field_name: 0, });
423                delta_exports.push(quote! {
424                    let current = self.#field_name.sum();
425                    let delta = current - state.#field_name;
426                    state.#field_name = current;
427                    // Use counter type - in Datadog use .as_count() to see raw values
428                    fast_telemetry::__macro_support::__write_dogstatsd(output, #statsd_metric_name, delta, "c", tags);
429                });
430            }
431            MetricKind::Distribution => {
432                let meta = visitor_meta(
433                    &prom_metric_name,
434                    &help,
435                    quote! { fast_telemetry::MetricKind::Distribution },
436                );
437                visitor_exports.push(quote! {
438                    let __ft_snapshot = self.#field_name.buckets_snapshot();
439                    visitor.distribution(#meta, fast_telemetry::MetricLabels::none(), &__ft_snapshot);
440                });
441                let buckets_state_field = format_ident!("{}_buckets", field_name);
442                state_label_count_exprs.push(quote! { 0usize });
443                state_fields.push(quote! { #buckets_state_field: [u64; 65], });
444                state_inits.push(quote! { #buckets_state_field: [0u64; 65], });
445                delta_exports.push(quote! {
446                    let snap = self.#field_name.buckets_snapshot();
447                    fast_telemetry::__macro_support::__write_dogstatsd_distribution_delta(
448                        output, #statsd_metric_name, &snap, &mut state.#buckets_state_field, tags
449                    );
450                });
451            }
452            MetricKind::DynamicCounter => {
453                let meta = visitor_meta(
454                    &prom_metric_name,
455                    &help,
456                    quote! { fast_telemetry::MetricKind::Counter },
457                );
458                visitor_exports.push(quote! {
459                    let __ft_meta = #meta;
460                    let __ft_overflow = self.#field_name.overflow_count();
461                    if __ft_overflow > 0 {
462                        visitor.dynamic_overflow(__ft_meta, __ft_overflow);
463                    }
464                    self.#field_name.visit_series(|labels, current| {
465                        visitor.counter(
466                            __ft_meta,
467                            fast_telemetry::MetricLabels::dynamic_pairs(labels),
468                            current as i64,
469                        );
470                    });
471                });
472                prom_dynamic_reserve_exprs.push(quote! {
473                    self.#field_name.cardinality().saturating_mul(
474                        #prom_metric_name.len()
475                            + #PROM_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
476                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
477                    )
478                });
479                dogstatsd_dynamic_reserve_exprs.push(quote! {
480                    self.#field_name.cardinality().saturating_mul(
481                        #statsd_metric_name.len()
482                            + #DOGSTATSD_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
483                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
484                    )
485                });
486                dogstatsd_delta_dynamic_reserve_exprs.push(quote! {
487                    self.#field_name.cardinality().saturating_mul(
488                        #statsd_metric_name.len()
489                            + #DOGSTATSD_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
490                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
491                    )
492                });
493                dogstatsd_dynamic_tag_line_exprs.push(quote! { self.#field_name.cardinality() });
494                dogstatsd_delta_dynamic_tag_line_exprs
495                    .push(quote! { self.#field_name.cardinality() });
496                state_label_count_exprs.push(quote! { self.#field_name.len() });
497                state_fields.push(quote! { #field_name: std::collections::HashMap<fast_telemetry::DynamicLabelSet, isize>, });
498                state_inits.push(quote! { #field_name: 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, current| {
510                        let key = fast_telemetry::DynamicLabelSet::from_canonical_pairs(labels);
511                        current_keys.insert(key.clone());
512                        let previous = state.#field_name.get(&key).copied().unwrap_or(0);
513                        let delta = current - previous;
514                        state.#field_name.insert(key, current);
515                        fast_telemetry::__macro_support::__write_dogstatsd_dynamic_pairs(
516                            output,
517                            #statsd_metric_name,
518                            delta,
519                            "c",
520                            labels,
521                            tags,
522                        );
523                    });
524                    // Prune state entries for evicted label sets
525                    state.#field_name.retain(|k, _| current_keys.contains(k));
526                });
527            }
528            MetricKind::DynamicDistribution => {
529                let meta = visitor_meta(
530                    &prom_metric_name,
531                    &help,
532                    quote! { fast_telemetry::MetricKind::Distribution },
533                );
534                let buckets_state_field = format_ident!("{}_buckets", field_name);
535                visitor_exports.push(quote! {
536                    let __ft_meta = #meta;
537                    let __ft_overflow = self.#field_name.overflow_count();
538                    if __ft_overflow > 0 {
539                        visitor.dynamic_overflow(__ft_meta, __ft_overflow);
540                    }
541                    self.#field_name.visit_series(|labels, _count, _sum, snapshot| {
542                        visitor.distribution(
543                            __ft_meta,
544                            fast_telemetry::MetricLabels::dynamic_pairs(labels),
545                            &snapshot,
546                        );
547                    });
548                });
549                prom_dynamic_reserve_exprs.push(quote! {
550                    self.#field_name.cardinality().saturating_mul(
551                        #prom_metric_name.len()
552                            + #PROM_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES
553                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
554                    )
555                });
556                dogstatsd_dynamic_reserve_exprs.push(quote! {
557                    self.#field_name.cardinality().saturating_mul(
558                        #statsd_metric_name.len()
559                            + #DOGSTATSD_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES
560                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
561                    )
562                });
563                dogstatsd_delta_dynamic_reserve_exprs.push(quote! {
564                    self.#field_name.cardinality().saturating_mul(
565                        #statsd_metric_name.len()
566                            + #DOGSTATSD_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES
567                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
568                    )
569                });
570                dogstatsd_dynamic_tag_line_exprs.push(quote! { self.#field_name.cardinality() });
571                dogstatsd_delta_dynamic_tag_line_exprs
572                    .push(quote! { self.#field_name.cardinality() });
573                state_label_count_exprs.push(quote! {
574                    self.#buckets_state_field.len()
575                });
576                state_fields.push(quote! { #buckets_state_field: std::collections::HashMap<fast_telemetry::DynamicLabelSet, [u64; 65]>, });
577                state_inits
578                    .push(quote! { #buckets_state_field: std::collections::HashMap::new(), });
579                delta_exports.push(quote! {
580                    let overflow = self.#field_name.overflow_count();
581                    if overflow > 0 {
582                        log::warn!(
583                            "fast-telemetry: {} hit cardinality cap, {} records routed to overflow",
584                            #statsd_metric_name,
585                            overflow
586                        );
587                    }
588                    let mut current_keys = std::collections::HashSet::new();
589                    self.#field_name.visit_series(|labels, _count, _sum, snap| {
590                        let key = fast_telemetry::DynamicLabelSet::from_canonical_pairs(labels);
591                        current_keys.insert(key.clone());
592                        let prev = state.#buckets_state_field.entry(key).or_insert([0u64; 65]);
593                        fast_telemetry::__macro_support::__write_dogstatsd_distribution_delta_dynamic_pairs(
594                            output, #statsd_metric_name, &snap, prev, labels, tags
595                        );
596                    });
597                    // Prune state entries for evicted label sets
598                    state.#buckets_state_field.retain(|k, _| current_keys.contains(k));
599                });
600            }
601            MetricKind::DynamicGauge => {
602                let meta = visitor_meta(
603                    &prom_metric_name,
604                    &help,
605                    quote! { fast_telemetry::MetricKind::Gauge },
606                );
607                visitor_exports.push(quote! {
608                    let __ft_meta = #meta;
609                    let __ft_overflow = self.#field_name.overflow_count();
610                    if __ft_overflow > 0 {
611                        visitor.dynamic_overflow(__ft_meta, __ft_overflow);
612                    }
613                    self.#field_name.visit_series(|labels, current| {
614                        visitor.gauge_f64(
615                            __ft_meta,
616                            fast_telemetry::MetricLabels::dynamic_pairs(labels),
617                            current,
618                        );
619                    });
620                });
621                prom_dynamic_reserve_exprs.push(quote! {
622                    self.#field_name.cardinality().saturating_mul(
623                        #prom_metric_name.len()
624                            + #PROM_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
625                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
626                    )
627                });
628                dogstatsd_dynamic_reserve_exprs.push(quote! {
629                    self.#field_name.cardinality().saturating_mul(
630                        #statsd_metric_name.len()
631                            + #DOGSTATSD_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
632                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
633                    )
634                });
635                dogstatsd_delta_dynamic_reserve_exprs.push(quote! {
636                    self.#field_name.cardinality().saturating_mul(
637                        #statsd_metric_name.len()
638                            + #DOGSTATSD_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
639                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
640                    )
641                });
642                dogstatsd_dynamic_tag_line_exprs.push(quote! { self.#field_name.cardinality() });
643                dogstatsd_delta_dynamic_tag_line_exprs
644                    .push(quote! { self.#field_name.cardinality() });
645                state_label_count_exprs.push(quote! { 0usize });
646                // Gauges are point-in-time, no delta tracking needed (always export current value)
647                delta_exports.push(quote! {
648                    let overflow = self.#field_name.overflow_count();
649                    if overflow > 0 {
650                        log::warn!(
651                            "fast-telemetry: {} hit cardinality cap, {} records routed to overflow",
652                            #statsd_metric_name,
653                            overflow
654                        );
655                    }
656                    fast_telemetry::DogStatsDExport::export_dogstatsd(&self.#field_name, output, #statsd_metric_name, tags);
657                });
658            }
659            MetricKind::DynamicGaugeI64 => {
660                let meta = visitor_meta(
661                    &prom_metric_name,
662                    &help,
663                    quote! { fast_telemetry::MetricKind::Gauge },
664                );
665                visitor_exports.push(quote! {
666                    let __ft_meta = #meta;
667                    let __ft_overflow = self.#field_name.overflow_count();
668                    if __ft_overflow > 0 {
669                        visitor.dynamic_overflow(__ft_meta, __ft_overflow);
670                    }
671                    self.#field_name.visit_series(|labels, current| {
672                        visitor.gauge_i64(
673                            __ft_meta,
674                            fast_telemetry::MetricLabels::dynamic_pairs(labels),
675                            current,
676                        );
677                    });
678                });
679                prom_dynamic_reserve_exprs.push(quote! {
680                    self.#field_name.cardinality().saturating_mul(
681                        #prom_metric_name.len()
682                            + #PROM_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
683                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
684                    )
685                });
686                dogstatsd_dynamic_reserve_exprs.push(quote! {
687                    self.#field_name.cardinality().saturating_mul(
688                        #statsd_metric_name.len()
689                            + #DOGSTATSD_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
690                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
691                    )
692                });
693                dogstatsd_delta_dynamic_reserve_exprs.push(quote! {
694                    self.#field_name.cardinality().saturating_mul(
695                        #statsd_metric_name.len()
696                            + #DOGSTATSD_DYNAMIC_SIMPLE_SERIES_OVERHEAD_BYTES
697                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
698                    )
699                });
700                dogstatsd_dynamic_tag_line_exprs.push(quote! { self.#field_name.cardinality() });
701                dogstatsd_delta_dynamic_tag_line_exprs
702                    .push(quote! { self.#field_name.cardinality() });
703                state_label_count_exprs.push(quote! { 0usize });
704                // i64 Gauges are point-in-time, no delta tracking needed (always export current value)
705                delta_exports.push(quote! {
706                    let overflow = self.#field_name.overflow_count();
707                    if overflow > 0 {
708                        log::warn!(
709                            "fast-telemetry: {} hit cardinality cap, {} records routed to overflow",
710                            #statsd_metric_name,
711                            overflow
712                        );
713                    }
714                    fast_telemetry::DogStatsDExport::export_dogstatsd(&self.#field_name, output, #statsd_metric_name, tags);
715                });
716            }
717            MetricKind::DynamicHistogram => {
718                let meta = visitor_meta(
719                    &prom_metric_name,
720                    &help,
721                    quote! { fast_telemetry::MetricKind::Histogram },
722                );
723                let count_state_field = format_ident!("{}_count", field_name);
724                let sum_state_field = format_ident!("{}_sum", field_name);
725                let count_metric_name = format!("{}.count", statsd_metric_name);
726                let sum_metric_name = format!("{}.sum", statsd_metric_name);
727                visitor_exports.push(quote! {
728                    let __ft_meta = #meta;
729                    let __ft_overflow = self.#field_name.overflow_count();
730                    if __ft_overflow > 0 {
731                        visitor.dynamic_overflow(__ft_meta, __ft_overflow);
732                    }
733                    self.#field_name.visit_series(|labels, series| {
734                        visitor.histogram(
735                            __ft_meta,
736                            fast_telemetry::MetricLabels::dynamic_pairs(labels),
737                            &series,
738                        );
739                    });
740                });
741                prom_dynamic_reserve_exprs.push(quote! {
742                    self.#field_name.cardinality().saturating_mul(
743                        #prom_metric_name.len()
744                            + #PROM_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES
745                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
746                    )
747                });
748                dogstatsd_dynamic_reserve_exprs.push(quote! {
749                    self.#field_name.cardinality().saturating_mul(
750                        #statsd_metric_name.len()
751                            + #DOGSTATSD_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES
752                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
753                    )
754                });
755                dogstatsd_delta_dynamic_reserve_exprs.push(quote! {
756                    self.#field_name.cardinality().saturating_mul(
757                        #statsd_metric_name.len()
758                            + #DOGSTATSD_DYNAMIC_COMPLEX_SERIES_OVERHEAD_BYTES
759                            + (#DYNAMIC_LABELS_PER_SERIES_ESTIMATE * #DYNAMIC_LABEL_PAIR_ESTIMATE_BYTES)
760                    )
761                });
762                dogstatsd_dynamic_tag_line_exprs.push(quote! { self.#field_name.cardinality() });
763                dogstatsd_delta_dynamic_tag_line_exprs
764                    .push(quote! { self.#field_name.cardinality() });
765                state_label_count_exprs.push(quote! {
766                    core::cmp::max(self.#count_state_field.len(), self.#sum_state_field.len())
767                });
768                state_fields.push(quote! { #count_state_field: std::collections::HashMap<fast_telemetry::DynamicLabelSet, u64>, });
769                state_fields.push(quote! { #sum_state_field: std::collections::HashMap<fast_telemetry::DynamicLabelSet, u64>, });
770                state_inits.push(quote! { #count_state_field: std::collections::HashMap::new(), });
771                state_inits.push(quote! { #sum_state_field: std::collections::HashMap::new(), });
772                delta_exports.push(quote! {
773                    let overflow = self.#field_name.overflow_count();
774                    if overflow > 0 {
775                        log::warn!(
776                            "fast-telemetry: {} hit cardinality cap, {} records routed to overflow",
777                            #statsd_metric_name,
778                            overflow
779                        );
780                    }
781                    let mut current_keys = std::collections::HashSet::new();
782                    self.#field_name.visit_series(|labels, series| {
783                        let key = fast_telemetry::DynamicLabelSet::from_canonical_pairs(labels);
784                        current_keys.insert(key.clone());
785                        let current_count = series.count();
786                        let current_sum = series.sum();
787                        let previous_count = state.#count_state_field.get(&key).copied().unwrap_or(0);
788                        let previous_sum = state.#sum_state_field.get(&key).copied().unwrap_or(0);
789                        let delta_count = if current_count >= previous_count {
790                            current_count - previous_count
791                        } else {
792                            current_count
793                        };
794                        let delta_sum = if current_sum >= previous_sum {
795                            current_sum - previous_sum
796                        } else {
797                            current_sum
798                        };
799                        state.#count_state_field.insert(key.clone(), current_count);
800                        state.#sum_state_field.insert(key, current_sum);
801                        fast_telemetry::__macro_support::__write_dogstatsd_dynamic_pairs(
802                            output,
803                            #count_metric_name,
804                            delta_count,
805                            "c",
806                            labels,
807                            tags,
808                        );
809                        fast_telemetry::__macro_support::__write_dogstatsd_dynamic_pairs(
810                            output,
811                            #sum_metric_name,
812                            delta_sum,
813                            "c",
814                            labels,
815                            tags,
816                        );
817                    });
818                    // Prune state entries for evicted label sets
819                    state.#count_state_field.retain(|k, _| current_keys.contains(k));
820                    state.#sum_state_field.retain(|k, _| current_keys.contains(k));
821                });
822            }
823            MetricKind::GaugeF64 | MetricKind::MaxGaugeF64 | MetricKind::MinGaugeF64 => {
824                let meta = visitor_meta(
825                    &prom_metric_name,
826                    &help,
827                    quote! { fast_telemetry::MetricKind::Gauge },
828                );
829                visitor_exports.push(quote! {
830                    visitor.gauge_f64(#meta, fast_telemetry::MetricLabels::none(), self.#field_name.get());
831                });
832                state_label_count_exprs.push(quote! { 0usize });
833                // Gauges are point-in-time, no delta tracking needed (always export current value)
834                delta_exports.push(quote! {
835                    fast_telemetry::DogStatsDExport::export_dogstatsd(&self.#field_name, output, #statsd_metric_name, tags);
836                });
837            }
838            MetricKind::Gauge | MetricKind::MaxGauge | MetricKind::MinGauge => {
839                let meta = visitor_meta(
840                    &prom_metric_name,
841                    &help,
842                    quote! { fast_telemetry::MetricKind::Gauge },
843                );
844                visitor_exports.push(quote! {
845                    visitor.gauge_i64(#meta, fast_telemetry::MetricLabels::none(), self.#field_name.get());
846                });
847                state_label_count_exprs.push(quote! { 0usize });
848                // Gauges are point-in-time, no delta tracking needed (always export current value)
849                delta_exports.push(quote! {
850                    fast_telemetry::DogStatsDExport::export_dogstatsd(&self.#field_name, output, #statsd_metric_name, tags);
851                });
852            }
853            MetricKind::Histogram => {
854                let meta = visitor_meta(
855                    &prom_metric_name,
856                    &help,
857                    quote! { fast_telemetry::MetricKind::Histogram },
858                );
859                visitor_exports.push(quote! {
860                    visitor.histogram(#meta, fast_telemetry::MetricLabels::none(), &self.#field_name);
861                });
862                let count_state_field = format_ident!("{}_count", field_name);
863                let sum_state_field = format_ident!("{}_sum", field_name);
864                let count_metric_name = format!("{}.count", statsd_metric_name);
865                let sum_metric_name = format!("{}.sum", statsd_metric_name);
866                state_label_count_exprs.push(quote! { 0usize });
867                state_fields.push(quote! { #count_state_field: u64, });
868                state_fields.push(quote! { #sum_state_field: u64, });
869                state_inits.push(quote! { #count_state_field: 0, });
870                state_inits.push(quote! { #sum_state_field: 0, });
871                delta_exports.push(quote! {
872                    let current_count = self.#field_name.count();
873                    let current_sum = self.#field_name.sum();
874                    let delta_count = if current_count >= state.#count_state_field {
875                        current_count - state.#count_state_field
876                    } else {
877                        current_count
878                    };
879                    let delta_sum = if current_sum >= state.#sum_state_field {
880                        current_sum - state.#sum_state_field
881                    } else {
882                        current_sum
883                    };
884                    state.#count_state_field = current_count;
885                    state.#sum_state_field = current_sum;
886                    fast_telemetry::__macro_support::__write_dogstatsd(output, #count_metric_name, delta_count, "c", tags);
887                    fast_telemetry::__macro_support::__write_dogstatsd(output, #sum_metric_name, delta_sum, "c", tags);
888                });
889            }
890            MetricKind::SampledTimer => {
891                let calls_metric_name = format!("{}_calls", prom_metric_name);
892                let samples_metric_name = format!("{}_samples", prom_metric_name);
893                let calls_help = format!("{} total calls", help);
894                let samples_help = format!("{} sampled latency in nanoseconds", help);
895                let calls_meta = visitor_meta(
896                    &calls_metric_name,
897                    &calls_help,
898                    quote! { fast_telemetry::MetricKind::Counter },
899                );
900                let samples_meta = visitor_meta(
901                    &samples_metric_name,
902                    &samples_help,
903                    quote! { fast_telemetry::MetricKind::Histogram },
904                );
905                let calls_state_field = format_ident!("{}_calls", field_name);
906                let count_state_field = format_ident!("{}_sample_count", field_name);
907                let sum_state_field = format_ident!("{}_sample_sum", field_name);
908                let calls_metric_name = format!("{}.calls", statsd_metric_name);
909                let count_metric_name = format!("{}.samples.count", statsd_metric_name);
910                let sum_metric_name = format!("{}.samples.sum", statsd_metric_name);
911                visitor_exports.push(quote! {
912                    visitor.counter(#calls_meta, fast_telemetry::MetricLabels::none(), self.#field_name.calls() as i64);
913                    visitor.histogram(#samples_meta, fast_telemetry::MetricLabels::none(), self.#field_name.histogram());
914                });
915                state_label_count_exprs.push(quote! { 0usize });
916                state_fields.push(quote! { #calls_state_field: u64, });
917                state_fields.push(quote! { #count_state_field: u64, });
918                state_fields.push(quote! { #sum_state_field: u64, });
919                state_inits.push(quote! { #calls_state_field: 0, });
920                state_inits.push(quote! { #count_state_field: 0, });
921                state_inits.push(quote! { #sum_state_field: 0, });
922                delta_exports.push(quote! {
923                    let current_calls = self.#field_name.calls();
924                    let current_count = self.#field_name.sample_count();
925                    let current_sum = self.#field_name.sample_sum_nanos();
926                    let delta_calls = if current_calls >= state.#calls_state_field {
927                        current_calls - state.#calls_state_field
928                    } else {
929                        current_calls
930                    };
931                    let delta_count = if current_count >= state.#count_state_field {
932                        current_count - state.#count_state_field
933                    } else {
934                        current_count
935                    };
936                    let delta_sum = if current_sum >= state.#sum_state_field {
937                        current_sum - state.#sum_state_field
938                    } else {
939                        current_sum
940                    };
941                    state.#calls_state_field = current_calls;
942                    state.#count_state_field = current_count;
943                    state.#sum_state_field = current_sum;
944                    fast_telemetry::__macro_support::__write_dogstatsd(output, #calls_metric_name, delta_calls, "c", tags);
945                    fast_telemetry::__macro_support::__write_dogstatsd(output, #count_metric_name, delta_count, "c", tags);
946                    fast_telemetry::__macro_support::__write_dogstatsd(output, #sum_metric_name, delta_sum, "c", tags);
947                });
948            }
949            MetricKind::LabeledCounter(label_ty) => {
950                let meta = visitor_meta(
951                    &prom_metric_name,
952                    &help,
953                    quote! { fast_telemetry::MetricKind::Counter },
954                );
955                let label = visitor_label(quote! { label });
956                visitor_exports.push(quote! {
957                    let __ft_meta = #meta;
958                    for (label, value) in self.#field_name.iter() {
959                        visitor.counter(
960                            __ft_meta,
961                            fast_telemetry::MetricLabels::one(#label),
962                            value as i64,
963                        );
964                    }
965                });
966                state_label_count_exprs.push(quote! { 0usize });
967                state_fields.push(quote! { #field_name: Vec<isize>, });
968                state_inits.push(quote! {
969                    #field_name: vec![0; <#label_ty as fast_telemetry::LabelEnum>::CARDINALITY],
970                });
971                delta_exports.push(quote! {
972                    for idx in 0..<#label_ty as fast_telemetry::LabelEnum>::CARDINALITY {
973                        let label = <#label_ty as fast_telemetry::LabelEnum>::from_index(idx);
974                        let current = self.#field_name.get(label);
975                        let delta = current - state.#field_name[idx];
976                        state.#field_name[idx] = current;
977                        fast_telemetry::__macro_support::__write_dogstatsd_with_label(
978                            output,
979                            #statsd_metric_name,
980                            delta,
981                            "c",
982                            <#label_ty as fast_telemetry::LabelEnum>::LABEL_NAME,
983                            <#label_ty as fast_telemetry::LabelEnum>::variant_name(label),
984                            tags,
985                        );
986                    }
987                });
988            }
989            MetricKind::LabeledGauge => {
990                let meta = visitor_meta(
991                    &prom_metric_name,
992                    &help,
993                    quote! { fast_telemetry::MetricKind::Gauge },
994                );
995                let label = visitor_label(quote! { label });
996                visitor_exports.push(quote! {
997                    let __ft_meta = #meta;
998                    for (label, value) in self.#field_name.iter() {
999                        visitor.gauge_i64(
1000                            __ft_meta,
1001                            fast_telemetry::MetricLabels::one(#label),
1002                            value,
1003                        );
1004                    }
1005                });
1006                state_label_count_exprs.push(quote! { 0usize });
1007                delta_exports.push(quote! {
1008                    fast_telemetry::DogStatsDExport::export_dogstatsd(&self.#field_name, output, #statsd_metric_name, tags);
1009                });
1010            }
1011            MetricKind::LabeledHistogram(label_ty) => {
1012                let meta = visitor_meta(
1013                    &prom_metric_name,
1014                    &help,
1015                    quote! { fast_telemetry::MetricKind::Histogram },
1016                );
1017                let count_state_field = format_ident!("{}_count", field_name);
1018                let sum_state_field = format_ident!("{}_sum", field_name);
1019                let count_metric_name = format!("{}.count", statsd_metric_name);
1020                let sum_metric_name = format!("{}.sum", statsd_metric_name);
1021                let label = visitor_label(quote! { label });
1022                visitor_exports.push(quote! {
1023                    let __ft_meta = #meta;
1024                    for (label, histogram) in self.#field_name.iter() {
1025                        visitor.histogram(
1026                            __ft_meta,
1027                            fast_telemetry::MetricLabels::one(#label),
1028                            histogram,
1029                        );
1030                    }
1031                });
1032                state_label_count_exprs.push(quote! { 0usize });
1033                state_fields.push(quote! { #count_state_field: Vec<u64>, });
1034                state_fields.push(quote! { #sum_state_field: Vec<u64>, });
1035                state_inits.push(quote! {
1036                    #count_state_field: vec![0; <#label_ty as fast_telemetry::LabelEnum>::CARDINALITY],
1037                });
1038                state_inits.push(quote! {
1039                    #sum_state_field: vec![0; <#label_ty as fast_telemetry::LabelEnum>::CARDINALITY],
1040                });
1041                delta_exports.push(quote! {
1042                    for idx in 0..<#label_ty as fast_telemetry::LabelEnum>::CARDINALITY {
1043                        let label = <#label_ty as fast_telemetry::LabelEnum>::from_index(idx);
1044                        let current_count = self.#field_name.get(label).count();
1045                        let current_sum = self.#field_name.get(label).sum();
1046                        let delta_count = if current_count >= state.#count_state_field[idx] {
1047                            current_count - state.#count_state_field[idx]
1048                        } else {
1049                            current_count
1050                        };
1051                        let delta_sum = if current_sum >= state.#sum_state_field[idx] {
1052                            current_sum - state.#sum_state_field[idx]
1053                        } else {
1054                            current_sum
1055                        };
1056                        state.#count_state_field[idx] = current_count;
1057                        state.#sum_state_field[idx] = current_sum;
1058                        fast_telemetry::__macro_support::__write_dogstatsd_with_label(
1059                            output,
1060                            #count_metric_name,
1061                            delta_count,
1062                            "c",
1063                            <#label_ty as fast_telemetry::LabelEnum>::LABEL_NAME,
1064                            <#label_ty as fast_telemetry::LabelEnum>::variant_name(label),
1065                            tags,
1066                        );
1067                        fast_telemetry::__macro_support::__write_dogstatsd_with_label(
1068                            output,
1069                            #sum_metric_name,
1070                            delta_sum,
1071                            "c",
1072                            <#label_ty as fast_telemetry::LabelEnum>::LABEL_NAME,
1073                            <#label_ty as fast_telemetry::LabelEnum>::variant_name(label),
1074                            tags,
1075                        );
1076                    }
1077                });
1078            }
1079            MetricKind::LabeledSampledTimer(label_ty) => {
1080                let calls_prom_metric_name = format!("{}_calls", prom_metric_name);
1081                let samples_prom_metric_name = format!("{}_samples", prom_metric_name);
1082                let calls_help = format!("{} total calls", help);
1083                let samples_help = format!("{} sampled latency in nanoseconds", help);
1084                let calls_meta = visitor_meta(
1085                    &calls_prom_metric_name,
1086                    &calls_help,
1087                    quote! { fast_telemetry::MetricKind::Counter },
1088                );
1089                let samples_meta = visitor_meta(
1090                    &samples_prom_metric_name,
1091                    &samples_help,
1092                    quote! { fast_telemetry::MetricKind::Histogram },
1093                );
1094                let calls_state_field = format_ident!("{}_calls", field_name);
1095                let count_state_field = format_ident!("{}_sample_count", field_name);
1096                let sum_state_field = format_ident!("{}_sample_sum", field_name);
1097                let calls_metric_name = format!("{}.calls", statsd_metric_name);
1098                let count_metric_name = format!("{}.samples.count", statsd_metric_name);
1099                let sum_metric_name = format!("{}.samples.sum", statsd_metric_name);
1100                let label = visitor_label(quote! { label });
1101                visitor_exports.push(quote! {
1102                    let __ft_calls_meta = #calls_meta;
1103                    let __ft_samples_meta = #samples_meta;
1104                    for (label, calls, histogram) in self.#field_name.iter() {
1105                        let __ft_labels = fast_telemetry::MetricLabels::one(#label);
1106                        visitor.counter(__ft_calls_meta, __ft_labels, calls.sum() as i64);
1107                        visitor.histogram(__ft_samples_meta, __ft_labels, histogram);
1108                    }
1109                });
1110                state_label_count_exprs.push(quote! { 0usize });
1111                state_fields.push(quote! { #calls_state_field: Vec<u64>, });
1112                state_fields.push(quote! { #count_state_field: Vec<u64>, });
1113                state_fields.push(quote! { #sum_state_field: Vec<u64>, });
1114                state_inits.push(quote! {
1115                    #calls_state_field: vec![0; <#label_ty as fast_telemetry::LabelEnum>::CARDINALITY],
1116                });
1117                state_inits.push(quote! {
1118                    #count_state_field: vec![0; <#label_ty as fast_telemetry::LabelEnum>::CARDINALITY],
1119                });
1120                state_inits.push(quote! {
1121                    #sum_state_field: vec![0; <#label_ty as fast_telemetry::LabelEnum>::CARDINALITY],
1122                });
1123                delta_exports.push(quote! {
1124                    for idx in 0..<#label_ty as fast_telemetry::LabelEnum>::CARDINALITY {
1125                        let label = <#label_ty as fast_telemetry::LabelEnum>::from_index(idx);
1126                        let current_calls = self.#field_name.calls(label);
1127                        let current_count = self.#field_name.sample_count(label);
1128                        let current_sum = self.#field_name.sample_sum_nanos(label);
1129                        let delta_calls = if current_calls >= state.#calls_state_field[idx] {
1130                            current_calls - state.#calls_state_field[idx]
1131                        } else {
1132                            current_calls
1133                        };
1134                        let delta_count = if current_count >= state.#count_state_field[idx] {
1135                            current_count - state.#count_state_field[idx]
1136                        } else {
1137                            current_count
1138                        };
1139                        let delta_sum = if current_sum >= state.#sum_state_field[idx] {
1140                            current_sum - state.#sum_state_field[idx]
1141                        } else {
1142                            current_sum
1143                        };
1144                        state.#calls_state_field[idx] = current_calls;
1145                        state.#count_state_field[idx] = current_count;
1146                        state.#sum_state_field[idx] = current_sum;
1147                        fast_telemetry::__macro_support::__write_dogstatsd_with_label(
1148                            output,
1149                            #calls_metric_name,
1150                            delta_calls,
1151                            "c",
1152                            <#label_ty as fast_telemetry::LabelEnum>::LABEL_NAME,
1153                            <#label_ty as fast_telemetry::LabelEnum>::variant_name(label),
1154                            tags,
1155                        );
1156                        fast_telemetry::__macro_support::__write_dogstatsd_with_label(
1157                            output,
1158                            #count_metric_name,
1159                            delta_count,
1160                            "c",
1161                            <#label_ty as fast_telemetry::LabelEnum>::LABEL_NAME,
1162                            <#label_ty as fast_telemetry::LabelEnum>::variant_name(label),
1163                            tags,
1164                        );
1165                        fast_telemetry::__macro_support::__write_dogstatsd_with_label(
1166                            output,
1167                            #sum_metric_name,
1168                            delta_sum,
1169                            "c",
1170                            <#label_ty as fast_telemetry::LabelEnum>::LABEL_NAME,
1171                            <#label_ty as fast_telemetry::LabelEnum>::variant_name(label),
1172                            tags,
1173                        );
1174                    }
1175                });
1176            }
1177        }
1178    }
1179
1180    let otlp_method = if enable_otlp {
1181        quote! {
1182            /// Export all metrics as OTLP protobuf `Metric` messages (cumulative temporality).
1183            ///
1184            /// `time_unix_nano` is a shared timestamp for all data points in this export cycle.
1185            /// Use `fast_telemetry::otlp::now_nanos()` to get the current time.
1186            ///
1187            /// Requires the `otlp` feature on the `fast-telemetry` dependency.
1188            pub fn export_otlp(&self, metrics: &mut Vec<fast_telemetry::otlp::pb::Metric>, time_unix_nano: u64) {
1189                #(#otlp_exports)*
1190            }
1191        }
1192    } else {
1193        quote! {}
1194    };
1195
1196    let clickhouse_method = if enable_clickhouse {
1197        quote! {
1198            /// Export all metrics as ClickHouse OTel-standard rows.
1199            ///
1200            /// `time_unix_nano` is a shared timestamp for all rows in this export cycle.
1201            /// Requires the `clickhouse` feature on the `fast-telemetry` dependency.
1202            pub fn export_clickhouse(
1203                &self,
1204                batch: &mut fast_telemetry::clickhouse::ClickHouseMetricBatch,
1205                time_unix_nano: u64,
1206            ) {
1207                #(#clickhouse_exports)*
1208            }
1209        }
1210    } else {
1211        quote! {}
1212    };
1213
1214    let expanded = quote! {
1215        /// State for tracking DogStatsD delta values.
1216        #vis struct #state_name {
1217            #(#state_fields)*
1218        }
1219
1220        impl #state_name {
1221            pub fn new() -> Self {
1222                Self {
1223                    #(#state_inits)*
1224                }
1225            }
1226
1227            /// Total number of dynamic label sets currently retained in delta state maps.
1228            pub fn tracked_label_sets(&self) -> usize {
1229                0usize #(+ #state_label_count_exprs)*
1230            }
1231        }
1232
1233        impl Default for #state_name {
1234            fn default() -> Self {
1235                Self::new()
1236            }
1237        }
1238
1239        impl #name {
1240            /// Export all metrics in Prometheus text exposition format.
1241            pub fn export_prometheus(&self, output: &mut String) {
1242                let __ft_prom_dynamic_reserve = 0usize #(+ #prom_dynamic_reserve_exprs)*;
1243                output.reserve(#prom_reserve_hint + __ft_prom_dynamic_reserve);
1244                #(#prometheus_exports)*
1245            }
1246
1247            /// Export all metrics in DogStatsD format (cumulative).
1248            ///
1249            /// - `output`: String buffer to append to
1250            /// - `tags`: Additional tags to include (e.g., `&[("env", "prod")]`)
1251            pub fn export_dogstatsd(&self, output: &mut String, tags: &[(&str, &str)]) {
1252                let __ft_tag_bytes = if tags.is_empty() {
1253                    0usize
1254                } else {
1255                    #DOGSTATSD_TAG_PREFIX_BYTES
1256                        + tags.iter().map(|(k, v)| k.len() + v.len() + #DOGSTATSD_TAG_PAIR_OVERHEAD_BYTES).sum::<usize>()
1257                };
1258                let __ft_dynamic_reserve = 0usize #(+ #dogstatsd_dynamic_reserve_exprs)*;
1259                let __ft_dynamic_tag_lines = 0usize #(+ #dogstatsd_dynamic_tag_line_exprs)*;
1260                output.reserve(
1261                    #dogstatsd_reserve_hint
1262                        + __ft_dynamic_reserve
1263                        + __ft_tag_bytes.saturating_mul(#dogstatsd_tag_line_hint + __ft_dynamic_tag_lines)
1264                );
1265                #(#dogstatsd_exports)*
1266            }
1267
1268            /// Export all metrics in DogStatsD format using per-sink delta temporality.
1269            ///
1270            /// Requires a mutable state object to track previous values.
1271            pub fn export_dogstatsd_delta(
1272                &self,
1273                output: &mut String,
1274                tags: &[(&str, &str)],
1275                state: &mut #state_name,
1276            ) {
1277                let __ft_tag_bytes = if tags.is_empty() {
1278                    0usize
1279                } else {
1280                    #DOGSTATSD_TAG_PREFIX_BYTES
1281                        + tags.iter().map(|(k, v)| k.len() + v.len() + #DOGSTATSD_TAG_PAIR_OVERHEAD_BYTES).sum::<usize>()
1282                };
1283                let __ft_dynamic_reserve = 0usize #(+ #dogstatsd_delta_dynamic_reserve_exprs)*;
1284                let __ft_dynamic_tag_lines = 0usize #(+ #dogstatsd_delta_dynamic_tag_line_exprs)*;
1285                output.reserve(
1286                    #dogstatsd_delta_reserve_hint
1287                        + __ft_dynamic_reserve
1288                        + __ft_tag_bytes.saturating_mul(#dogstatsd_delta_tag_line_hint + __ft_dynamic_tag_lines)
1289                );
1290                #(#delta_exports)*
1291            }
1292
1293            /// Export all metrics in DogStatsD format with configurable temporality.
1294            pub fn export_dogstatsd_with_temporality(
1295                &self,
1296                output: &mut String,
1297                tags: &[(&str, &str)],
1298                temporality: fast_telemetry::Temporality,
1299                state: &mut #state_name,
1300            ) {
1301                match temporality {
1302                    fast_telemetry::Temporality::Cumulative => self.export_dogstatsd(output, tags),
1303                    fast_telemetry::Temporality::Delta => self.export_dogstatsd_delta(output, tags, state),
1304                }
1305            }
1306
1307            /// Visit all metrics as structured cumulative observations.
1308            pub fn visit_metrics<V: fast_telemetry::MetricVisitor + ?Sized>(&self, visitor: &mut V) {
1309                #(#visitor_exports)*
1310            }
1311
1312            #otlp_method
1313            #clickhouse_method
1314        }
1315    };
1316
1317    Ok(TokenStream::from(expanded))
1318}
1319
1320/// Derive macro for implementing `LabelEnum` on enums.
1321///
1322/// Automatically generates all required trait methods from the enum definition.
1323/// Converts variant names to snake_case for Prometheus label values.
1324///
1325/// # Attributes
1326///
1327/// - `#[label_name = "..."]` (required on enum): The Prometheus label name
1328/// - `#[label = "..."]` (optional on variant): Override the snake_case variant name
1329///
1330/// # Example
1331///
1332/// ```ignore
1333/// use fast_telemetry_macros::LabelEnum;
1334///
1335/// #[derive(LabelEnum)]
1336/// #[label_name = "method"]
1337/// enum HttpMethod {
1338///     Get,
1339///     Post,
1340///     Put,
1341///     Delete,
1342///     #[label = "other"]
1343///     Unknown,
1344/// }
1345///
1346/// // Generates:
1347/// // - CARDINALITY = 5
1348/// // - LABEL_NAME = "method"
1349/// // - as_index() returns 0, 1, 2, 3, 4
1350/// // - from_index() returns Get, Post, Put, Delete, Unknown
1351/// // - variant_name() returns "get", "post", "put", "delete", "other"
1352/// // ```
1353#[proc_macro_derive(LabelEnum, attributes(label_name, label))]
1354pub fn derive_label_enum(input: TokenStream) -> TokenStream {
1355    let input = parse_macro_input!(input as DeriveInput);
1356    match derive_label_enum_impl(input) {
1357        Ok(ts) => ts,
1358        Err(err) => err.to_compile_error().into(),
1359    }
1360}
1361
1362fn derive_label_enum_impl(input: DeriveInput) -> syn::Result<TokenStream> {
1363    let name = &input.ident;
1364
1365    // Extract label_name from enum attributes (required)
1366    let label_name = extract_label_name(&input.attrs).ok_or_else(|| {
1367        syn::Error::new_spanned(
1368            name,
1369            "LabelEnum requires #[label_name = \"...\"] attribute on the enum",
1370        )
1371    })?;
1372
1373    // Get enum variants
1374    let variants = match &input.data {
1375        Data::Enum(data) => &data.variants,
1376        _ => {
1377            return Err(syn::Error::new_spanned(
1378                &input,
1379                "LabelEnum can only be derived for enums",
1380            ));
1381        }
1382    };
1383    if variants.is_empty() {
1384        return Err(syn::Error::new_spanned(
1385            name,
1386            "LabelEnum requires at least one variant",
1387        ));
1388    }
1389
1390    let cardinality = variants.len();
1391
1392    // Generate as_index match arms
1393    let as_index_arms: Vec<_> = variants
1394        .iter()
1395        .enumerate()
1396        .map(|(idx, variant)| {
1397            let variant_ident = &variant.ident;
1398            quote! { Self::#variant_ident => #idx, }
1399        })
1400        .collect();
1401
1402    // Generate from_index match arms
1403    let from_index_arms: Vec<_> = variants
1404        .iter()
1405        .enumerate()
1406        .map(|(idx, variant)| {
1407            let variant_ident = &variant.ident;
1408            quote! { #idx => Self::#variant_ident, }
1409        })
1410        .collect();
1411
1412    // Get the last variant for the default case
1413    let last_variant = &variants[variants.len() - 1].ident;
1414
1415    // Generate variant_name match arms
1416    let variant_name_arms: Vec<_> = variants
1417        .iter()
1418        .map(|variant| {
1419            let variant_ident = &variant.ident;
1420            let label_value = extract_label_override(&variant.attrs)
1421                .unwrap_or_else(|| to_snake_case(&variant_ident.to_string()));
1422            quote! { Self::#variant_ident => #label_value, }
1423        })
1424        .collect();
1425
1426    let expanded = quote! {
1427        impl fast_telemetry::LabelEnum for #name {
1428            const CARDINALITY: usize = #cardinality;
1429            const LABEL_NAME: &'static str = #label_name;
1430
1431            fn as_index(self) -> usize {
1432                match self {
1433                    #(#as_index_arms)*
1434                }
1435            }
1436
1437            fn from_index(index: usize) -> Self {
1438                match index {
1439                    #(#from_index_arms)*
1440                    _ => Self::#last_variant,
1441                }
1442            }
1443
1444            fn variant_name(self) -> &'static str {
1445                match self {
1446                    #(#variant_name_arms)*
1447                }
1448            }
1449        }
1450    };
1451
1452    Ok(TokenStream::from(expanded))
1453}
1454
1455/// Extract #[label_name = "..."] from enum attributes.
1456fn extract_label_name(attrs: &[syn::Attribute]) -> Option<String> {
1457    for attr in attrs {
1458        if attr.path().is_ident("label_name")
1459            && let Meta::NameValue(nv) = &attr.meta
1460            && let Expr::Lit(expr_lit) = &nv.value
1461            && let Lit::Str(lit) = &expr_lit.lit
1462        {
1463            return Some(lit.value());
1464        }
1465    }
1466    None
1467}
1468
1469/// Extract #[label = "..."] from variant attributes.
1470fn extract_label_override(attrs: &[syn::Attribute]) -> Option<String> {
1471    for attr in attrs {
1472        if attr.path().is_ident("label")
1473            && let Meta::NameValue(nv) = &attr.meta
1474            && let Expr::Lit(expr_lit) = &nv.value
1475            && let Lit::Str(lit) = &expr_lit.lit
1476        {
1477            return Some(lit.value());
1478        }
1479    }
1480    None
1481}
1482
1483/// Convert PascalCase to snake_case.
1484fn to_snake_case(s: &str) -> String {
1485    let mut result = String::new();
1486    for (i, c) in s.chars().enumerate() {
1487        if c.is_uppercase() {
1488            if i > 0 {
1489                result.push('_');
1490            }
1491            for lower in c.to_lowercase() {
1492                result.push(lower);
1493            }
1494        } else {
1495            result.push(c);
1496        }
1497    }
1498    result
1499}
1500
1501fn extract_metric_prefix(attrs: &[syn::Attribute]) -> Option<String> {
1502    for attr in attrs {
1503        if attr.path().is_ident("metric_prefix")
1504            && let Meta::NameValue(nv) = &attr.meta
1505            && let Expr::Lit(expr_lit) = &nv.value
1506            && let Lit::Str(lit) = &expr_lit.lit
1507        {
1508            return Some(lit.value());
1509        }
1510    }
1511    None
1512}
1513
1514fn extract_help(attrs: &[syn::Attribute]) -> Option<String> {
1515    for attr in attrs {
1516        if attr.path().is_ident("help")
1517            && let Meta::NameValue(nv) = &attr.meta
1518            && let Expr::Lit(expr_lit) = &nv.value
1519            && let Lit::Str(lit) = &expr_lit.lit
1520        {
1521            return Some(lit.value());
1522        }
1523    }
1524    None
1525}