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 = concat_two(name, ".calls");
435        let samples_name = concat_two(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
443#[inline]
444fn concat_two(a: &str, b: &str) -> String {
445    let mut s = String::with_capacity(a.len() + b.len());
446    s.push_str(a);
447    s.push_str(b);
448    s
449}
450
451impl DogStatsDExport for Distribution {
452    /// Export distribution as `|d` samples for Datadog percentile computation.
453    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
454        let snap = self.buckets_snapshot();
455        __write_dogstatsd_distribution(output, name, &snap, tags);
456    }
457}
458
459impl<L: LabelEnum> DogStatsDExport for LabeledCounter<L> {
460    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
461        for (label, count) in self.iter() {
462            output.push_str(name);
463            output.push(':');
464            push_display(output, count);
465            output.push_str("|c");
466            append_tags_with_label(output, L::LABEL_NAME, label.variant_name(), tags);
467            output.push('\n');
468        }
469    }
470}
471
472impl<L: LabelEnum> DogStatsDExport for LabeledGauge<L> {
473    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
474        for (label, value) in self.iter() {
475            output.push_str(name);
476            output.push(':');
477            push_display(output, value);
478            output.push_str("|g");
479            append_tags_with_label(output, L::LABEL_NAME, label.variant_name(), tags);
480            output.push('\n');
481        }
482    }
483}
484
485impl<L: LabelEnum> DogStatsDExport for LabeledHistogram<L> {
486    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
487        for (label, histogram) in self.iter() {
488            let variant = label.variant_name();
489
490            output.push_str(name);
491            output.push_str(".count:");
492            push_display(output, histogram.count());
493            output.push_str("|c");
494            append_tags_with_label(output, L::LABEL_NAME, variant, tags);
495            output.push('\n');
496
497            output.push_str(name);
498            output.push_str(".sum:");
499            push_display(output, histogram.sum());
500            output.push_str("|c");
501            append_tags_with_label(output, L::LABEL_NAME, variant, tags);
502            output.push('\n');
503        }
504    }
505}
506
507impl<L: LabelEnum> DogStatsDExport for LabeledSampledTimer<L> {
508    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
509        let calls_name = concat_two(name, ".calls");
510        let samples_count_name = concat_three(name, ".samples", ".count");
511        let samples_sum_name = concat_three(name, ".samples", ".sum");
512
513        for (label, calls, histogram) in self.iter() {
514            let variant = label.variant_name();
515
516            __write_dogstatsd_with_label(
517                output,
518                &calls_name,
519                calls.sum(),
520                "c",
521                L::LABEL_NAME,
522                variant,
523                tags,
524            );
525
526            __write_dogstatsd_with_label(
527                output,
528                &samples_count_name,
529                histogram.count(),
530                "c",
531                L::LABEL_NAME,
532                variant,
533                tags,
534            );
535
536            __write_dogstatsd_with_label(
537                output,
538                &samples_sum_name,
539                histogram.sum(),
540                "c",
541                L::LABEL_NAME,
542                variant,
543                tags,
544            );
545        }
546    }
547}
548
549#[inline]
550fn concat_three(a: &str, b: &str, c: &str) -> String {
551    let mut s = String::with_capacity(a.len() + b.len() + c.len());
552    s.push_str(a);
553    s.push_str(b);
554    s.push_str(c);
555    s
556}
557
558impl DogStatsDExport for DynamicCounter {
559    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
560        self.visit_series(|labels, count| {
561            __write_dogstatsd_dynamic_pairs(output, name, count, "c", labels, tags);
562        });
563    }
564}
565
566impl DogStatsDExport for DynamicGauge {
567    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
568        self.visit_series(|labels, value| {
569            output.push_str(name);
570            output.push(':');
571            write_gauge_f64_value(output, value);
572            output.push_str("|g");
573            append_tags_with_dynamic_label_pairs(output, labels, tags);
574            output.push('\n');
575        });
576    }
577}
578
579impl DogStatsDExport for DynamicGaugeI64 {
580    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
581        self.visit_series(|labels, value| {
582            __write_dogstatsd_dynamic_pairs(output, name, value, "g", labels, tags);
583        });
584    }
585}
586
587impl DogStatsDExport for DynamicHistogram {
588    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
589        let count_name = concat_two(name, ".count");
590        let sum_name = concat_two(name, ".sum");
591        self.visit_series(|labels, series| {
592            __write_dogstatsd_dynamic_pairs(output, &count_name, series.count(), "c", labels, tags);
593            __write_dogstatsd_dynamic_pairs(output, &sum_name, series.sum(), "c", labels, tags);
594        });
595    }
596}
597
598impl DogStatsDExport for DynamicDistribution {
599    /// Export distribution as `|d` samples per label set for Datadog percentile computation.
600    fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
601        self.visit_series(|labels, _count, _sum, snap| {
602            write_dogstatsd_distribution_dynamic_pairs(output, name, &snap, labels, tags);
603        });
604    }
605}
606
607#[cfg(test)]
608mod tests {
609    use super::DogStatsDExport;
610    use crate::{Counter, Distribution, DynamicCounter, DynamicDistribution, Gauge, Histogram};
611
612    #[test]
613    fn test_dogstatsd_counter() {
614        let counter = Counter::new(4);
615        counter.inc();
616        counter.inc();
617
618        let mut output = String::new();
619        counter.export_dogstatsd(&mut output, "test.counter", &[]);
620
621        assert_eq!(output, "test.counter:2|c\n");
622    }
623
624    #[test]
625    fn test_dogstatsd_counter_with_tags() {
626        let counter = Counter::new(4);
627        counter.add(100);
628
629        let mut output = String::new();
630        counter.export_dogstatsd(
631            &mut output,
632            "test.counter",
633            &[("env", "prod"), ("host", "web01")],
634        );
635
636        assert_eq!(output, "test.counter:100|c|#env:prod,host:web01\n");
637    }
638
639    #[test]
640    fn test_dogstatsd_gauge() {
641        let gauge = Gauge::new();
642        gauge.set(42);
643
644        let mut output = String::new();
645        gauge.export_dogstatsd(&mut output, "test.gauge", &[]);
646
647        assert_eq!(output, "test.gauge:42|g\n");
648    }
649
650    #[test]
651    fn test_dogstatsd_gauge_with_tags() {
652        let gauge = Gauge::new();
653        gauge.set(-10);
654
655        let mut output = String::new();
656        gauge.export_dogstatsd(&mut output, "memory.used", &[("region", "us-east")]);
657
658        assert_eq!(output, "memory.used:-10|g|#region:us-east\n");
659    }
660
661    #[test]
662    fn test_dogstatsd_histogram() {
663        let histogram = Histogram::new(&[10, 100], 4);
664        histogram.record(5);
665        histogram.record(50);
666        histogram.record(500);
667
668        let mut output = String::new();
669        histogram.export_dogstatsd(&mut output, "latency", &[]);
670
671        assert!(output.contains("latency.count:3|c\n"));
672        assert!(output.contains("latency.sum:555|c\n"));
673    }
674
675    #[test]
676    fn test_dogstatsd_histogram_with_tags() {
677        let histogram = Histogram::new(&[100], 4);
678        histogram.record(50);
679        histogram.record(150);
680
681        let mut output = String::new();
682        histogram.export_dogstatsd(&mut output, "latency", &[("service", "api")]);
683
684        assert!(output.contains("latency.count:2|c|#service:api\n"));
685        assert!(output.contains("latency.sum:200|c|#service:api\n"));
686    }
687
688    #[test]
689    fn test_dogstatsd_distribution() {
690        let dist = Distribution::new(4);
691        dist.record(100);
692        dist.record(200);
693        dist.record(300);
694
695        let mut output = String::new();
696        dist.export_dogstatsd(&mut output, "latency", &[]);
697
698        assert!(output.contains("latency:96|d\n"));
699        assert!(output.contains("latency:192|d\n"));
700        assert!(output.contains("latency:384|d\n"));
701    }
702
703    #[test]
704    fn test_dogstatsd_distribution_with_tags() {
705        let dist = Distribution::new(4);
706        dist.record(50);
707        dist.record(150);
708
709        let mut output = String::new();
710        dist.export_dogstatsd(&mut output, "latency", &[("service", "api")]);
711
712        assert!(output.contains("latency:48|d|#service:api\n"));
713        assert!(output.contains("latency:192|d|#service:api\n"));
714    }
715
716    #[test]
717    fn test_dogstatsd_distribution_empty() {
718        let dist = Distribution::new(4);
719
720        let mut output = String::new();
721        dist.export_dogstatsd(&mut output, "latency", &[]);
722
723        assert!(output.is_empty());
724    }
725
726    #[test]
727    fn test_dogstatsd_distribution_sample_rate() {
728        let dist = Distribution::new(4);
729        dist.record(100);
730        dist.record(100);
731        dist.record(100);
732
733        let mut output = String::new();
734        dist.export_dogstatsd(&mut output, "latency", &[]);
735
736        let line = output.lines().next().expect("should have a line");
737        assert!(line.starts_with("latency:96|d|@"));
738        assert!(line.contains("|@0.3333333333333333"));
739    }
740
741    #[test]
742    fn test_dogstatsd_dynamic_counter() {
743        let counter = DynamicCounter::new(4);
744        counter.add(&[("endpoint", "ep1"), ("method", "GET")], 3);
745
746        let mut output = String::new();
747        counter.export_dogstatsd(&mut output, "requests", &[("env", "prod")]);
748
749        assert!(output.contains("requests:3|c|#"));
750        assert!(output.contains("endpoint:ep1"));
751        assert!(output.contains("method:GET"));
752        assert!(output.contains("env:prod"));
753    }
754
755    #[test]
756    fn test_dogstatsd_dynamic_distribution() {
757        let dist = DynamicDistribution::new(4);
758        dist.record(&[("endpoint", "ep1")], 100);
759        dist.record(&[("endpoint", "ep1")], 100);
760
761        let mut output = String::new();
762        dist.export_dogstatsd(&mut output, "latency", &[("env", "prod")]);
763
764        assert!(output.contains("latency:96|d|@0.5|#endpoint:ep1,env:prod"));
765    }
766}