1#[doc(hidden)]
5pub mod re_export {
6 pub use paste;
7 pub use prometheus;
8}
9
10#[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 pub struct $service {
58 registry: prometheus::Registry,
59 $(
60 $metric_attribute: $metric_type,
61 )*
62 }
63
64 impl $service {
65 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 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 pub fn get_counter_example(&self) -> &MetricCounter {
178 &self.counter_example
179 }
180
181 pub fn get_gauge_example(&self) -> &MetricGauge {
183 &self.gauge_example
184 }
185
186 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}