1use crate::{
2 Counter, Distribution, DynamicCounter, DynamicDistribution, DynamicGauge, DynamicGaugeI64,
3 DynamicHistogram, DynamicLabelSet, Gauge, GaugeF64, Histogram, LabelEnum, LabeledCounter,
4 LabeledGauge, LabeledHistogram, exp_buckets::ExpBucketsSnapshot,
5};
6use std::fmt::{Display, Write as _};
7
8pub trait DogStatsDExport {
17 fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]);
23}
24
25fn push_display(output: &mut String, value: impl Display) {
26 let _ = write!(output, "{}", value);
27}
28
29fn write_gauge_f64_value(output: &mut String, value: f64) {
30 if value.fract() == 0.0 {
31 push_display(output, value as i64);
32 return;
33 }
34
35 let start_len = output.len();
36 let _ = write!(output, "{:.6}", value);
37 while output.ends_with('0') {
38 output.pop();
39 }
40 if output.ends_with('.') {
41 output.pop();
42 }
43 if output.len() == start_len {
44 output.push('0');
45 }
46}
47
48fn append_tags(output: &mut String, tags: &[(&str, &str)]) {
50 if !tags.is_empty() {
51 output.push_str("|#");
52 for (i, (k, v)) in tags.iter().enumerate() {
53 if i > 0 {
54 output.push(',');
55 }
56 output.push_str(k);
57 output.push(':');
58 output.push_str(v);
59 }
60 }
61}
62
63fn append_tags_with_label(
65 output: &mut String,
66 label_name: &str,
67 label_value: &str,
68 tags: &[(&str, &str)],
69) {
70 output.push_str("|#");
71 output.push_str(label_name);
72 output.push(':');
73 output.push_str(label_value);
74 for (k, v) in tags {
75 output.push(',');
76 output.push_str(k);
77 output.push(':');
78 output.push_str(v);
79 }
80}
81
82fn append_tags_with_dynamic_label_pairs(
83 output: &mut String,
84 labels: &[(String, String)],
85 tags: &[(&str, &str)],
86) {
87 output.push_str("|#");
88 let mut first = true;
89 for (k, v) in labels {
90 if !first {
91 output.push(',');
92 }
93 first = false;
94 output.push_str(k);
95 output.push(':');
96 output.push_str(v);
97 }
98 for (k, v) in tags {
99 if !first {
100 output.push(',');
101 }
102 first = false;
103 output.push_str(k);
104 output.push(':');
105 output.push_str(v);
106 }
107}
108
109fn append_tags_with_dynamic_labels(
110 output: &mut String,
111 labels: &DynamicLabelSet,
112 tags: &[(&str, &str)],
113) {
114 append_tags_with_dynamic_label_pairs(output, labels.pairs(), tags);
115}
116
117#[doc(hidden)]
118pub fn __write_dogstatsd(
119 output: &mut String,
120 name: &str,
121 value: impl Display,
122 metric_type: &str,
123 tags: &[(&str, &str)],
124) {
125 output.push_str(name);
126 output.push(':');
127 push_display(output, value);
128 output.push('|');
129 output.push_str(metric_type);
130 append_tags(output, tags);
131 output.push('\n');
132}
133
134#[doc(hidden)]
135pub fn __write_dogstatsd_with_label(
136 output: &mut String,
137 name: &str,
138 value: impl Display,
139 metric_type: &str,
140 label_name: &str,
141 label_value: &str,
142 tags: &[(&str, &str)],
143) {
144 output.push_str(name);
145 output.push(':');
146 push_display(output, value);
147 output.push('|');
148 output.push_str(metric_type);
149 append_tags_with_label(output, label_name, label_value, tags);
150 output.push('\n');
151}
152
153#[doc(hidden)]
154pub fn __write_dogstatsd_dynamic(
155 output: &mut String,
156 name: &str,
157 value: impl Display,
158 metric_type: &str,
159 labels: &DynamicLabelSet,
160 tags: &[(&str, &str)],
161) {
162 output.push_str(name);
163 output.push(':');
164 push_display(output, value);
165 output.push('|');
166 output.push_str(metric_type);
167 append_tags_with_dynamic_labels(output, labels, tags);
168 output.push('\n');
169}
170
171#[doc(hidden)]
172pub fn __write_dogstatsd_dynamic_pairs(
173 output: &mut String,
174 name: &str,
175 value: impl Display,
176 metric_type: &str,
177 labels: &[(String, String)],
178 tags: &[(&str, &str)],
179) {
180 output.push_str(name);
181 output.push(':');
182 push_display(output, value);
183 output.push('|');
184 output.push_str(metric_type);
185 append_tags_with_dynamic_label_pairs(output, labels, tags);
186 output.push('\n');
187}
188
189fn write_distribution_sample(output: &mut String, name: &str, value: u64, count: u64) {
193 output.push_str(name);
194 output.push(':');
195 push_display(output, value);
196 output.push_str("|d");
197 if count > 1 {
198 output.push_str("|@");
200 let _ = write!(output, "{}", 1.0 / count as f64);
201 }
202}
203
204#[doc(hidden)]
210pub fn __write_dogstatsd_distribution(
211 output: &mut String,
212 name: &str,
213 snap: &ExpBucketsSnapshot,
214 tags: &[(&str, &str)],
215) {
216 for (value, count) in snap.iter_samples() {
217 write_distribution_sample(output, name, value, count);
218 append_tags(output, tags);
219 output.push('\n');
220 }
221}
222
223#[doc(hidden)]
225pub fn __write_dogstatsd_distribution_dynamic(
226 output: &mut String,
227 name: &str,
228 snap: &ExpBucketsSnapshot,
229 labels: &DynamicLabelSet,
230 tags: &[(&str, &str)],
231) {
232 for (value, count) in snap.iter_samples() {
233 write_distribution_sample(output, name, value, count);
234 append_tags_with_dynamic_labels(output, labels, tags);
235 output.push('\n');
236 }
237}
238
239fn write_dogstatsd_distribution_dynamic_pairs(
240 output: &mut String,
241 name: &str,
242 snap: &ExpBucketsSnapshot,
243 labels: &[(String, String)],
244 tags: &[(&str, &str)],
245) {
246 for (value, count) in snap.iter_samples() {
247 write_distribution_sample(output, name, value, count);
248 append_tags_with_dynamic_label_pairs(output, labels, tags);
249 output.push('\n');
250 }
251}
252
253#[doc(hidden)]
259pub fn __write_dogstatsd_distribution_delta(
260 output: &mut String,
261 name: &str,
262 current: &ExpBucketsSnapshot,
263 previous: &mut [u64; 65],
264 tags: &[(&str, &str)],
265) {
266 for (i, &cur) in current.positive.iter().enumerate() {
267 let delta = cur.saturating_sub(previous[i]);
268 previous[i] = cur;
269 if delta > 0 {
270 let value = ExpBucketsSnapshot::bucket_midpoint(i);
271 write_distribution_sample(output, name, value, delta);
272 append_tags(output, tags);
273 output.push('\n');
274 }
275 }
276 let zero_delta = current.zero_count.saturating_sub(previous[64]);
277 previous[64] = current.zero_count;
278 if zero_delta > 0 {
279 write_distribution_sample(output, name, 0, zero_delta);
280 append_tags(output, tags);
281 output.push('\n');
282 }
283}
284
285#[doc(hidden)]
287pub fn __write_dogstatsd_distribution_delta_dynamic(
288 output: &mut String,
289 name: &str,
290 current: &ExpBucketsSnapshot,
291 previous: &mut [u64; 65],
292 labels: &DynamicLabelSet,
293 tags: &[(&str, &str)],
294) {
295 for (i, &cur) in current.positive.iter().enumerate() {
296 let delta = cur.saturating_sub(previous[i]);
297 previous[i] = cur;
298 if delta > 0 {
299 let value = ExpBucketsSnapshot::bucket_midpoint(i);
300 write_distribution_sample(output, name, value, delta);
301 append_tags_with_dynamic_labels(output, labels, tags);
302 output.push('\n');
303 }
304 }
305 let zero_delta = current.zero_count.saturating_sub(previous[64]);
306 previous[64] = current.zero_count;
307 if zero_delta > 0 {
308 write_distribution_sample(output, name, 0, zero_delta);
309 append_tags_with_dynamic_labels(output, labels, tags);
310 output.push('\n');
311 }
312}
313
314#[doc(hidden)]
316pub fn __write_dogstatsd_distribution_delta_dynamic_pairs(
317 output: &mut String,
318 name: &str,
319 current: &ExpBucketsSnapshot,
320 previous: &mut [u64; 65],
321 labels: &[(String, String)],
322 tags: &[(&str, &str)],
323) {
324 for (i, &cur) in current.positive.iter().enumerate() {
325 let delta = cur.saturating_sub(previous[i]);
326 previous[i] = cur;
327 if delta > 0 {
328 let value = ExpBucketsSnapshot::bucket_midpoint(i);
329 write_distribution_sample(output, name, value, delta);
330 append_tags_with_dynamic_label_pairs(output, labels, tags);
331 output.push('\n');
332 }
333 }
334 let zero_delta = current.zero_count.saturating_sub(previous[64]);
335 previous[64] = current.zero_count;
336 if zero_delta > 0 {
337 write_distribution_sample(output, name, 0, zero_delta);
338 append_tags_with_dynamic_label_pairs(output, labels, tags);
339 output.push('\n');
340 }
341}
342
343impl DogStatsDExport for Counter {
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.sum());
348 output.push_str("|c");
349 append_tags(output, tags);
350 output.push('\n');
351 }
352}
353
354impl DogStatsDExport for Gauge {
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 GaugeF64 {
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());
371 output.push_str("|g");
372 append_tags(output, tags);
373 output.push('\n');
374 }
375}
376
377impl DogStatsDExport for Histogram {
378 fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
383 output.push_str(name);
384 output.push_str(".count:");
385 push_display(output, self.count());
386 output.push_str("|c");
387 append_tags(output, tags);
388 output.push('\n');
389
390 output.push_str(name);
391 output.push_str(".sum:");
392 push_display(output, self.sum());
393 output.push_str("|c");
394 append_tags(output, tags);
395 output.push('\n');
396 }
397}
398
399impl DogStatsDExport for Distribution {
400 fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
402 let snap = self.buckets_snapshot();
403 __write_dogstatsd_distribution(output, name, &snap, tags);
404 }
405}
406
407impl<L: LabelEnum> DogStatsDExport for LabeledCounter<L> {
408 fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
409 for (label, count) in self.iter() {
410 output.push_str(name);
411 output.push(':');
412 push_display(output, count);
413 output.push_str("|c");
414 append_tags_with_label(output, L::LABEL_NAME, label.variant_name(), tags);
415 output.push('\n');
416 }
417 }
418}
419
420impl<L: LabelEnum> DogStatsDExport for LabeledGauge<L> {
421 fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
422 for (label, value) in self.iter() {
423 output.push_str(name);
424 output.push(':');
425 push_display(output, value);
426 output.push_str("|g");
427 append_tags_with_label(output, L::LABEL_NAME, label.variant_name(), tags);
428 output.push('\n');
429 }
430 }
431}
432
433impl<L: LabelEnum> DogStatsDExport for LabeledHistogram<L> {
434 fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
435 for (label, _buckets, sum, count) in self.iter() {
436 let variant = label.variant_name();
437
438 output.push_str(name);
439 output.push_str(".count:");
440 push_display(output, count);
441 output.push_str("|c");
442 append_tags_with_label(output, L::LABEL_NAME, variant, tags);
443 output.push('\n');
444
445 output.push_str(name);
446 output.push_str(".sum:");
447 push_display(output, sum);
448 output.push_str("|c");
449 append_tags_with_label(output, L::LABEL_NAME, variant, tags);
450 output.push('\n');
451 }
452 }
453}
454
455impl DogStatsDExport for DynamicCounter {
456 fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
457 self.visit_series(|labels, count| {
458 __write_dogstatsd_dynamic_pairs(output, name, count, "c", labels, tags);
459 });
460 }
461}
462
463impl DogStatsDExport for DynamicGauge {
464 fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
465 self.visit_series(|labels, value| {
466 output.push_str(name);
467 output.push(':');
468 write_gauge_f64_value(output, value);
469 output.push_str("|g");
470 append_tags_with_dynamic_label_pairs(output, labels, tags);
471 output.push('\n');
472 });
473 }
474}
475
476impl DogStatsDExport for DynamicGaugeI64 {
477 fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
478 self.visit_series(|labels, value| {
479 __write_dogstatsd_dynamic_pairs(output, name, value, "g", labels, tags);
480 });
481 }
482}
483
484impl DogStatsDExport for DynamicHistogram {
485 fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
486 let count_name = format!("{}.count", name);
487 let sum_name = format!("{}.sum", name);
488 self.visit_series(|labels, series| {
489 __write_dogstatsd_dynamic_pairs(output, &count_name, series.count(), "c", labels, tags);
490 __write_dogstatsd_dynamic_pairs(output, &sum_name, series.sum(), "c", labels, tags);
491 });
492 }
493}
494
495impl DogStatsDExport for DynamicDistribution {
496 fn export_dogstatsd(&self, output: &mut String, name: &str, tags: &[(&str, &str)]) {
498 self.visit_series(|labels, _count, _sum, snap| {
499 write_dogstatsd_distribution_dynamic_pairs(output, name, &snap, labels, tags);
500 });
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use super::DogStatsDExport;
507 use crate::{Counter, Distribution, DynamicCounter, DynamicDistribution, Gauge, Histogram};
508
509 #[test]
510 fn test_dogstatsd_counter() {
511 let counter = Counter::new(4);
512 counter.inc();
513 counter.inc();
514
515 let mut output = String::new();
516 counter.export_dogstatsd(&mut output, "test.counter", &[]);
517
518 assert_eq!(output, "test.counter:2|c\n");
519 }
520
521 #[test]
522 fn test_dogstatsd_counter_with_tags() {
523 let counter = Counter::new(4);
524 counter.add(100);
525
526 let mut output = String::new();
527 counter.export_dogstatsd(
528 &mut output,
529 "test.counter",
530 &[("env", "prod"), ("host", "web01")],
531 );
532
533 assert_eq!(output, "test.counter:100|c|#env:prod,host:web01\n");
534 }
535
536 #[test]
537 fn test_dogstatsd_gauge() {
538 let gauge = Gauge::new();
539 gauge.set(42);
540
541 let mut output = String::new();
542 gauge.export_dogstatsd(&mut output, "test.gauge", &[]);
543
544 assert_eq!(output, "test.gauge:42|g\n");
545 }
546
547 #[test]
548 fn test_dogstatsd_gauge_with_tags() {
549 let gauge = Gauge::new();
550 gauge.set(-10);
551
552 let mut output = String::new();
553 gauge.export_dogstatsd(&mut output, "memory.used", &[("region", "us-east")]);
554
555 assert_eq!(output, "memory.used:-10|g|#region:us-east\n");
556 }
557
558 #[test]
559 fn test_dogstatsd_histogram() {
560 let histogram = Histogram::new(&[10, 100], 4);
561 histogram.record(5);
562 histogram.record(50);
563 histogram.record(500);
564
565 let mut output = String::new();
566 histogram.export_dogstatsd(&mut output, "latency", &[]);
567
568 assert!(output.contains("latency.count:3|c\n"));
569 assert!(output.contains("latency.sum:555|c\n"));
570 }
571
572 #[test]
573 fn test_dogstatsd_histogram_with_tags() {
574 let histogram = Histogram::new(&[100], 4);
575 histogram.record(50);
576 histogram.record(150);
577
578 let mut output = String::new();
579 histogram.export_dogstatsd(&mut output, "latency", &[("service", "api")]);
580
581 assert!(output.contains("latency.count:2|c|#service:api\n"));
582 assert!(output.contains("latency.sum:200|c|#service:api\n"));
583 }
584
585 #[test]
586 fn test_dogstatsd_distribution() {
587 let dist = Distribution::new(4);
588 dist.record(100);
589 dist.record(200);
590 dist.record(300);
591
592 let mut output = String::new();
593 dist.export_dogstatsd(&mut output, "latency", &[]);
594
595 assert!(output.contains("latency:96|d\n"));
596 assert!(output.contains("latency:192|d\n"));
597 assert!(output.contains("latency:384|d\n"));
598 }
599
600 #[test]
601 fn test_dogstatsd_distribution_with_tags() {
602 let dist = Distribution::new(4);
603 dist.record(50);
604 dist.record(150);
605
606 let mut output = String::new();
607 dist.export_dogstatsd(&mut output, "latency", &[("service", "api")]);
608
609 assert!(output.contains("latency:48|d|#service:api\n"));
610 assert!(output.contains("latency:192|d|#service:api\n"));
611 }
612
613 #[test]
614 fn test_dogstatsd_distribution_empty() {
615 let dist = Distribution::new(4);
616
617 let mut output = String::new();
618 dist.export_dogstatsd(&mut output, "latency", &[]);
619
620 assert!(output.is_empty());
621 }
622
623 #[test]
624 fn test_dogstatsd_distribution_sample_rate() {
625 let dist = Distribution::new(4);
626 dist.record(100);
627 dist.record(100);
628 dist.record(100);
629
630 let mut output = String::new();
631 dist.export_dogstatsd(&mut output, "latency", &[]);
632
633 let line = output.lines().next().expect("should have a line");
634 assert!(line.starts_with("latency:96|d|@"));
635 assert!(line.contains("|@0.3333333333333333"));
636 }
637
638 #[test]
639 fn test_dogstatsd_dynamic_counter() {
640 let counter = DynamicCounter::new(4);
641 counter.add(&[("endpoint", "ep1"), ("method", "GET")], 3);
642
643 let mut output = String::new();
644 counter.export_dogstatsd(&mut output, "requests", &[("env", "prod")]);
645
646 assert!(output.contains("requests:3|c|#"));
647 assert!(output.contains("endpoint:ep1"));
648 assert!(output.contains("method:GET"));
649 assert!(output.contains("env:prod"));
650 }
651
652 #[test]
653 fn test_dogstatsd_dynamic_distribution() {
654 let dist = DynamicDistribution::new(4);
655 dist.record(&[("endpoint", "ep1")], 100);
656 dist.record(&[("endpoint", "ep1")], 100);
657
658 let mut output = String::new();
659 dist.export_dogstatsd(&mut output, "latency", &[("env", "prod")]);
660
661 assert!(output.contains("latency:96|d|@0.5|#endpoint:ep1,env:prod"));
662 }
663}