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