use prometheus::{
core::{MetricVec, MetricVecBuilder},
exponential_buckets, histogram_opts, linear_buckets, register_gauge, register_gauge_vec,
register_histogram, register_histogram_vec, register_int_counter, register_int_counter_vec,
register_int_gauge, register_int_gauge_vec, Gauge, GaugeVec, Histogram, HistogramVec,
IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Opts,
};
use crate::time::Instant;
const LINERA_NAMESPACE: &str = "linera";
fn registration_failure(name: &str, error: impl std::fmt::Display) -> String {
format!("cannot register metric {name}: {error}")
}
fn materialize_unlabeled<Builder: MetricVecBuilder>(
metric: MetricVec<Builder>,
label_names: &[&str],
) -> MetricVec<Builder> {
if label_names.is_empty() {
metric.with_label_values(&[]);
}
metric
}
#[macro_export]
macro_rules! declare_metrics {
($(
$(#[$attribute:meta])*
$visibility:vis static $name:ident: $metric_type:ty = $registration:expr;
)*) => {
$(
$(#[$attribute])*
$visibility static $name: ::std::sync::LazyLock<$metric_type> =
::std::sync::LazyLock::new(|| $registration);
)*
pub fn init_metrics() {
$( ::std::sync::LazyLock::force(&$name); )*
}
};
}
pub fn register_int_counter_vec(
name: &str,
description: &str,
label_names: &[&str],
) -> IntCounterVec {
let counter_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
materialize_unlabeled(
register_int_counter_vec!(counter_opts, label_names)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
label_names,
)
}
pub fn register_int_counter_vec_with_subsystem(
subsystem: &str,
name: &str,
description: &str,
label_names: &[&str],
) -> IntCounterVec {
let counter_opts = Opts::new(name, description)
.namespace(LINERA_NAMESPACE)
.subsystem(subsystem);
materialize_unlabeled(
register_int_counter_vec!(counter_opts, label_names)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
label_names,
)
}
pub fn register_int_counter(name: &str, description: &str) -> IntCounter {
let counter_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
register_int_counter!(counter_opts)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
}
pub fn register_int_counter_with_subsystem(
subsystem: &str,
name: &str,
description: &str,
) -> IntCounter {
let counter_opts = Opts::new(name, description)
.namespace(LINERA_NAMESPACE)
.subsystem(subsystem);
register_int_counter!(counter_opts)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
}
pub fn register_histogram_vec(
name: &str,
description: &str,
label_names: &[&str],
buckets: Option<Vec<f64>>,
) -> HistogramVec {
let histogram_opts = if let Some(buckets) = buckets {
histogram_opts!(name, description, buckets).namespace(LINERA_NAMESPACE)
} else {
histogram_opts!(name, description).namespace(LINERA_NAMESPACE)
};
materialize_unlabeled(
register_histogram_vec!(histogram_opts, label_names)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
label_names,
)
}
pub fn register_histogram_vec_with_subsystem(
subsystem: &str,
name: &str,
description: &str,
label_names: &[&str],
buckets: Option<Vec<f64>>,
) -> HistogramVec {
let histogram_opts = if let Some(buckets) = buckets {
histogram_opts!(name, description, buckets)
.namespace(LINERA_NAMESPACE)
.subsystem(subsystem)
} else {
histogram_opts!(name, description)
.namespace(LINERA_NAMESPACE)
.subsystem(subsystem)
};
materialize_unlabeled(
register_histogram_vec!(histogram_opts, label_names)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
label_names,
)
}
pub fn register_histogram(name: &str, description: &str, buckets: Option<Vec<f64>>) -> Histogram {
let histogram_opts = if let Some(buckets) = buckets {
histogram_opts!(name, description, buckets).namespace(LINERA_NAMESPACE)
} else {
histogram_opts!(name, description).namespace(LINERA_NAMESPACE)
};
register_histogram!(histogram_opts)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
}
pub fn register_histogram_with_subsystem(
subsystem: &str,
name: &str,
description: &str,
buckets: Option<Vec<f64>>,
) -> Histogram {
let histogram_opts = if let Some(buckets) = buckets {
histogram_opts!(name, description, buckets)
.namespace(LINERA_NAMESPACE)
.subsystem(subsystem)
} else {
histogram_opts!(name, description)
.namespace(LINERA_NAMESPACE)
.subsystem(subsystem)
};
register_histogram!(histogram_opts)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
}
pub fn register_int_gauge(name: &str, description: &str) -> IntGauge {
let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
register_int_gauge!(gauge_opts)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
}
pub fn register_int_gauge_with_subsystem(
subsystem: &str,
name: &str,
description: &str,
) -> IntGauge {
let gauge_opts = Opts::new(name, description)
.namespace(LINERA_NAMESPACE)
.subsystem(subsystem);
register_int_gauge!(gauge_opts)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
}
pub fn register_int_gauge_vec(name: &str, description: &str, label_names: &[&str]) -> IntGaugeVec {
let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
materialize_unlabeled(
register_int_gauge_vec!(gauge_opts, label_names)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
label_names,
)
}
pub fn register_gauge(name: &str, description: &str) -> Gauge {
let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
register_gauge!(gauge_opts)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
}
pub fn register_gauge_with_subsystem(subsystem: &str, name: &str, description: &str) -> Gauge {
let gauge_opts = Opts::new(name, description)
.namespace(LINERA_NAMESPACE)
.subsystem(subsystem);
register_gauge!(gauge_opts)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
}
pub fn register_gauge_vec(name: &str, description: &str, label_names: &[&str]) -> GaugeVec {
let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
materialize_unlabeled(
register_gauge_vec!(gauge_opts, label_names)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
label_names,
)
}
pub fn register_int_gauge_vec_with_subsystem(
subsystem: &str,
name: &str,
description: &str,
label_names: &[&str],
) -> IntGaugeVec {
let gauge_opts = Opts::new(name, description)
.namespace(LINERA_NAMESPACE)
.subsystem(subsystem);
materialize_unlabeled(
register_int_gauge_vec!(gauge_opts, label_names)
.unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
label_names,
)
}
pub fn exponential_bucket_interval(start_value: f64, end_value: f64) -> Option<Vec<f64>> {
let quot = end_value / start_value;
let factor = 3.0_f64;
let count_approx = quot.ln() / factor.ln();
let count = count_approx.round() as usize;
let mut buckets = exponential_buckets(start_value, factor, count)
.expect("Exponential buckets creation should not fail!");
if let Some(last) = buckets.last() {
if *last < end_value {
buckets.push(end_value);
}
}
Some(buckets)
}
pub fn exponential_bucket_latencies(max_latency: f64) -> Option<Vec<f64>> {
exponential_bucket_interval(0.001_f64, max_latency)
}
pub fn linear_bucket_interval(start_value: f64, width: f64, end_value: f64) -> Option<Vec<f64>> {
let count = (end_value - start_value) / width;
let count = count.round() as usize;
let mut buckets = linear_buckets(start_value, width, count)
.expect("Linear buckets creation should not fail!");
buckets.push(end_value);
Some(buckets)
}
enum MeasurementUnit {
Milliseconds,
Microseconds,
}
pub struct ActiveMeasurementGuard<'metric, Metric>
where
Metric: MeasureLatency,
{
start: Instant,
metric: Option<&'metric Metric>,
unit: MeasurementUnit,
}
impl<Metric> ActiveMeasurementGuard<'_, Metric>
where
Metric: MeasureLatency,
{
pub fn finish(mut self) -> f64 {
self.finish_by_ref()
}
fn finish_by_ref(&mut self) -> f64 {
match self.metric.take() {
Some(metric) => {
let latency = match self.unit {
MeasurementUnit::Milliseconds => self.start.elapsed().as_secs_f64() * 1000.0,
MeasurementUnit::Microseconds => {
self.start.elapsed().as_secs_f64() * 1_000_000.0
}
};
metric.finish_measurement(latency);
latency
}
None => {
f64::NAN
}
}
}
}
impl<Metric> Drop for ActiveMeasurementGuard<'_, Metric>
where
Metric: MeasureLatency,
{
fn drop(&mut self) {
self.finish_by_ref();
}
}
pub trait MeasureLatency: Sized {
fn measure_latency(&self) -> ActiveMeasurementGuard<'_, Self>;
fn measure_latency_us(&self) -> ActiveMeasurementGuard<'_, Self>;
fn finish_measurement(&self, milliseconds: f64);
}
impl MeasureLatency for HistogramVec {
fn measure_latency(&self) -> ActiveMeasurementGuard<'_, Self> {
ActiveMeasurementGuard {
start: Instant::now(),
metric: Some(self),
unit: MeasurementUnit::Milliseconds,
}
}
fn measure_latency_us(&self) -> ActiveMeasurementGuard<'_, Self> {
ActiveMeasurementGuard {
start: Instant::now(),
metric: Some(self),
unit: MeasurementUnit::Microseconds,
}
}
fn finish_measurement(&self, milliseconds: f64) {
self.with_label_values(&[]).observe(milliseconds);
}
}
impl MeasureLatency for Histogram {
fn measure_latency(&self) -> ActiveMeasurementGuard<'_, Self> {
ActiveMeasurementGuard {
start: Instant::now(),
metric: Some(self),
unit: MeasurementUnit::Milliseconds,
}
}
fn measure_latency_us(&self) -> ActiveMeasurementGuard<'_, Self> {
ActiveMeasurementGuard {
start: Instant::now(),
metric: Some(self),
unit: MeasurementUnit::Microseconds,
}
}
fn finish_measurement(&self, milliseconds: f64) {
self.observe(milliseconds);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_failed_registration_names_the_metric() {
let message = registration_failure("some_metric", "Duplicate metrics collector");
assert!(message.contains("some_metric"), "got: {message}");
}
#[test]
fn subsystem_is_inserted_between_namespace_and_name() {
register_int_counter_with_subsystem(
"testsubsystem",
"testname",
"Pins the namespace/subsystem/name composition",
);
assert!(prometheus::gather()
.iter()
.any(|family| family.get_name() == "linera_testsubsystem_testname"));
}
fn assert_float_vec_eq(left: &[f64], right: &[f64]) {
const EPSILON: f64 = 1e-10;
assert_eq!(left.len(), right.len(), "Vectors have different lengths");
for (i, (l, r)) in left.iter().zip(right.iter()).enumerate() {
assert!(
(l - r).abs() < EPSILON,
"Vectors differ at index {i}: {l} != {r}"
);
}
}
#[test]
fn test_linear_bucket_interval() {
let buckets = linear_bucket_interval(0.05, 0.01, 0.1).unwrap();
assert_float_vec_eq(&buckets, &[0.05, 0.06, 0.07, 0.08, 0.09, 0.1]);
let buckets = linear_bucket_interval(100.0, 50.0, 500.0).unwrap();
assert_float_vec_eq(
&buckets,
&[
100.0, 150.0, 200.0, 250.0, 300.0, 350.0, 400.0, 450.0, 500.0,
],
);
let buckets = linear_bucket_interval(0.05, 0.12, 0.5).unwrap();
assert_float_vec_eq(&buckets, &[0.05, 0.17, 0.29, 0.41, 0.5]);
let buckets = linear_bucket_interval(100.0, 150.0, 500.0).unwrap();
assert_float_vec_eq(&buckets, &[100.0, 250.0, 400.0, 500.0]);
}
}