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