Skip to main content

fast_telemetry/export/text/
dogstatsd.rs

1use crate::{
2    Counter, Distribution, DynamicCounter, DynamicDistribution, DynamicGauge, DynamicGaugeI64,
3    DynamicHistogram, DynamicLabelSet, Gauge, GaugeF64, Histogram, LabelEnum, LabeledCounter,
4    LabeledGauge, LabeledHistogram, LabeledSampledTimer, MaxGauge, MaxGaugeF64, MinGauge,
5    MinGaugeF64, SampledTimer, exp_buckets::ExpBucketsSnapshot,
6};
7use std::fmt::{Display, Write as _};
8
9/// Trait for exporting a metric in DogStatsD format.
10///
11/// Format: `metric.name:value|type|#tag1:value1,tag2:value2`
12///
13/// Types:
14/// - `c` - counter (increment)
15/// - `g` - gauge (point-in-time value)
16/// - `d` - distribution (percentile-capable: p50/p95/p99 in Datadog)
17pub trait DogStatsDExport {
18    /// Export this metric to the output string in DogStatsD format.
19    ///
20    /// - `output`: String buffer to append to (one line per metric, newline-terminated)
21    /// - `name`: The metric name (with prefix already applied)
22    /// - `tags`: Additional tags to append (e.g., `&[("env", "prod")]`)
23    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]);
24}
25
26fn push_display(output: &mut String, value: impl Display) {
27    let _ = write!(output, "{}", value);
28}
29
30fn write_gauge_f64_value(output: &mut String, value: f64) {
31    if value.fract() == 0.0 {
32        push_display(output, value as i64);
33        return;
34    }
35
36    let start_len = output.len();
37    let _ = write!(output, "{:.6}", value);
38    while output.ends_with('0') {
39        output.pop();
40    }
41    if output.ends_with('.') {
42        output.pop();
43    }
44    if output.len() == start_len {
45        output.push('0');
46    }
47}
48
49/// Helper to append tags in DogStatsD format: `|#tag1:value1,tag2:value2`
50fn append_tags(output: &mut String, tags: &[(&str, &str)]) {
51    if !tags.is_empty() {
52        output.push_str("|#");
53        for (i, (k, v)) in tags.iter().enumerate() {
54            if i > 0 {
55                output.push(',');
56            }
57            output.push_str(k);
58            output.push(':');
59            output.push_str(v);
60        }
61    }
62}
63
64/// Helper to append tags with an additional label prepended.
65fn append_tags_with_label(
66    output: &mut String,
67    label_name: &str,
68    label_value: &str,
69    tags: &[(&str, &str)],
70) {
71    output.push_str("|#");
72    output.push_str(label_name);
73    output.push(':');
74    output.push_str(label_value);
75    for (k, v) in tags {
76        output.push(',');
77        output.push_str(k);
78        output.push(':');
79        output.push_str(v);
80    }
81}
82
83fn append_tags_with_dynamic_label_pairs(
84    output: &mut String,
85    labels: &[(String, String)],
86    tags: &[(&str, &str)],
87) {
88    output.push_str("|#");
89    let mut first = true;
90    for (k, v) in labels {
91        if !first {
92            output.push(',');
93        }
94        first = false;
95        output.push_str(k);
96        output.push(':');
97        output.push_str(v);
98    }
99    for (k, v) in tags {
100        if !first {
101            output.push(',');
102        }
103        first = false;
104        output.push_str(k);
105        output.push(':');
106        output.push_str(v);
107    }
108}
109
110fn append_tags_with_dynamic_labels(
111    output: &mut String,
112    labels: &DynamicLabelSet,
113    tags: &[(&str, &str)],
114) {
115    append_tags_with_dynamic_label_pairs(output, labels.pairs(), tags);
116}
117
118#[doc(hidden)]
119pub fn __write_dogstatsd(
120    output: &mut String,
121    name: &str,
122    value: impl Display,
123    metric_type: &str,
124    tags: &[(&str, &str)],
125) {
126    output.push_str(name);
127    output.push(':');
128    push_display(output, value);
129    output.push('|');
130    output.push_str(metric_type);
131    append_tags(output, tags);
132    output.push('\n');
133}
134
135#[doc(hidden)]
136pub fn __write_dogstatsd_with_label(
137    output: &mut String,
138    name: &str,
139    value: impl Display,
140    metric_type: &str,
141    label_name: &str,
142    label_value: &str,
143    tags: &[(&str, &str)],
144) {
145    output.push_str(name);
146    output.push(':');
147    push_display(output, value);
148    output.push('|');
149    output.push_str(metric_type);
150    append_tags_with_label(output, label_name, label_value, tags);
151    output.push('\n');
152}
153
154#[doc(hidden)]
155pub fn __write_dogstatsd_dynamic(
156    output: &mut String,
157    name: &str,
158    value: impl Display,
159    metric_type: &str,
160    labels: &DynamicLabelSet,
161    tags: &[(&str, &str)],
162) {
163    output.push_str(name);
164    output.push(':');
165    push_display(output, value);
166    output.push('|');
167    output.push_str(metric_type);
168    append_tags_with_dynamic_labels(output, labels, tags);
169    output.push('\n');
170}
171
172#[doc(hidden)]
173pub fn __write_dogstatsd_dynamic_pairs(
174    output: &mut String,
175    name: &str,
176    value: impl Display,
177    metric_type: &str,
178    labels: &[(String, String)],
179    tags: &[(&str, &str)],
180) {
181    output.push_str(name);
182    output.push(':');
183    push_display(output, value);
184    output.push('|');
185    output.push_str(metric_type);
186    append_tags_with_dynamic_label_pairs(output, labels, tags);
187    output.push('\n');
188}
189
190/// Append a `|d` sample with optional sample_rate.
191///
192/// Format: `name:value|d[|@rate]` — tags appended by the caller afterward.
193fn write_distribution_sample(output: &mut String, name: &str, value: u64, count: u64) {
194    output.push_str(name);
195    output.push(':');
196    push_display(output, value);
197    output.push_str("|d");
198    if count > 1 {
199        // sample_rate = 1/count tells the agent to multiply this sample by count
200        output.push_str("|@");
201        let _ = write!(output, "{}", 1.0 / count as f64);
202    }
203}
204
205/// Write DogStatsD `|d` distribution lines from an [`ExpBucketsSnapshot`].
206///
207/// Emits one line per non-zero bucket using the bucket midpoint as the
208/// representative value and `@sample_rate` to encode the bucket count.
209/// This allows Datadog to compute p50/p95/p99 percentiles.
210#[doc(hidden)]
211pub fn __write_dogstatsd_distribution(
212    output: &mut String,
213    name: &str,
214    snap: &ExpBucketsSnapshot,
215    tags: &[(&str, &str)],
216) {
217    for (value, count) in snap.iter_samples() {
218        write_distribution_sample(output, name, value, count);
219        append_tags(output, tags);
220        output.push('\n');
221    }
222}
223
224/// Write DogStatsD `|d` distribution lines with dynamic labels.
225#[doc(hidden)]
226pub fn __write_dogstatsd_distribution_dynamic(
227    output: &mut String,
228    name: &str,
229    snap: &ExpBucketsSnapshot,
230    labels: &DynamicLabelSet,
231    tags: &[(&str, &str)],
232) {
233    for (value, count) in snap.iter_samples() {
234        write_distribution_sample(output, name, value, count);
235        append_tags_with_dynamic_labels(output, labels, tags);
236        output.push('\n');
237    }
238}
239
240fn write_dogstatsd_distribution_dynamic_pairs(
241    output: &mut String,
242    name: &str,
243    snap: &ExpBucketsSnapshot,
244    labels: &[(String, String)],
245    tags: &[(&str, &str)],
246) {
247    for (value, count) in snap.iter_samples() {
248        write_distribution_sample(output, name, value, count);
249        append_tags_with_dynamic_label_pairs(output, labels, tags);
250        output.push('\n');
251    }
252}
253
254/// Write DogStatsD `|d` distribution lines for the **delta** between the current
255/// snapshot and previously-stored bucket counts.
256///
257/// `previous` is a `[u64; 65]` array: indices 0..64 are positive buckets,
258/// index 64 is the zero-count.  Updated in-place to the current values.
259#[doc(hidden)]
260pub fn __write_dogstatsd_distribution_delta(
261    output: &mut String,
262    name: &str,
263    current: &ExpBucketsSnapshot,
264    previous: &mut [u64; 65],
265    tags: &[(&str, &str)],
266) {
267    for (i, &cur) in current.positive.iter().enumerate() {
268        let delta = cur.saturating_sub(previous[i]);
269        previous[i] = cur;
270        if delta > 0 {
271            let value = ExpBucketsSnapshot::bucket_midpoint(i);
272            write_distribution_sample(output, name, value, delta);
273            append_tags(output, tags);
274            output.push('\n');
275        }
276    }
277    let zero_delta = current.zero_count.saturating_sub(previous[64]);
278    previous[64] = current.zero_count;
279    if zero_delta > 0 {
280        write_distribution_sample(output, name, 0, zero_delta);
281        append_tags(output, tags);
282        output.push('\n');
283    }
284}
285
286/// Write DogStatsD `|d` distribution delta lines with dynamic labels.
287#[doc(hidden)]
288pub fn __write_dogstatsd_distribution_delta_dynamic(
289    output: &mut String,
290    name: &str,
291    current: &ExpBucketsSnapshot,
292    previous: &mut [u64; 65],
293    labels: &DynamicLabelSet,
294    tags: &[(&str, &str)],
295) {
296    for (i, &cur) in current.positive.iter().enumerate() {
297        let delta = cur.saturating_sub(previous[i]);
298        previous[i] = cur;
299        if delta > 0 {
300            let value = ExpBucketsSnapshot::bucket_midpoint(i);
301            write_distribution_sample(output, name, value, delta);
302            append_tags_with_dynamic_labels(output, labels, tags);
303            output.push('\n');
304        }
305    }
306    let zero_delta = current.zero_count.saturating_sub(previous[64]);
307    previous[64] = current.zero_count;
308    if zero_delta > 0 {
309        write_distribution_sample(output, name, 0, zero_delta);
310        append_tags_with_dynamic_labels(output, labels, tags);
311        output.push('\n');
312    }
313}
314
315/// Write DogStatsD `|d` distribution delta lines with dynamic labels as borrowed pairs.
316#[doc(hidden)]
317pub fn __write_dogstatsd_distribution_delta_dynamic_pairs(
318    output: &mut String,
319    name: &str,
320    current: &ExpBucketsSnapshot,
321    previous: &mut [u64; 65],
322    labels: &[(String, String)],
323    tags: &[(&str, &str)],
324) {
325    for (i, &cur) in current.positive.iter().enumerate() {
326        let delta = cur.saturating_sub(previous[i]);
327        previous[i] = cur;
328        if delta > 0 {
329            let value = ExpBucketsSnapshot::bucket_midpoint(i);
330            write_distribution_sample(output, name, value, delta);
331            append_tags_with_dynamic_label_pairs(output, labels, tags);
332            output.push('\n');
333        }
334    }
335    let zero_delta = current.zero_count.saturating_sub(previous[64]);
336    previous[64] = current.zero_count;
337    if zero_delta > 0 {
338        write_distribution_sample(output, name, 0, zero_delta);
339        append_tags_with_dynamic_label_pairs(output, labels, tags);
340        output.push('\n');
341    }
342}
343
344impl DogStatsDExport for Counter {
345    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
346        output.push_str(name);
347        output.push(':');
348        push_display(output, self.sum());
349        output.push_str("|c");
350        append_tags(output, tags);
351        output.push('\n');
352    }
353}
354
355impl DogStatsDExport for Gauge {
356    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
357        output.push_str(name);
358        output.push(':');
359        push_display(output, self.get());
360        output.push_str("|g");
361        append_tags(output, tags);
362        output.push('\n');
363    }
364}
365
366impl DogStatsDExport for MaxGauge {
367    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
368        output.push_str(name);
369        output.push(':');
370        push_display(output, self.get());
371        output.push_str("|g");
372        append_tags(output, tags);
373        output.push('\n');
374    }
375}
376
377impl DogStatsDExport for MaxGaugeF64 {
378    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
379        output.push_str(name);
380        output.push(':');
381        write_gauge_f64_value(output, self.get());
382        output.push_str("|g");
383        append_tags(output, tags);
384        output.push('\n');
385    }
386}
387
388impl DogStatsDExport for MinGauge {
389    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
390        output.push_str(name);
391        output.push(':');
392        push_display(output, self.get());
393        output.push_str("|g");
394        append_tags(output, tags);
395        output.push('\n');
396    }
397}
398
399impl DogStatsDExport for MinGaugeF64 {
400    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
401        output.push_str(name);
402        output.push(':');
403        write_gauge_f64_value(output, self.get());
404        output.push_str("|g");
405        append_tags(output, tags);
406        output.push('\n');
407    }
408}
409
410impl DogStatsDExport for GaugeF64 {
411    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
412        output.push_str(name);
413        output.push(':');
414        // Format with up to 6 decimal places, trimming trailing zeros.
415        write_gauge_f64_value(output, self.get());
416        output.push_str("|g");
417        append_tags(output, tags);
418        output.push('\n');
419    }
420}
421
422impl DogStatsDExport for Histogram {
423    /// Export histogram as count + sum metrics.
424    ///
425    /// DogStatsD distributions expect raw samples, not pre-aggregated buckets.
426    /// We export `name.count` and `name.sum` as counters instead.
427    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
428        output.push_str(name);
429        output.push_str(".count:");
430        push_display(output, self.count());
431        output.push_str("|c");
432        append_tags(output, tags);
433        output.push('\n');
434
435        output.push_str(name);
436        output.push_str(".sum:");
437        push_display(output, self.sum());
438        output.push_str("|c");
439        append_tags(output, tags);
440        output.push('\n');
441    }
442}
443
444impl DogStatsDExport for SampledTimer {
445    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
446        let calls_name = format!("{name}.calls");
447        let samples_name = format!("{name}.samples");
448        self.calls_metric()
449            .export_dogstatsd(output, &calls_name, tags);
450        self.histogram()
451            .export_dogstatsd(output, &samples_name, tags);
452    }
453}
454
455impl DogStatsDExport for Distribution {
456    /// Export distribution as `|d` samples for Datadog percentile computation.
457    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
458        let snap = self.buckets_snapshot();
459        __write_dogstatsd_distribution(output, name, &snap, tags);
460    }
461}
462
463impl<L: LabelEnum> DogStatsDExport for LabeledCounter<L> {
464    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
465        for (label, count) in self.iter() {
466            output.push_str(name);
467            output.push(':');
468            push_display(output, count);
469            output.push_str("|c");
470            append_tags_with_label(output, L::LABEL_NAME, label.variant_name(), tags);
471            output.push('\n');
472        }
473    }
474}
475
476impl<L: LabelEnum> DogStatsDExport for LabeledGauge<L> {
477    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
478        for (label, value) in self.iter() {
479            output.push_str(name);
480            output.push(':');
481            push_display(output, value);
482            output.push_str("|g");
483            append_tags_with_label(output, L::LABEL_NAME, label.variant_name(), tags);
484            output.push('\n');
485        }
486    }
487}
488
489impl<L: LabelEnum> DogStatsDExport for LabeledHistogram<L> {
490    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
491        for (label, _buckets, sum, count) in self.iter() {
492            let variant = label.variant_name();
493
494            output.push_str(name);
495            output.push_str(".count:");
496            push_display(output, count);
497            output.push_str("|c");
498            append_tags_with_label(output, L::LABEL_NAME, variant, tags);
499            output.push('\n');
500
501            output.push_str(name);
502            output.push_str(".sum:");
503            push_display(output, sum);
504            output.push_str("|c");
505            append_tags_with_label(output, L::LABEL_NAME, variant, tags);
506            output.push('\n');
507        }
508    }
509}
510
511impl<L: LabelEnum> DogStatsDExport for LabeledSampledTimer<L> {
512    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
513        let calls_name = format!("{name}.calls");
514        let samples_name = format!("{name}.samples");
515
516        for (label, calls, histogram) in self.iter() {
517            let variant = label.variant_name();
518
519            __write_dogstatsd_with_label(
520                output,
521                &calls_name,
522                calls.sum(),
523                "c",
524                L::LABEL_NAME,
525                variant,
526                tags,
527            );
528
529            __write_dogstatsd_with_label(
530                output,
531                &format!("{}.count", samples_name),
532                histogram.count(),
533                "c",
534                L::LABEL_NAME,
535                variant,
536                tags,
537            );
538
539            __write_dogstatsd_with_label(
540                output,
541                &format!("{}.sum", samples_name),
542                histogram.sum(),
543                "c",
544                L::LABEL_NAME,
545                variant,
546                tags,
547            );
548        }
549    }
550}
551
552impl DogStatsDExport for DynamicCounter {
553    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
554        self.visit_series(|labels, count| {
555            __write_dogstatsd_dynamic_pairs(output, name, count, "c", labels, tags);
556        });
557    }
558}
559
560impl DogStatsDExport for DynamicGauge {
561    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
562        self.visit_series(|labels, value| {
563            output.push_str(name);
564            output.push(':');
565            write_gauge_f64_value(output, value);
566            output.push_str("|g");
567            append_tags_with_dynamic_label_pairs(output, labels, tags);
568            output.push('\n');
569        });
570    }
571}
572
573impl DogStatsDExport for DynamicGaugeI64 {
574    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
575        self.visit_series(|labels, value| {
576            __write_dogstatsd_dynamic_pairs(output, name, value, "g", labels, tags);
577        });
578    }
579}
580
581impl DogStatsDExport for DynamicHistogram {
582    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
583        let count_name = format!("{}.count", name);
584        let sum_name = format!("{}.sum", name);
585        self.visit_series(|labels, series| {
586            __write_dogstatsd_dynamic_pairs(output, &count_name, series.count(), "c", labels, tags);
587            __write_dogstatsd_dynamic_pairs(output, &sum_name, series.sum(), "c", labels, tags);
588        });
589    }
590}
591
592impl DogStatsDExport for DynamicDistribution {
593    /// Export distribution as `|d` samples per label set for Datadog percentile computation.
594    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
595        self.visit_series(|labels, _count, _sum, snap| {
596            write_dogstatsd_distribution_dynamic_pairs(output, name, &snap, labels, tags);
597        });
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use super::DogStatsDExport;
604    use crate::{Counter, Distribution, DynamicCounter, DynamicDistribution, Gauge, Histogram};
605
606    #[test]
607    fn test_dogstatsd_counter() {
608        let counter = Counter::new(4);
609        counter.inc();
610        counter.inc();
611
612        let mut output = String::new();
613        counter.export_dogstatsd(&mut output, "test.counter", &[]);
614
615        assert_eq!(output, "test.counter:2|c\n");
616    }
617
618    #[test]
619    fn test_dogstatsd_counter_with_tags() {
620        let counter = Counter::new(4);
621        counter.add(100);
622
623        let mut output = String::new();
624        counter.export_dogstatsd(
625            &mut output,
626            "test.counter",
627            &[("env", "prod"), ("host", "web01")],
628        );
629
630        assert_eq!(output, "test.counter:100|c|#env:prod,host:web01\n");
631    }
632
633    #[test]
634    fn test_dogstatsd_gauge() {
635        let gauge = Gauge::new();
636        gauge.set(42);
637
638        let mut output = String::new();
639        gauge.export_dogstatsd(&mut output, "test.gauge", &[]);
640
641        assert_eq!(output, "test.gauge:42|g\n");
642    }
643
644    #[test]
645    fn test_dogstatsd_gauge_with_tags() {
646        let gauge = Gauge::new();
647        gauge.set(-10);
648
649        let mut output = String::new();
650        gauge.export_dogstatsd(&mut output, "memory.used", &[("region", "us-east")]);
651
652        assert_eq!(output, "memory.used:-10|g|#region:us-east\n");
653    }
654
655    #[test]
656    fn test_dogstatsd_histogram() {
657        let histogram = Histogram::new(&[10, 100], 4);
658        histogram.record(5);
659        histogram.record(50);
660        histogram.record(500);
661
662        let mut output = String::new();
663        histogram.export_dogstatsd(&mut output, "latency", &[]);
664
665        assert!(output.contains("latency.count:3|c\n"));
666        assert!(output.contains("latency.sum:555|c\n"));
667    }
668
669    #[test]
670    fn test_dogstatsd_histogram_with_tags() {
671        let histogram = Histogram::new(&[100], 4);
672        histogram.record(50);
673        histogram.record(150);
674
675        let mut output = String::new();
676        histogram.export_dogstatsd(&mut output, "latency", &[("service", "api")]);
677
678        assert!(output.contains("latency.count:2|c|#service:api\n"));
679        assert!(output.contains("latency.sum:200|c|#service:api\n"));
680    }
681
682    #[test]
683    fn test_dogstatsd_distribution() {
684        let dist = Distribution::new(4);
685        dist.record(100);
686        dist.record(200);
687        dist.record(300);
688
689        let mut output = String::new();
690        dist.export_dogstatsd(&mut output, "latency", &[]);
691
692        assert!(output.contains("latency:96|d\n"));
693        assert!(output.contains("latency:192|d\n"));
694        assert!(output.contains("latency:384|d\n"));
695    }
696
697    #[test]
698    fn test_dogstatsd_distribution_with_tags() {
699        let dist = Distribution::new(4);
700        dist.record(50);
701        dist.record(150);
702
703        let mut output = String::new();
704        dist.export_dogstatsd(&mut output, "latency", &[("service", "api")]);
705
706        assert!(output.contains("latency:48|d|#service:api\n"));
707        assert!(output.contains("latency:192|d|#service:api\n"));
708    }
709
710    #[test]
711    fn test_dogstatsd_distribution_empty() {
712        let dist = Distribution::new(4);
713
714        let mut output = String::new();
715        dist.export_dogstatsd(&mut output, "latency", &[]);
716
717        assert!(output.is_empty());
718    }
719
720    #[test]
721    fn test_dogstatsd_distribution_sample_rate() {
722        let dist = Distribution::new(4);
723        dist.record(100);
724        dist.record(100);
725        dist.record(100);
726
727        let mut output = String::new();
728        dist.export_dogstatsd(&mut output, "latency", &[]);
729
730        let line = output.lines().next().expect("should have a line");
731        assert!(line.starts_with("latency:96|d|@"));
732        assert!(line.contains("|@0.3333333333333333"));
733    }
734
735    #[test]
736    fn test_dogstatsd_dynamic_counter() {
737        let counter = DynamicCounter::new(4);
738        counter.add(&[("endpoint", "ep1"), ("method", "GET")], 3);
739
740        let mut output = String::new();
741        counter.export_dogstatsd(&mut output, "requests", &[("env", "prod")]);
742
743        assert!(output.contains("requests:3|c|#"));
744        assert!(output.contains("endpoint:ep1"));
745        assert!(output.contains("method:GET"));
746        assert!(output.contains("env:prod"));
747    }
748
749    #[test]
750    fn test_dogstatsd_dynamic_distribution() {
751        let dist = DynamicDistribution::new(4);
752        dist.record(&[("endpoint", "ep1")], 100);
753        dist.record(&[("endpoint", "ep1")], 100);
754
755        let mut output = String::new();
756        dist.export_dogstatsd(&mut output, "latency", &[("env", "prod")]);
757
758        assert!(output.contains("latency:96|d|@0.5|#endpoint:ep1,env:prod"));
759    }
760}