mithril_metric/
helper.rs

1//! Helper to create a metric service.
2
3/// Required re-exports to avoid requiring childs crates to depend on them when using the `build_metrics_service` macro.
4#[doc(hidden)]
5pub mod re_export {
6    pub use paste;
7    pub use prometheus;
8}
9
10/// Create a MetricService.
11///
12/// To build the service you need to provide the structure name and a list of metrics.
13/// Each metrics is defined by an attribute name, a type, a metric name and a help message.
14///
15/// The attribute name will be used to create a getter method for the metric.
16///
17/// Crate that use this macro should have `paste` as dependency.
18///
19/// # Example of 'build_metrics_service' and metrics usage
20///
21/// ```
22///     use slog::Logger;
23///     use mithril_common::{entities::Epoch, StdResult};
24///     use mithril_metric::build_metrics_service;
25///     use mithril_metric::{MetricCollector, MetricCounter, MetricCounterWithLabels, MetricGauge, MetricsServiceExporter};
26///
27///     build_metrics_service!(
28///         MetricsService,
29///         counter_example: MetricCounter(
30///             "custom_counter_example_name",
31///             "Example of a counter metric"
32///         ),
33///         gauge_example: MetricGauge(
34///             "custom_gauge_example_name",
35///             "Example of a gauge metric"
36///         ),
37///         counter_with_labels_example: MetricCounterWithLabels(
38///             "custom_counter_with_labels_example_name",
39///             "Example of a counter with labels metric",
40///             &["label"]
41///         )
42///     );
43///
44///     let service = MetricsService::new(Logger::root(slog::Discard, slog::o!())).unwrap();
45///     service.get_counter_example().increment();
46///     service.get_gauge_example().record(Epoch(12));
47///     service.get_counter_with_labels_example().increment(&["guest"]);
48/// ```
49#[macro_export]
50macro_rules! build_metrics_service {
51
52    ($service:ident, $($metric_attribute:ident:$metric_type:ident ($name:literal, $help:literal $(, $labels:expr)?)),*) => {
53        use $crate::helper::re_export::paste;
54        use $crate::helper::re_export::prometheus;
55        paste::item! {
56            /// Metrics service which is responsible for recording and exposing metrics.
57            pub struct $service {
58                registry: prometheus::Registry,
59                $(
60                    $metric_attribute: $metric_type,
61                )*
62            }
63
64            impl $service {
65                /// Create a new MetricsService instance.
66                pub fn new(logger: slog::Logger) -> mithril_common::StdResult<Self> {
67
68                    let registry = prometheus::Registry::new();
69
70                    $(
71                        let $metric_attribute = $metric_type::new(
72                            mithril_common::logging::LoggerExtensions::new_with_component_name::<Self>(
73                                &logger,
74                            ),
75                            $name,
76                            $help,
77                            $($labels,)?
78                        )?;
79                        registry.register($metric_attribute.collector())?;
80                    )*
81
82                    Ok(Self {
83                        registry,
84                        $(
85                            $metric_attribute,
86                        )*
87                    })
88                }
89                $(
90                    /// Get the `$metric_attribute` counter.
91                    pub fn [<get_ $metric_attribute>](&self) -> &$metric_type {
92                        &self.$metric_attribute
93                    }
94                )*
95            }
96
97            impl MetricsServiceExporter for $service {
98                fn export_metrics(&self) -> mithril_common::StdResult<String> {
99                    Ok(prometheus::TextEncoder::new().encode_to_string(&self.registry.gather())?)
100                }
101            }
102
103        }
104    };
105}
106
107#[cfg(test)]
108pub(crate) mod test_tools {
109    mithril_common::define_test_logger!();
110}
111
112#[cfg(test)]
113mod tests {
114    use std::collections::BTreeMap;
115
116    use crate::{
117        MetricCollector, MetricCounter, MetricCounterWithLabels, MetricGauge,
118        MetricsServiceExporter,
119    };
120
121    use super::*;
122    use mithril_common::{StdResult, entities::Epoch};
123    use prometheus::{Registry, TextEncoder};
124    use prometheus_parse::Value;
125    use slog::Logger;
126    use test_tools::TestLogger;
127
128    fn parse_metrics(raw_metrics: &str) -> StdResult<BTreeMap<String, Value>> {
129        Ok(
130            prometheus_parse::Scrape::parse(raw_metrics.lines().map(|s| Ok(s.to_owned())))?
131                .samples
132                .into_iter()
133                .map(|s| (s.metric, s.value))
134                .collect::<BTreeMap<_, _>>(),
135        )
136    }
137
138    pub struct MetricsServiceExample {
139        registry: Registry,
140        counter_example: MetricCounter,
141        gauge_example: MetricGauge,
142        counter_with_labels_example: MetricCounterWithLabels,
143    }
144
145    impl MetricsServiceExample {
146        pub fn new(logger: Logger) -> StdResult<Self> {
147            let registry = Registry::new();
148
149            let counter_example = MetricCounter::new(
150                logger.clone(),
151                "counter_example",
152                "Example of a counter metric",
153            )?;
154            registry.register(counter_example.collector())?;
155
156            let gauge_example =
157                MetricGauge::new(logger.clone(), "gauge_example", "Example of a gauge metric")?;
158            registry.register(gauge_example.collector())?;
159
160            let counter_with_labels_example = MetricCounterWithLabels::new(
161                logger.clone(),
162                "counter_with_labels_example",
163                "Example of a counter with labels metric",
164                &["label_1", "label_2"],
165            )?;
166            registry.register(counter_with_labels_example.collector())?;
167
168            Ok(Self {
169                registry,
170                counter_example,
171                gauge_example,
172                counter_with_labels_example,
173            })
174        }
175
176        /// Get the `counter_example` counter.
177        pub fn get_counter_example(&self) -> &MetricCounter {
178            &self.counter_example
179        }
180
181        /// Get the `gauge_example` counter.
182        pub fn get_gauge_example(&self) -> &MetricGauge {
183            &self.gauge_example
184        }
185
186        /// Get the `counter_with_labels_example` counter.
187        pub fn get_counter_with_labels_example(&self) -> &MetricCounterWithLabels {
188            &self.counter_with_labels_example
189        }
190    }
191
192    impl MetricsServiceExporter for MetricsServiceExample {
193        fn export_metrics(&self) -> StdResult<String> {
194            Ok(TextEncoder::new().encode_to_string(&self.registry.gather())?)
195        }
196    }
197
198    #[test]
199    fn test_service_creation() {
200        let service = MetricsServiceExample::new(TestLogger::stdout()).unwrap();
201        service.get_counter_example().increment();
202        service.get_counter_example().increment();
203        service.get_gauge_example().record(Epoch(12));
204        service.get_counter_with_labels_example().increment(&["A", "200"]);
205
206        assert_eq!(2, service.get_counter_example().get());
207        assert_eq!(Epoch(12), Epoch(service.get_gauge_example().get() as u64));
208        assert_eq!(
209            1,
210            service.get_counter_with_labels_example().get(&["A", "200"])
211        );
212    }
213
214    build_metrics_service!(
215        MetricsServiceExampleBuildWithMacro,
216        counter_example: MetricCounter(
217            "custom_counter_example_name",
218            "Example of a counter metric"
219        ),
220        gauge_example: MetricGauge(
221            "custom_gauge_example_name",
222            "Example of a gauge metric"
223        ),
224        counter_with_labels_example: MetricCounterWithLabels(
225            "custom_counter_with_labels_example_name",
226            "Example of a counter with labels metric",
227            &["label_1", "label_2"]
228        )
229    );
230
231    #[test]
232    fn test_service_creation_using_build_metrics_service_macro() {
233        let service = MetricsServiceExampleBuildWithMacro::new(TestLogger::stdout()).unwrap();
234        service.get_counter_example().increment();
235        service.get_counter_example().increment();
236        service.get_gauge_example().record(Epoch(12));
237        service.get_counter_with_labels_example().increment(&["A", "200"]);
238
239        assert_eq!(2, service.get_counter_example().get());
240        assert_eq!(Epoch(12), Epoch(service.get_gauge_example().get() as u64));
241        assert_eq!(
242            1,
243            service.get_counter_with_labels_example().get(&["A", "200"])
244        );
245    }
246
247    #[test]
248    fn test_build_metrics_service_named_metrics_with_attribute_name() {
249        let service = MetricsServiceExampleBuildWithMacro::new(TestLogger::stdout()).unwrap();
250        assert_eq!(
251            "custom_counter_example_name",
252            service.get_counter_example().name()
253        );
254        assert_eq!(
255            "custom_gauge_example_name",
256            service.get_gauge_example().name()
257        );
258        assert_eq!(
259            "custom_counter_with_labels_example_name",
260            service.get_counter_with_labels_example().name()
261        );
262    }
263
264    #[test]
265    fn test_build_metrics_service_provide_a_functional_export_metrics_function() {
266        let service = MetricsServiceExampleBuildWithMacro::new(TestLogger::stdout()).unwrap();
267
268        service.counter_example.increment();
269        service.gauge_example.record(Epoch(12));
270        service.counter_with_labels_example.increment(&["A", "200"]);
271
272        let exported_metrics = service.export_metrics().unwrap();
273
274        let parsed_metrics = parse_metrics(&exported_metrics).unwrap();
275
276        let parsed_metrics_expected = BTreeMap::from([
277            (service.counter_example.name(), Value::Counter(1.0)),
278            (service.gauge_example.name(), Value::Gauge(12.0)),
279            (
280                service.counter_with_labels_example.name(),
281                Value::Counter(1.0),
282            ),
283        ]);
284
285        assert_eq!(parsed_metrics_expected, parsed_metrics);
286    }
287}