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
10pub trait DogStatsDExport {
19 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#[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#[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
182fn 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 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#[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#[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#[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#[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#[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 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 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 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 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}