Skip to main content

dogstatsd/
metrics.rs

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    // my_count:1|c
74    // my_count:-1|c
75    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    // my_stat:500|ms
109    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    // my_stat:500|ms
137    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    // my_gauge:1000|g
161    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    // my_histogram:1000|h
184    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    // my_distribution:1000|d
207    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    // my_set:45|s
230    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/// Represents the different states a service can be in
247#[derive(Clone, Copy, Debug)]
248pub enum ServiceStatus {
249    /// OK State
250    OK,
251    /// Warning State
252    Warning,
253    /// Critical State
254    Critical,
255    /// Unknown State
256    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/// Struct for adding optional pieces to a service check
271#[derive(Default, Clone, Copy, Debug)]
272pub struct ServiceCheckOptions<'a> {
273    /// An optional timestamp to include with the check
274    pub timestamp: Option<i32>,
275    /// An optional hostname to include with the check
276    pub hostname: Option<&'a str>,
277    /// An optional message to include with the check
278    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    // _sc|my_service.can_connect|1
303    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/// Represents priority levels for an event.
336#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
337pub enum EventPriority {
338    ///low
339    Low,
340    #[default]
341    ///normal
342    Normal,
343}
344
345impl EventPriority {
346    /// convert to string
347    pub fn as_str(&self) -> &'static str {
348        match self {
349            EventPriority::Low => "low",
350            EventPriority::Normal => "normal",
351        }
352    }
353}
354
355/// Represents alert types for an event.
356#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
357pub enum EventAlertType {
358    #[default]
359    /// info
360    Info,
361    /// warning
362    Warning,
363    /// error
364    Error,
365    ///success
366    Success,
367}
368
369//
370impl EventAlertType {
371    /// convert to string
372    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
382// https://docs.datadoghq.com/developers/dogstatsd/datagram_shell/?tab=events
383pub 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        // Add optional fields if they are present
415        if let Some(timestamp) = self.timestamp {
416            buf.push_str("|d:");
417            buf.push_str(&timestamp.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}