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
37fn 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
52fn 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
178fn 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 output.push_str("|@");
189 (1.0_f64 / count as f64).fast_push(output);
190 }
191}
192
193#[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#[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#[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#[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#[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 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 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 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 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}