1use chrono::{DateTime, Utc};
2
3pub fn format_for_send<M, I, S>(
4 in_metric: &M,
5 in_namespace: &str,
6 tags: I,
7 default_tags: &[u8],
8) -> Vec<u8>
9where
10 M: Metric,
11 I: IntoIterator<Item = S>,
12 S: AsRef<str>,
13{
14 let metric = in_metric.metric_type_format();
15 let namespace = if in_metric.uses_namespace() {
16 in_namespace
17 } else {
18 ""
19 };
20 let mut buf = Vec::with_capacity(metric.len() + namespace.len());
21
22 if !namespace.is_empty() {
23 buf.extend_from_slice(namespace.as_bytes());
24 buf.extend_from_slice(b".");
25 }
26
27 buf.extend_from_slice(metric.as_bytes());
28
29 let mut tags_iter = tags.into_iter();
30 let mut next_tag = tags_iter.next();
31 let has_tags = next_tag.is_some();
32
33 if next_tag.is_some() || !default_tags.is_empty() {
34 buf.extend_from_slice(b"|#");
35 }
36
37 while next_tag.is_some() {
38 buf.extend_from_slice(next_tag.unwrap().as_ref().as_bytes());
39
40 next_tag = tags_iter.next();
41
42 if next_tag.is_some() {
43 buf.extend_from_slice(b",");
44 }
45 }
46
47 if !default_tags.is_empty() {
48 if has_tags {
49 buf.extend_from_slice(b",")
50 }
51
52 buf.extend_from_slice(default_tags);
53 }
54
55 buf
56}
57
58pub trait Metric {
59 fn metric_type_format(&self) -> String;
60
61 fn uses_namespace(&self) -> bool {
62 true
63 }
64}
65
66pub enum CountMetric<'a> {
67 Incr(&'a str, i64),
68 Decr(&'a str, i64),
69 Arbitrary(&'a str, i64),
70}
71
72impl<'a> Metric for CountMetric<'a> {
73 fn metric_type_format(&self) -> String {
76 match *self {
77 CountMetric::Incr(stat, amount) => {
78 let mut buf = String::with_capacity(3 + stat.len() + amount.to_string().len() + 3);
79 buf.push_str(stat);
80 buf.push_str(&format!(":{}|c", amount));
81 buf
82 }
83 CountMetric::Decr(stat, amount) => {
84 let mut buf = String::with_capacity(3 + stat.len() + amount.to_string().len() + 4);
85 buf.push_str(stat);
86 buf.push_str(&format!(":{}|c", -amount));
87 buf
88 }
89 CountMetric::Arbitrary(stat, amount) => {
90 let mut buf = String::with_capacity(3 + stat.len() + 23);
91 buf.push_str(stat);
92 buf.push(':');
93 buf.push_str(&amount.to_string());
94 buf.push_str("|c");
95 buf
96 }
97 }
98 }
99}
100
101pub struct TimeMetric<'a> {
102 start_time: &'a DateTime<Utc>,
103 end_time: &'a DateTime<Utc>,
104 stat: &'a str,
105}
106
107impl<'a> Metric for TimeMetric<'a> {
108 fn metric_type_format(&self) -> String {
110 let dur = self.end_time.signed_duration_since(*self.start_time);
111 let mut buf = String::with_capacity(3 + self.stat.len() + 11);
112 buf.push_str(self.stat);
113 buf.push(':');
114 buf.push_str(&dur.num_milliseconds().to_string());
115 buf.push_str("|ms");
116 buf
117 }
118}
119
120impl<'a> TimeMetric<'a> {
121 pub fn new(stat: &'a str, start_time: &'a DateTime<Utc>, end_time: &'a DateTime<Utc>) -> Self {
122 TimeMetric {
123 start_time,
124 end_time,
125 stat,
126 }
127 }
128}
129
130pub struct TimingMetric<'a> {
131 ms: i64,
132 stat: &'a str,
133}
134
135impl<'a> Metric for TimingMetric<'a> {
136 fn metric_type_format(&self) -> String {
138 let ms = self.ms.to_string();
139 let mut buf = String::with_capacity(3 + self.stat.len() + ms.len());
140 buf.push_str(self.stat);
141 buf.push(':');
142 buf.push_str(&ms);
143 buf.push_str("|ms");
144 buf
145 }
146}
147
148impl<'a> TimingMetric<'a> {
149 pub fn new(stat: &'a str, ms: i64) -> Self {
150 TimingMetric { ms, stat }
151 }
152}
153
154pub struct GaugeMetric<'a> {
155 stat: &'a str,
156 val: &'a str,
157}
158
159impl<'a> Metric for GaugeMetric<'a> {
160 fn metric_type_format(&self) -> String {
162 let mut buf = String::with_capacity(3 + self.stat.len() + self.val.len());
163 buf.push_str(self.stat);
164 buf.push(':');
165 buf.push_str(self.val);
166 buf.push_str("|g");
167 buf
168 }
169}
170
171impl<'a> GaugeMetric<'a> {
172 pub fn new(stat: &'a str, val: &'a str) -> Self {
173 GaugeMetric { stat, val }
174 }
175}
176
177pub struct HistogramMetric<'a> {
178 stat: &'a str,
179 val: &'a str,
180}
181
182impl<'a> Metric for HistogramMetric<'a> {
183 fn metric_type_format(&self) -> String {
185 let mut buf = String::with_capacity(3 + self.stat.len() + self.val.len());
186 buf.push_str(self.stat);
187 buf.push(':');
188 buf.push_str(self.val);
189 buf.push_str("|h");
190 buf
191 }
192}
193
194impl<'a> HistogramMetric<'a> {
195 pub fn new(stat: &'a str, val: &'a str) -> Self {
196 HistogramMetric { stat, val }
197 }
198}
199
200pub struct DistributionMetric<'a> {
201 stat: &'a str,
202 val: &'a str,
203}
204
205impl<'a> Metric for DistributionMetric<'a> {
206 fn metric_type_format(&self) -> String {
208 let mut buf = String::with_capacity(3 + self.stat.len() + self.val.len());
209 buf.push_str(self.stat);
210 buf.push(':');
211 buf.push_str(self.val);
212 buf.push_str("|d");
213 buf
214 }
215}
216
217impl<'a> DistributionMetric<'a> {
218 pub fn new(stat: &'a str, val: &'a str) -> Self {
219 DistributionMetric { stat, val }
220 }
221}
222
223pub struct SetMetric<'a> {
224 stat: &'a str,
225 val: &'a str,
226}
227
228impl<'a> Metric for SetMetric<'a> {
229 fn metric_type_format(&self) -> String {
231 let mut buf = String::with_capacity(3 + self.stat.len() + self.val.len());
232 buf.push_str(self.stat);
233 buf.push(':');
234 buf.push_str(self.val);
235 buf.push_str("|s");
236 buf
237 }
238}
239
240impl<'a> SetMetric<'a> {
241 pub fn new(stat: &'a str, val: &'a str) -> Self {
242 SetMetric { stat, val }
243 }
244}
245
246#[derive(Clone, Copy, Debug)]
248pub enum ServiceStatus {
249 OK,
251 Warning,
253 Critical,
255 Unknown,
257}
258
259impl ServiceStatus {
260 fn to_int(self) -> i32 {
261 match self {
262 ServiceStatus::OK => 0,
263 ServiceStatus::Warning => 1,
264 ServiceStatus::Critical => 2,
265 ServiceStatus::Unknown => 3,
266 }
267 }
268}
269
270#[derive(Default, Clone, Copy, Debug)]
272pub struct ServiceCheckOptions<'a> {
273 pub timestamp: Option<i32>,
275 pub hostname: Option<&'a str>,
277 pub message: Option<&'a str>,
279}
280
281impl<'a> ServiceCheckOptions<'a> {
282 fn len(&self) -> usize {
283 let mut length = 0;
284 length += self.timestamp.map_or(0, |ts| format!("{}", ts).len() + 3);
285 length += self.hostname.map_or(0, |host| host.len() + 3);
286 length += self.message.map_or(0, |msg| msg.len() + 3);
287 length
288 }
289}
290
291pub struct ServiceCheck<'a> {
292 stat: &'a str,
293 val: ServiceStatus,
294 options: ServiceCheckOptions<'a>,
295}
296
297impl<'a> Metric for ServiceCheck<'a> {
298 fn uses_namespace(&self) -> bool {
299 false
300 }
301
302 fn metric_type_format(&self) -> String {
304 let mut buf = String::with_capacity(6 + self.stat.len() + self.options.len());
305 buf.push_str("_sc|");
306 buf.push_str(self.stat);
307 buf.push('|');
308 buf.push_str(&format!("{}", self.val.to_int()));
309
310 if let Some(timestamp) = self.options.timestamp {
311 buf.push_str("|d:");
312 buf.push_str(&format!("{}", timestamp));
313 }
314
315 if let Some(hostname) = self.options.hostname {
316 buf.push_str("|h:");
317 buf.push_str(hostname);
318 }
319
320 if let Some(message) = self.options.message {
321 buf.push_str("|m:");
322 buf.push_str(message);
323 }
324
325 buf
326 }
327}
328
329impl<'a> ServiceCheck<'a> {
330 pub fn new(stat: &'a str, val: ServiceStatus, options: ServiceCheckOptions<'a>) -> Self {
331 ServiceCheck { stat, val, options }
332 }
333}
334
335#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
337pub enum EventPriority {
338 Low,
340 #[default]
341 Normal,
343}
344
345impl EventPriority {
346 pub fn as_str(&self) -> &'static str {
348 match self {
349 EventPriority::Low => "low",
350 EventPriority::Normal => "normal",
351 }
352 }
353}
354
355#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
357pub enum EventAlertType {
358 #[default]
359 Info,
361 Warning,
363 Error,
365 Success,
367}
368
369impl EventAlertType {
371 pub fn as_str(&self) -> &'static str {
373 match self {
374 EventAlertType::Info => "info",
375 EventAlertType::Warning => "warning",
376 EventAlertType::Error => "error",
377 EventAlertType::Success => "success",
378 }
379 }
380}
381
382pub struct Event<'a> {
384 title: &'a str,
385 text: &'a str,
386 timestamp: Option<u64>,
387 hostname: Option<&'a str>,
388 aggregation_key: Option<&'a str>,
389 priority: Option<EventPriority>,
390 source_type_name: Option<&'a str>,
391 alert_type: Option<EventAlertType>,
392}
393
394impl<'a> Metric for Event<'a> {
395 fn uses_namespace(&self) -> bool {
396 false
397 }
398
399 fn metric_type_format(&self) -> String {
400 let title_len = self.title.len().to_string();
401 let text_len = self.text.len().to_string();
402 let mut buf = String::with_capacity(
403 self.title.len() + self.text.len() + title_len.len() + text_len.len() + 6,
404 );
405 buf.push_str("_e{");
406 buf.push_str(&title_len);
407 buf.push(',');
408 buf.push_str(&text_len);
409 buf.push_str("}:");
410 buf.push_str(self.title);
411 buf.push('|');
412 buf.push_str(self.text);
413
414 if let Some(timestamp) = self.timestamp {
416 buf.push_str("|d:");
417 buf.push_str(×tamp.to_string());
418 }
419 if let Some(hostname) = self.hostname {
420 buf.push_str("|h:");
421 buf.push_str(hostname);
422 }
423 if let Some(aggregation_key) = self.aggregation_key {
424 buf.push_str("|k:");
425 buf.push_str(aggregation_key);
426 }
427 if let Some(priority) = self.priority {
428 buf.push_str("|p:");
429 buf.push_str(priority.as_str());
430 }
431 if let Some(source_type_name) = self.source_type_name {
432 buf.push_str("|s:");
433 buf.push_str(source_type_name);
434 }
435 if let Some(alert_type) = self.alert_type {
436 buf.push_str("|t:");
437 buf.push_str(alert_type.as_str());
438 }
439
440 buf
441 }
442}
443
444impl<'a> Event<'a> {
445 pub fn new(title: &'a str, text: &'a str) -> Self {
446 Event {
447 title,
448 text,
449 timestamp: None,
450 hostname: None,
451 aggregation_key: None,
452 priority: None,
453 source_type_name: None,
454 alert_type: None,
455 }
456 }
457
458 pub fn with_timestamp(mut self, timestamp: u64) -> Self {
459 self.timestamp = Some(timestamp);
460 self
461 }
462
463 pub fn with_hostname(mut self, hostname: &'a str) -> Self {
464 self.hostname = Some(hostname);
465 self
466 }
467
468 pub fn with_aggregation_key(mut self, aggregation_key: &'a str) -> Self {
469 self.aggregation_key = Some(aggregation_key);
470 self
471 }
472
473 pub fn with_priority(mut self, priority: EventPriority) -> Self {
474 self.priority = Some(priority);
475 self
476 }
477
478 pub fn with_source_type_name(mut self, source_type_name: &'a str) -> Self {
479 self.source_type_name = Some(source_type_name);
480 self
481 }
482
483 pub fn with_alert_type(mut self, alert_type: EventAlertType) -> Self {
484 self.alert_type = Some(alert_type);
485 self
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use chrono::{TimeZone, Utc};
493
494 #[test]
495 fn test_format_for_send_no_tags() {
496 assert_eq!(
497 &b"namespace.foo:1|c"[..],
498 &format_for_send(
499 &CountMetric::Incr("foo", 1),
500 "namespace",
501 &[] as &[String],
502 &String::default().into_bytes()
503 )[..]
504 )
505 }
506
507 #[test]
508 fn test_format_for_optional_value_in_increment() {
509 assert_eq!(
510 &b"namespace.foo:20|c"[..],
511 &format_for_send(
512 &CountMetric::Incr("foo", 20),
513 "namespace",
514 &[] as &[String],
515 &String::default().into_bytes()
516 )[..]
517 )
518 }
519
520 #[test]
521 fn test_format_for_send_no_namespace() {
522 assert_eq!(
523 &b"foo:1|c|#tag:1,tag:2"[..],
524 &format_for_send(
525 &CountMetric::Incr("foo", 1),
526 "",
527 ["tag:1", "tag:2"],
528 &String::default().into_bytes()
529 )[..]
530 )
531 }
532
533 #[test]
534 fn test_format_for_no_default_tags() {
535 assert_eq!(
536 &b"namespace.foo:1|c|#tag:1,tag:2,defaultag:3,seconddefault:4"[..],
537 &format_for_send(
538 &CountMetric::Incr("foo", 1),
539 "namespace",
540 ["tag:1", "tag:2"],
541 &String::from("defaultag:3,seconddefault:4").into_bytes()
542 )[..]
543 )
544 }
545
546 #[test]
547 fn test_format_for_send_everything() {
548 assert_eq!(
549 &b"namespace.foo:1|c|#tag:1,tag:2,defaultag:3,seconddefault:4"[..],
550 &format_for_send(
551 &CountMetric::Incr("foo", 1),
552 "namespace",
553 ["tag:1", "tag:2"],
554 &String::from("defaultag:3,seconddefault:4").into_bytes()
555 )[..]
556 )
557 }
558
559 #[test]
560 fn test_format_for_send_everything_omit_namespace() {
561 assert_eq!(
562 &b"_e{5,4}:title|text|#tag:1,tag:2"[..],
563 &format_for_send(
564 &Event::new("title", "text"),
565 "namespace",
566 ["tag:1", "tag:2"],
567 &String::default().into_bytes()
568 )[..]
569 )
570 }
571
572 #[test]
573 fn test_format_with_only_default_tags() {
574 assert_eq!(
575 &b"namespace.foo:1|c|#defaultag:3,seconddefault:4"[..],
576 &format_for_send(
577 &CountMetric::Incr("foo", 1),
578 "namespace",
579 &[] as &[String],
580 &String::from("defaultag:3,seconddefault:4").into_bytes()
581 )[..]
582 )
583 }
584
585 #[test]
586 fn test_count_incr_metric() {
587 let metric = CountMetric::Incr("incr", 1);
588
589 assert_eq!("incr:1|c", metric.metric_type_format())
590 }
591
592 #[test]
593 fn test_count_decr_metric() {
594 let metric = CountMetric::Decr("decr", 1);
595
596 assert_eq!("decr:-1|c", metric.metric_type_format())
597 }
598
599 #[test]
600 fn test_count_decr_by_value_metric() {
601 let metric = CountMetric::Decr("decr", 35);
602
603 assert_eq!("decr:-35|c", metric.metric_type_format())
604 }
605
606 #[test]
607 fn test_count_metric() {
608 let metric = CountMetric::Arbitrary("arb", 54321);
609 assert_eq!("arb:54321|c", metric.metric_type_format());
610 let metric = CountMetric::Arbitrary("arb", -12345);
611 assert_eq!("arb:-12345|c", metric.metric_type_format());
612 let metric = CountMetric::Arbitrary("arb", 0);
613 assert_eq!("arb:0|c", metric.metric_type_format());
614 }
615
616 #[test]
617 fn test_time_metric() {
618 let start_time = Utc.with_ymd_and_hms(2016, 4, 24, 0, 0, 0).unwrap();
619 let end_time = Utc
620 .timestamp_millis_opt(start_time.timestamp_millis() + 900)
621 .unwrap();
622 let metric = TimeMetric::new("time", &start_time, &end_time);
623
624 assert_eq!("time:900|ms", metric.metric_type_format())
625 }
626
627 #[test]
628 fn test_timing_metric() {
629 let metric = TimingMetric::new("timing", 720);
630
631 assert_eq!("timing:720|ms", metric.metric_type_format())
632 }
633
634 #[test]
635 fn test_gauge_metric() {
636 let metric = GaugeMetric::new("gauge", "12345");
637
638 assert_eq!("gauge:12345|g", metric.metric_type_format())
639 }
640
641 #[test]
642 fn test_histogram_metric() {
643 let metric = HistogramMetric::new("histogram", "67890");
644
645 assert_eq!("histogram:67890|h", metric.metric_type_format())
646 }
647
648 #[test]
649 fn test_distribution_metric() {
650 let metric = DistributionMetric::new("distribution", "67890");
651
652 assert_eq!("distribution:67890|d", metric.metric_type_format())
653 }
654
655 #[test]
656 fn test_set_metric() {
657 let metric = SetMetric::new("set", "13579");
658
659 assert_eq!("set:13579|s", metric.metric_type_format())
660 }
661
662 #[test]
663 fn test_service_check() {
664 let metric = ServiceCheck::new(
665 "redis.can_connect",
666 ServiceStatus::Warning,
667 ServiceCheckOptions::default(),
668 );
669
670 assert_eq!("_sc|redis.can_connect|1", metric.metric_type_format())
671 }
672
673 #[test]
674 fn test_service_check_with_timestamp() {
675 let options = ServiceCheckOptions {
676 timestamp: Some(1234567890),
677 ..Default::default()
678 };
679 let metric = ServiceCheck::new("redis.can_connect", ServiceStatus::Warning, options);
680
681 assert_eq!(
682 "_sc|redis.can_connect|1|d:1234567890",
683 metric.metric_type_format()
684 )
685 }
686
687 #[test]
688 fn test_service_check_with_hostname() {
689 let options = ServiceCheckOptions {
690 hostname: Some("my_server.localhost"),
691 ..Default::default()
692 };
693 let metric = ServiceCheck::new("redis.can_connect", ServiceStatus::Warning, options);
694
695 assert_eq!(
696 "_sc|redis.can_connect|1|h:my_server.localhost",
697 metric.metric_type_format()
698 )
699 }
700
701 #[test]
702 fn test_service_check_with_message() {
703 let options = ServiceCheckOptions {
704 message: Some("Service is possibly down"),
705 ..Default::default()
706 };
707 let metric = ServiceCheck::new("redis.can_connect", ServiceStatus::Warning, options);
708
709 assert_eq!(
710 "_sc|redis.can_connect|1|m:Service is possibly down",
711 metric.metric_type_format()
712 )
713 }
714
715 #[test]
716 fn test_service_check_with_all() {
717 let options = ServiceCheckOptions {
718 timestamp: Some(1234567890),
719 hostname: Some("my_server.localhost"),
720 message: Some("Service is possibly down"),
721 };
722 let metric = ServiceCheck::new("redis.can_connect", ServiceStatus::Warning, options);
723
724 assert_eq!(
725 "_sc|redis.can_connect|1|d:1234567890|h:my_server.localhost|m:Service is possibly down",
726 metric.metric_type_format()
727 )
728 }
729
730 #[test]
731 fn test_event() {
732 let metric = Event::new("Event Title", "Event Body - Something Happened");
733
734 assert_eq!(
735 "_e{11,31}:Event Title|Event Body - Something Happened",
736 metric.metric_type_format()
737 )
738 }
739
740 #[test]
741 fn test_event_with_options() {
742 let metric = Event::new("Event Title", "Event Body - Something Happened")
743 .with_timestamp(1638480000)
744 .with_hostname("localhost")
745 .with_aggregation_key("service_down")
746 .with_priority(EventPriority::Normal)
747 .with_source_type_name("monitoring")
748 .with_alert_type(EventAlertType::Error);
749
750 assert_eq!(
751 "_e{11,31}:Event Title|Event Body - Something Happened|d:1638480000|h:localhost|k:service_down|p:normal|s:monitoring|t:error",
752 metric.metric_type_format()
753 )
754 }
755}
756
757#[cfg(all(feature = "unstable", test))]
758mod bench {
759 extern crate test;
760
761 use self::test::Bencher;
762 use super::*;
763
764 struct NullMetric;
765
766 impl Metric for NullMetric {
767 fn metric_type_format(&self) -> String {
768 String::new()
769 }
770 }
771
772 #[bench]
773 fn bench_format_for_send(b: &mut Bencher) {
774 let metric = NullMetric;
775
776 b.iter(|| {
777 format_for_send(
778 &metric,
779 "foo",
780 &["bar", "baz"],
781 &String::default().into_bytes(),
782 );
783 })
784 }
785
786 #[bench]
787 fn bench_set_metric(b: &mut Bencher) {
788 let metric = SetMetric {
789 stat: "blahblahblah-blahblahblah",
790 val: "valuel",
791 };
792
793 b.iter(|| metric.metric_type_format())
794 }
795
796 #[bench]
797 fn bench_set_counter(b: &mut Bencher) {
798 let metric = CountMetric::Incr("foo", 1);
799
800 b.iter(|| metric.metric_type_format())
801 }
802}