1use prometheus::{
7 core::{MetricVec, MetricVecBuilder},
8 exponential_buckets, histogram_opts, linear_buckets, register_gauge, register_gauge_vec,
9 register_histogram, register_histogram_vec, register_int_counter, register_int_counter_vec,
10 register_int_gauge, register_int_gauge_vec, Gauge, GaugeVec, Histogram, HistogramVec,
11 IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Opts,
12};
13
14use crate::time::Instant;
15
16const LINERA_NAMESPACE: &str = "linera";
17
18fn registration_failure(name: &str, error: impl std::fmt::Display) -> String {
23 format!("cannot register metric {name}: {error}")
24}
25
26fn materialize_unlabeled<Builder: MetricVecBuilder>(
34 metric: MetricVec<Builder>,
35 label_names: &[&str],
36) -> MetricVec<Builder> {
37 if label_names.is_empty() {
38 metric.with_label_values(&[]);
39 }
40 metric
41}
42
43#[macro_export]
49macro_rules! declare_metrics {
50 ($(
51 $(#[$attribute:meta])*
52 $visibility:vis static $name:ident: $metric_type:ty = $registration:expr;
53 )*) => {
54 $(
55 $(#[$attribute])*
56 $visibility static $name: ::std::sync::LazyLock<$metric_type> =
57 ::std::sync::LazyLock::new(|| $registration);
58 )*
59
60 pub fn init_metrics() {
62 $( ::std::sync::LazyLock::force(&$name); )*
63 }
64 };
65}
66
67pub fn register_int_counter_vec(
69 name: &str,
70 description: &str,
71 label_names: &[&str],
72) -> IntCounterVec {
73 let counter_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
74 materialize_unlabeled(
75 register_int_counter_vec!(counter_opts, label_names)
76 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
77 label_names,
78 )
79}
80
81pub fn register_int_counter_vec_with_subsystem(
84 subsystem: &str,
85 name: &str,
86 description: &str,
87 label_names: &[&str],
88) -> IntCounterVec {
89 let counter_opts = Opts::new(name, description)
90 .namespace(LINERA_NAMESPACE)
91 .subsystem(subsystem);
92 materialize_unlabeled(
93 register_int_counter_vec!(counter_opts, label_names)
94 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
95 label_names,
96 )
97}
98
99pub fn register_int_counter(name: &str, description: &str) -> IntCounter {
101 let counter_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
102 register_int_counter!(counter_opts)
103 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
104}
105
106pub fn register_histogram_vec(
108 name: &str,
109 description: &str,
110 label_names: &[&str],
111 buckets: Option<Vec<f64>>,
112) -> HistogramVec {
113 let histogram_opts = if let Some(buckets) = buckets {
114 histogram_opts!(name, description, buckets).namespace(LINERA_NAMESPACE)
115 } else {
116 histogram_opts!(name, description).namespace(LINERA_NAMESPACE)
117 };
118
119 materialize_unlabeled(
120 register_histogram_vec!(histogram_opts, label_names)
121 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
122 label_names,
123 )
124}
125
126pub fn register_histogram_vec_with_subsystem(
129 subsystem: &str,
130 name: &str,
131 description: &str,
132 label_names: &[&str],
133 buckets: Option<Vec<f64>>,
134) -> HistogramVec {
135 let histogram_opts = if let Some(buckets) = buckets {
136 histogram_opts!(name, description, buckets)
137 .namespace(LINERA_NAMESPACE)
138 .subsystem(subsystem)
139 } else {
140 histogram_opts!(name, description)
141 .namespace(LINERA_NAMESPACE)
142 .subsystem(subsystem)
143 };
144
145 materialize_unlabeled(
146 register_histogram_vec!(histogram_opts, label_names)
147 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
148 label_names,
149 )
150}
151
152pub fn register_histogram(name: &str, description: &str, buckets: Option<Vec<f64>>) -> Histogram {
154 let histogram_opts = if let Some(buckets) = buckets {
155 histogram_opts!(name, description, buckets).namespace(LINERA_NAMESPACE)
156 } else {
157 histogram_opts!(name, description).namespace(LINERA_NAMESPACE)
158 };
159
160 register_histogram!(histogram_opts)
161 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
162}
163
164pub fn register_histogram_with_subsystem(
167 subsystem: &str,
168 name: &str,
169 description: &str,
170 buckets: Option<Vec<f64>>,
171) -> Histogram {
172 let histogram_opts = if let Some(buckets) = buckets {
173 histogram_opts!(name, description, buckets)
174 .namespace(LINERA_NAMESPACE)
175 .subsystem(subsystem)
176 } else {
177 histogram_opts!(name, description)
178 .namespace(LINERA_NAMESPACE)
179 .subsystem(subsystem)
180 };
181
182 register_histogram!(histogram_opts)
183 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
184}
185
186pub fn register_int_gauge(name: &str, description: &str) -> IntGauge {
188 let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
189 register_int_gauge!(gauge_opts)
190 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
191}
192
193pub fn register_int_gauge_with_subsystem(
196 subsystem: &str,
197 name: &str,
198 description: &str,
199) -> IntGauge {
200 let gauge_opts = Opts::new(name, description)
201 .namespace(LINERA_NAMESPACE)
202 .subsystem(subsystem);
203 register_int_gauge!(gauge_opts)
204 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
205}
206
207pub fn register_int_gauge_vec(name: &str, description: &str, label_names: &[&str]) -> IntGaugeVec {
209 let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
210 materialize_unlabeled(
211 register_int_gauge_vec!(gauge_opts, label_names)
212 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
213 label_names,
214 )
215}
216
217pub fn register_gauge(name: &str, description: &str) -> Gauge {
220 let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
221 register_gauge!(gauge_opts)
222 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
223}
224
225pub fn register_gauge_with_subsystem(subsystem: &str, name: &str, description: &str) -> Gauge {
228 let gauge_opts = Opts::new(name, description)
229 .namespace(LINERA_NAMESPACE)
230 .subsystem(subsystem);
231 register_gauge!(gauge_opts)
232 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
233}
234
235pub fn register_int_counter_with_subsystem(
238 subsystem: &str,
239 name: &str,
240 description: &str,
241) -> IntCounter {
242 let counter_opts = Opts::new(name, description)
243 .namespace(LINERA_NAMESPACE)
244 .subsystem(subsystem);
245 register_int_counter!(counter_opts)
246 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
247}
248
249pub fn register_gauge_vec(name: &str, description: &str, label_names: &[&str]) -> GaugeVec {
252 let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
253 materialize_unlabeled(
254 register_gauge_vec!(gauge_opts, label_names)
255 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
256 label_names,
257 )
258}
259
260pub fn register_int_gauge_vec_with_subsystem(
263 subsystem: &str,
264 name: &str,
265 description: &str,
266 label_names: &[&str],
267) -> IntGaugeVec {
268 let gauge_opts = Opts::new(name, description)
269 .namespace(LINERA_NAMESPACE)
270 .subsystem(subsystem);
271 materialize_unlabeled(
272 register_int_gauge_vec!(gauge_opts, label_names)
273 .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
274 label_names,
275 )
276}
277
278#[expect(
280 clippy::cast_possible_truncation,
281 clippy::cast_sign_loss,
282 reason = "histogram bucket count; loss of precision is acceptable"
283)]
284pub fn exponential_bucket_interval(start_value: f64, end_value: f64) -> Option<Vec<f64>> {
285 let quot = end_value / start_value;
286 let factor = 3.0_f64;
287 let count_approx = quot.ln() / factor.ln();
288 let count = count_approx.round() as usize;
289 let mut buckets = exponential_buckets(start_value, factor, count)
290 .expect("Exponential buckets creation should not fail!");
291 if let Some(last) = buckets.last() {
292 if *last < end_value {
293 buckets.push(end_value);
294 }
295 }
296 Some(buckets)
297}
298
299pub fn exponential_bucket_latencies(max_latency: f64) -> Option<Vec<f64>> {
301 exponential_bucket_interval(0.001_f64, max_latency)
302}
303
304#[expect(
306 clippy::cast_possible_truncation,
307 clippy::cast_sign_loss,
308 reason = "histogram bucket count; loss of precision is acceptable"
309)]
310pub fn linear_bucket_interval(start_value: f64, width: f64, end_value: f64) -> Option<Vec<f64>> {
311 let count = (end_value - start_value) / width;
312 let count = count.round() as usize;
313 let mut buckets = linear_buckets(start_value, width, count)
314 .expect("Linear buckets creation should not fail!");
315 buckets.push(end_value);
316 Some(buckets)
317}
318
319enum MeasurementUnit {
321 Milliseconds,
323 Microseconds,
325}
326
327pub struct ActiveMeasurementGuard<'metric, Metric>
331where
332 Metric: MeasureLatency,
333{
334 start: Instant,
335 metric: Option<&'metric Metric>,
336 unit: MeasurementUnit,
337}
338
339impl<Metric> ActiveMeasurementGuard<'_, Metric>
340where
341 Metric: MeasureLatency,
342{
343 pub fn finish(mut self) -> f64 {
346 self.finish_by_ref()
347 }
348
349 fn finish_by_ref(&mut self) -> f64 {
353 match self.metric.take() {
354 Some(metric) => {
355 let latency = match self.unit {
356 MeasurementUnit::Milliseconds => self.start.elapsed().as_secs_f64() * 1000.0,
357 MeasurementUnit::Microseconds => {
358 self.start.elapsed().as_secs_f64() * 1_000_000.0
359 }
360 };
361 metric.finish_measurement(latency);
362 latency
363 }
364 None => {
365 f64::NAN
368 }
369 }
370 }
371}
372
373impl<Metric> Drop for ActiveMeasurementGuard<'_, Metric>
374where
375 Metric: MeasureLatency,
376{
377 fn drop(&mut self) {
378 self.finish_by_ref();
379 }
380}
381
382pub trait MeasureLatency: Sized {
384 fn measure_latency(&self) -> ActiveMeasurementGuard<'_, Self>;
387
388 fn measure_latency_us(&self) -> ActiveMeasurementGuard<'_, Self>;
391
392 fn finish_measurement(&self, milliseconds: f64);
394}
395
396impl MeasureLatency for HistogramVec {
397 fn measure_latency(&self) -> ActiveMeasurementGuard<'_, Self> {
398 ActiveMeasurementGuard {
399 start: Instant::now(),
400 metric: Some(self),
401 unit: MeasurementUnit::Milliseconds,
402 }
403 }
404
405 fn measure_latency_us(&self) -> ActiveMeasurementGuard<'_, Self> {
406 ActiveMeasurementGuard {
407 start: Instant::now(),
408 metric: Some(self),
409 unit: MeasurementUnit::Microseconds,
410 }
411 }
412
413 fn finish_measurement(&self, milliseconds: f64) {
414 self.with_label_values(&[]).observe(milliseconds);
415 }
416}
417
418impl MeasureLatency for Histogram {
419 fn measure_latency(&self) -> ActiveMeasurementGuard<'_, Self> {
420 ActiveMeasurementGuard {
421 start: Instant::now(),
422 metric: Some(self),
423 unit: MeasurementUnit::Milliseconds,
424 }
425 }
426
427 fn measure_latency_us(&self) -> ActiveMeasurementGuard<'_, Self> {
428 ActiveMeasurementGuard {
429 start: Instant::now(),
430 metric: Some(self),
431 unit: MeasurementUnit::Microseconds,
432 }
433 }
434
435 fn finish_measurement(&self, milliseconds: f64) {
436 self.observe(milliseconds);
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
448 fn a_failed_registration_names_the_metric() {
449 let message = registration_failure("some_metric", "Duplicate metrics collector");
450 assert!(message.contains("some_metric"), "got: {message}");
451 }
452
453 #[test]
456 fn subsystem_is_inserted_between_namespace_and_name() {
457 register_int_counter_with_subsystem(
458 "testsubsystem",
459 "testname",
460 "Pins the namespace/subsystem/name composition",
461 );
462
463 assert!(prometheus::gather()
464 .iter()
465 .any(|family| family.get_name() == "linera_testsubsystem_testname"));
466 }
467
468 fn assert_float_vec_eq(left: &[f64], right: &[f64]) {
470 const EPSILON: f64 = 1e-10;
471
472 assert_eq!(left.len(), right.len(), "Vectors have different lengths");
473 for (i, (l, r)) in left.iter().zip(right.iter()).enumerate() {
474 assert!(
475 (l - r).abs() < EPSILON,
476 "Vectors differ at index {i}: {l} != {r}"
477 );
478 }
479 }
480
481 #[test]
482 fn test_linear_bucket_interval() {
483 let buckets = linear_bucket_interval(0.05, 0.01, 0.1).unwrap();
485 assert_float_vec_eq(&buckets, &[0.05, 0.06, 0.07, 0.08, 0.09, 0.1]);
486
487 let buckets = linear_bucket_interval(100.0, 50.0, 500.0).unwrap();
489 assert_float_vec_eq(
490 &buckets,
491 &[
492 100.0, 150.0, 200.0, 250.0, 300.0, 350.0, 400.0, 450.0, 500.0,
493 ],
494 );
495
496 let buckets = linear_bucket_interval(0.05, 0.12, 0.5).unwrap();
498 assert_float_vec_eq(&buckets, &[0.05, 0.17, 0.29, 0.41, 0.5]);
499
500 let buckets = linear_bucket_interval(100.0, 150.0, 500.0).unwrap();
502 assert_float_vec_eq(&buckets, &[100.0, 250.0, 400.0, 500.0]);
503 }
504}