simple_metrics/
lib.rs

1pub mod labels;
2pub mod labels_builder;
3pub mod macros;
4pub mod metric_def;
5pub mod store;
6
7pub use labels::Labels;
8pub use labels_builder::LabelsBuilder;
9pub use metric_def::{MetricDef, ToMetricDef};
10pub use store::MetricStore;
11
12/// Internal trait for rendering a collection of metrics into a
13/// string.
14pub trait RenderIntoMetrics {
15    fn render_into_metrics(&self, namespace: Option<&str>) -> String;
16}
17
18#[derive(Debug, Clone)]
19pub enum MetricValue {
20    I32(i32),
21    I64(i64),
22    I128(i128),
23    U32(u32),
24    U64(u64),
25    U128(u128),
26    F64(f64),
27    Bool(bool),
28}
29
30impl MetricValue {
31    pub fn render(&self) -> String {
32        match self {
33            MetricValue::I32(v) => v.to_string(),
34            MetricValue::I64(v) => v.to_string(),
35            MetricValue::I128(v) => v.to_string(),
36            MetricValue::U32(v) => v.to_string(),
37            MetricValue::U64(v) => v.to_string(),
38            MetricValue::U128(v) => v.to_string(),
39            MetricValue::F64(v) => format!("{}", v),
40            MetricValue::Bool(v) => {
41                if *v {
42                    1_i64.to_string()
43                } else {
44                    0_i64.to_string()
45                }
46            }
47        }
48    }
49}
50
51impl From<i32> for MetricValue {
52    fn from(v: i32) -> Self {
53        MetricValue::I32(v)
54    }
55}
56
57impl From<i64> for MetricValue {
58    fn from(v: i64) -> Self {
59        MetricValue::I64(v)
60    }
61}
62
63impl From<i128> for MetricValue {
64    fn from(v: i128) -> Self {
65        MetricValue::I128(v)
66    }
67}
68
69impl From<u32> for MetricValue {
70    fn from(v: u32) -> Self {
71        MetricValue::U32(v)
72    }
73}
74
75impl From<u64> for MetricValue {
76    fn from(v: u64) -> Self {
77        MetricValue::U64(v)
78    }
79}
80
81impl From<u128> for MetricValue {
82    fn from(v: u128) -> Self {
83        MetricValue::U128(v)
84    }
85}
86
87impl From<f64> for MetricValue {
88    fn from(v: f64) -> Self {
89        MetricValue::F64(v)
90    }
91}
92
93impl From<bool> for MetricValue {
94    fn from(v: bool) -> Self {
95        MetricValue::Bool(v)
96    }
97}
98
99/// Sample holds a single measurement of metrics
100#[derive(Debug, Clone)]
101pub struct Sample {
102    labels: Labels,
103    value: MetricValue,
104}
105
106impl Sample {
107    pub fn new<T: Into<MetricValue>>(labels: &Labels, value: T) -> Self {
108        Self {
109            labels: labels.clone(),
110            value: value.into(),
111        }
112    }
113}
114
115#[derive(Debug, Clone, PartialEq)]
116pub enum Error {
117    /// InvalidMetricName means the metric name doesn't comply with
118    /// the Prometheus data model
119    ///
120    /// For more details, see
121    /// <https://prometheus.io/docs/concepts/data_model/>
122    InvalidMetricName(String),
123
124    /// InvalidLabelName means the label name doesn't comply with the
125    /// Prometheus data model.
126    ///
127    /// For more details, see
128    /// <https://prometheus.io/docs/concepts/data_model/>
129    InvalidLabelName(String),
130}
131
132/// Metric type
133///
134/// This library doesn't distinguish between a counter, gauge and
135/// histogram metric types. It's your responsibility to ensure that
136/// the counter only increases and histogram provides consistent data.
137#[derive(Clone, Debug)]
138pub enum MetricType {
139    /// A counter is a cumulative metric that represents a single
140    /// monotonically increasing counter whose value can only increase
141    /// or be reset to zero on restart.
142    // TODO: Make counter work only with Option<u64>?
143    Counter,
144
145    /// A gauge is a metric that represents a single numerical value
146    /// that can arbitrarily go up and down.
147    Gauge,
148
149    /// A histogram samples observations (usually things like request
150    /// durations or response sizes) and counts them in configurable
151    /// buckets.
152    Histogram,
153}
154
155impl std::fmt::Display for MetricType {
156    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
157        match self {
158            MetricType::Counter => write!(f, "counter"),
159            MetricType::Gauge => write!(f, "gauge"),
160            MetricType::Histogram => write!(f, "histogram"),
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use crate::metric_def::{MetricDef, ToMetricDef};
168    use crate::{labels_builder::LabelsBuilder, store::MetricStore};
169
170    use super::*;
171
172    pub struct State {
173        name: String,
174        client: String,
175        health: bool,
176        height: i64,
177        delta: f64,
178        maybe: Option<i64>,
179    }
180
181    #[derive(Clone, Eq, Hash, PartialEq, Ord, PartialOrd)]
182    pub enum ServiceMetric {
183        WorkerHealth,
184        ServiceHeight,
185        ServiceDelta,
186        Maybe,
187        Maybe2,
188    }
189
190    impl ToMetricDef for ServiceMetric {
191        fn to_metric_def(&self) -> MetricDef {
192            match self {
193                ServiceMetric::WorkerHealth => {
194                    MetricDef::new("worker_health", "worker health", MetricType::Gauge).unwrap()
195                }
196                ServiceMetric::ServiceHeight => {
197                    MetricDef::new("service_height", "service height", MetricType::Gauge).unwrap()
198                }
199                ServiceMetric::ServiceDelta => {
200                    MetricDef::gauge("service_delta", "service delta").unwrap()
201                }
202                ServiceMetric::Maybe => MetricDef::gauge("service_maybe", "service maybe").unwrap(),
203                ServiceMetric::Maybe2 => {
204                    MetricDef::gauge("service_maybe2", "service maybe2").unwrap()
205                }
206            }
207        }
208    }
209
210    #[test]
211    fn complex() {
212        let states = vec![
213            State {
214                name: "a".into(),
215                client: "woot".into(),
216                health: true,
217                height: 100,
218                delta: 1.0,
219                maybe: Some(100),
220            },
221            State {
222                name: "b".into(),
223                client: "woot".into(),
224                health: true,
225                height: 200,
226                delta: 2.2,
227                maybe: Some(100),
228            },
229            State {
230                name: "c".into(),
231                client: "meh".into(),
232                health: true,
233                height: 300,
234                delta: 3.0,
235                maybe: Some(100),
236            },
237            State {
238                name: "d".into(),
239                client: "meh".into(),
240                health: false,
241                height: 0,
242                delta: 291283791287391287391.123,
243                maybe: None,
244            },
245        ];
246
247        let static_labels_builder = LabelsBuilder::from([("process", "simple-metrics")]);
248        let static_labels = static_labels_builder.build().unwrap();
249
250        let namespace = String::from("test_exporter");
251        let mut store: MetricStore<ServiceMetric> =
252            MetricStore::new().with_static_labels(static_labels);
253
254        for s in states {
255            let common_builder = LabelsBuilder::from([("name", s.name)]);
256            let common = common_builder.build().unwrap();
257
258            store.add_sample(ServiceMetric::WorkerHealth, Sample::new(&common, s.health));
259
260            let lbs = common.builder().with("client", s.client).build().unwrap();
261
262            store.add_value(ServiceMetric::ServiceHeight, &lbs, s.height);
263
264            if let Some(maybe) = s.maybe {
265                store.add_value(ServiceMetric::Maybe, &lbs, maybe)
266            }
267
268            store.maybe_add_value(ServiceMetric::Maybe2, &lbs, s.maybe);
269
270            let lbs_p = lbs.builder().with("type", "pos").build().unwrap();
271            store.add_value(ServiceMetric::ServiceDelta, &lbs_p, s.delta);
272
273            let lbs_n = lbs.builder().with("type", "neg").build().unwrap();
274            store.add_value(ServiceMetric::ServiceDelta, &lbs_n, -s.delta);
275        }
276
277        let actual1 = store.render_into_metrics(Some(&namespace));
278        let actual2 = store
279            .to_rich_samples()
280            .render_into_metrics(Some(&namespace));
281
282        let expected = r#"# HELP test_exporter_worker_health worker health
283# TYPE test_exporter_worker_health gauge
284test_exporter_worker_health{name="a",process="simple-metrics"} 1
285test_exporter_worker_health{name="b",process="simple-metrics"} 1
286test_exporter_worker_health{name="c",process="simple-metrics"} 1
287test_exporter_worker_health{name="d",process="simple-metrics"} 0
288
289# HELP test_exporter_service_height service height
290# TYPE test_exporter_service_height gauge
291test_exporter_service_height{client="woot",name="a",process="simple-metrics"} 100
292test_exporter_service_height{client="woot",name="b",process="simple-metrics"} 200
293test_exporter_service_height{client="meh",name="c",process="simple-metrics"} 300
294test_exporter_service_height{client="meh",name="d",process="simple-metrics"} 0
295
296# HELP test_exporter_service_delta service delta
297# TYPE test_exporter_service_delta gauge
298test_exporter_service_delta{client="woot",name="a",process="simple-metrics",type="pos"} 1
299test_exporter_service_delta{client="woot",name="a",process="simple-metrics",type="neg"} -1
300test_exporter_service_delta{client="woot",name="b",process="simple-metrics",type="pos"} 2.2
301test_exporter_service_delta{client="woot",name="b",process="simple-metrics",type="neg"} -2.2
302test_exporter_service_delta{client="meh",name="c",process="simple-metrics",type="pos"} 3
303test_exporter_service_delta{client="meh",name="c",process="simple-metrics",type="neg"} -3
304test_exporter_service_delta{client="meh",name="d",process="simple-metrics",type="pos"} 291283791287391300000
305test_exporter_service_delta{client="meh",name="d",process="simple-metrics",type="neg"} -291283791287391300000
306
307# HELP test_exporter_service_maybe service maybe
308# TYPE test_exporter_service_maybe gauge
309test_exporter_service_maybe{client="woot",name="a",process="simple-metrics"} 100
310test_exporter_service_maybe{client="woot",name="b",process="simple-metrics"} 100
311test_exporter_service_maybe{client="meh",name="c",process="simple-metrics"} 100
312
313# HELP test_exporter_service_maybe2 service maybe2
314# TYPE test_exporter_service_maybe2 gauge
315test_exporter_service_maybe2{client="woot",name="a",process="simple-metrics"} 100
316test_exporter_service_maybe2{client="woot",name="b",process="simple-metrics"} 100
317test_exporter_service_maybe2{client="meh",name="c",process="simple-metrics"} 100
318"#;
319        assert_eq!(actual1, expected);
320        assert_eq!(actual2, expected);
321    }
322
323    pub struct SimpleState {
324        pub name: String,
325        pub health: bool,
326        pub height: i64,
327    }
328
329    #[test]
330    fn simple() {
331        let states = vec![
332            SimpleState {
333                name: "a".into(),
334                health: true,
335                height: 100,
336            },
337            SimpleState {
338                name: "b".into(),
339                health: false,
340                height: 200,
341            },
342        ];
343
344        let static_labels = LabelsBuilder::new()
345            .with("process", "simple-metrics")
346            .build()
347            .unwrap();
348
349        let mut store: MetricStore<ServiceMetric> =
350            MetricStore::new().with_static_labels(static_labels);
351
352        for s in states {
353            let common = LabelsBuilder::from([("name", s.name)]).build().unwrap();
354
355            store.add_sample(ServiceMetric::WorkerHealth, Sample::new(&common, s.health));
356            store.add_value(ServiceMetric::ServiceHeight, &common, s.height)
357        }
358
359        let _cloned_store = store.clone();
360
361        let actual = store.render_into_metrics(None);
362        println!("{}", actual);
363
364        let expected = r#"# HELP worker_health worker health
365# TYPE worker_health gauge
366worker_health{name="a",process="simple-metrics"} 1
367worker_health{name="b",process="simple-metrics"} 0
368
369# HELP service_height service height
370# TYPE service_height gauge
371service_height{name="a",process="simple-metrics"} 100
372service_height{name="b",process="simple-metrics"} 200
373"#;
374        assert_eq!(actual, expected);
375    }
376
377    #[test]
378    fn simple_with_labels_chain() {
379        let states = vec![
380            SimpleState {
381                name: "a".into(),
382                health: true,
383                height: 100,
384            },
385            SimpleState {
386                name: "b".into(),
387                health: false,
388                height: 200,
389            },
390        ];
391
392        let static_labels = LabelsBuilder::new()
393            .with("process", "simple-metrics")
394            .build()
395            .unwrap();
396
397        let mut store: MetricStore<ServiceMetric> =
398            MetricStore::new().with_static_labels(static_labels);
399
400        let common_builder =
401            LabelsBuilder::from([("common_a", "some_value"), ("common_b", "other_value")]);
402
403        for s in states {
404            let state_labels = common_builder.clone().with("name", s.name).build().unwrap();
405
406            store.add_sample(
407                ServiceMetric::WorkerHealth,
408                Sample::new(&state_labels, s.health),
409            );
410            store.add_value(ServiceMetric::ServiceHeight, &state_labels, s.height)
411        }
412
413        let actual = store.render_into_metrics(None);
414
415        let expected = r#"# HELP worker_health worker health
416# TYPE worker_health gauge
417worker_health{common_a="some_value",common_b="other_value",name="a",process="simple-metrics"} 1
418worker_health{common_a="some_value",common_b="other_value",name="b",process="simple-metrics"} 0
419
420# HELP service_height service height
421# TYPE service_height gauge
422service_height{common_a="some_value",common_b="other_value",name="a",process="simple-metrics"} 100
423service_height{common_a="some_value",common_b="other_value",name="b",process="simple-metrics"} 200
424"#;
425        assert_eq!(actual, expected);
426    }
427
428    #[test]
429    fn simple_with_label_group() {
430        let states = vec![
431            SimpleState {
432                name: "a".into(),
433                health: true,
434                height: 100,
435            },
436            SimpleState {
437                name: "b".into(),
438                health: false,
439                height: 200,
440            },
441        ];
442
443        let static_labels = LabelsBuilder::new()
444            .with("process", "simple-metrics")
445            .build()
446            .unwrap();
447
448        let mut store: MetricStore<ServiceMetric> =
449            MetricStore::new().with_static_labels(static_labels);
450
451        let common_builder =
452            LabelsBuilder::from([("common_a", "some_value"), ("common_b", "other_value")]);
453
454        for s in states {
455            let state_labels = common_builder.clone().with("name", s.name).build().unwrap();
456
457            store.add_with_common_labels(
458                &state_labels,
459                &[
460                    (ServiceMetric::WorkerHealth, s.health.into()),
461                    (ServiceMetric::ServiceHeight, s.height.into()),
462                ],
463            );
464        }
465
466        let actual = store.render_into_metrics(None);
467
468        let expected = r#"# HELP worker_health worker health
469# TYPE worker_health gauge
470worker_health{common_a="some_value",common_b="other_value",name="a",process="simple-metrics"} 1
471worker_health{common_a="some_value",common_b="other_value",name="b",process="simple-metrics"} 0
472
473# HELP service_height service height
474# TYPE service_height gauge
475service_height{common_a="some_value",common_b="other_value",name="a",process="simple-metrics"} 100
476service_height{common_a="some_value",common_b="other_value",name="b",process="simple-metrics"} 200
477"#;
478        assert_eq!(actual, expected);
479    }
480
481    #[test]
482    fn simple_with_namespace() {
483        let states = vec![
484            SimpleState {
485                name: "a".into(),
486                health: true,
487                height: 100,
488            },
489            SimpleState {
490                name: "b".into(),
491                health: false,
492                height: 200,
493            },
494        ];
495
496        let static_labels = LabelsBuilder::new()
497            .with("process", "simple-metrics")
498            .build()
499            .unwrap();
500
501        let mut store: MetricStore<ServiceMetric> =
502            MetricStore::new().with_static_labels(static_labels);
503
504        for s in states {
505            let common = LabelsBuilder::from([("name", s.name)]).build().unwrap();
506
507            store.add_sample(ServiceMetric::WorkerHealth, Sample::new(&common, s.health));
508            store.add_value(ServiceMetric::ServiceHeight, &common, s.height)
509        }
510
511        let _cloned_store = store.clone();
512
513        let actual = store.render_into_metrics(Some("namespace"));
514        println!("{}", actual);
515
516        let expected = r#"# HELP namespace_worker_health worker health
517# TYPE namespace_worker_health gauge
518namespace_worker_health{name="a",process="simple-metrics"} 1
519namespace_worker_health{name="b",process="simple-metrics"} 0
520
521# HELP namespace_service_height service height
522# TYPE namespace_service_height gauge
523namespace_service_height{name="a",process="simple-metrics"} 100
524namespace_service_height{name="b",process="simple-metrics"} 200
525"#;
526        assert_eq!(actual, expected);
527    }
528
529    #[test]
530    fn simple_escape_label_values() {
531        let states = vec![
532            SimpleState {
533                name: r#""a""#.into(),
534                health: true,
535                height: 100,
536            },
537            SimpleState {
538                name: r#""b""#.into(),
539                health: false,
540                height: 200,
541            },
542        ];
543
544        let static_labels = LabelsBuilder::new()
545            .with("process", "simple-metrics")
546            .build()
547            .unwrap();
548
549        let mut store: MetricStore<ServiceMetric> =
550            MetricStore::new().with_static_labels(static_labels);
551
552        for s in states {
553            let common = LabelsBuilder::from([("name", s.name)]).build().unwrap();
554
555            store.add_sample(ServiceMetric::WorkerHealth, Sample::new(&common, s.health));
556        }
557
558        let _cloned_store = store.clone();
559
560        let actual = store.render_into_metrics(Some("namespace"));
561        println!("{}", actual);
562
563        let expected = r#"# HELP namespace_worker_health worker health
564# TYPE namespace_worker_health gauge
565namespace_worker_health{name="\"a\"",process="simple-metrics"} 1
566namespace_worker_health{name="\"b\"",process="simple-metrics"} 0
567"#;
568        assert_eq!(actual, expected);
569    }
570
571    #[test]
572    fn invalid_metric_name() {
573        let starting_with_a_digit = MetricDef::new(
574            "1starting_with_a_digit",
575            "starting with a digit",
576            MetricType::Gauge,
577        );
578        assert!(starting_with_a_digit.is_err());
579
580        let has_spaces = MetricDef::new(
581            "service health",
582            "has spaces in the metric name",
583            MetricType::Gauge,
584        );
585        assert!(has_spaces.is_err());
586
587        let has_weird_chars =
588            MetricDef::new("dobrĂ½_den", "has non ascii characters", MetricType::Gauge);
589        assert!(has_weird_chars.is_err());
590    }
591}