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