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