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