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
9pub trait DogStatsDExport {
18 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
49fn 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
64fn 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
190fn 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 output.push_str("|@");
201 let _ = write!(output, "{}", 1.0 / count as f64);
202 }
203}
204
205#[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#[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#[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#[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#[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 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 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 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 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}