use prometheus_client::encoding::text::{EncodeMetric, Encoder};
use prometheus_client::metrics::gauge::Gauge;
use prometheus_client::metrics::{MetricType, TypedMetric};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Default)]
pub struct RangeGauge {
gauge: Gauge<u64, AtomicU64>,
min: Arc<AtomicU64>,
max: Arc<AtomicU64>,
reset_cs: Arc<Mutex<()>>,
}
impl RangeGauge {
pub fn inc(&self) -> u64 {
self.inc_by(1)
}
pub fn inc_by(&self, v: u64) -> u64 {
let prev = self.gauge.inc_by(v);
self.update_max(prev + v);
prev
}
pub fn dec(&self) -> u64 {
self.dec_by(1)
}
pub fn dec_by(&self, v: u64) -> u64 {
let prev = self.gauge.dec_by(v);
self.update_min(prev - v);
prev
}
pub fn set(&self, v: u64) -> u64 {
let prev = self.gauge.set(v);
self.update_max(v);
self.update_min(v);
prev
}
pub fn get(&self) -> u64 {
self.gauge.get()
}
pub fn inner(&self) -> &AtomicU64 {
self.gauge.inner()
}
fn update_max(&self, new_max: u64) {
self.max.fetch_max(new_max, Ordering::AcqRel);
}
fn update_min(&self, new_min: u64) {
self.min.fetch_min(new_min, Ordering::AcqRel);
}
fn get_values(&self) -> (u64, u64, u64) {
let _reset_guard = self.reset_cs.lock().unwrap();
let current = self.get();
let min = std::cmp::min(current, self.min.swap(current, Ordering::AcqRel));
let max = std::cmp::max(current, self.max.swap(current, Ordering::AcqRel));
let current_fixup = self.get();
self.min.fetch_min(current_fixup, Ordering::AcqRel);
self.max.fetch_max(current_fixup, Ordering::AcqRel);
(min, current, max)
}
}
impl TypedMetric for RangeGauge {
const TYPE: MetricType = MetricType::Gauge;
}
impl EncodeMetric for RangeGauge {
fn encode(&self, mut encoder: Encoder) -> Result<(), std::io::Error> {
let (min, current, max) = self.get_values();
encoder
.no_suffix()?
.no_bucket()?
.encode_value(current)?
.no_exemplar()?;
encoder
.encode_suffix("min")?
.no_bucket()?
.encode_value(min)?
.no_exemplar()?;
encoder
.encode_suffix("max")?
.no_bucket()?
.encode_value(max)?
.no_exemplar()?;
Ok(())
}
fn metric_type(&self) -> MetricType {
Self::TYPE
}
}
pub struct GaugeGuard<G: GenericGauge>(G);
impl<G: GenericGauge> GaugeGuard<G> {
pub fn new(gauge: G) -> Self {
gauge.inc();
Self(gauge)
}
}
impl<G: GenericGauge> Drop for GaugeGuard<G> {
fn drop(&mut self) {
self.0.dec();
}
}
pub trait GenericGauge {
fn inc(&self);
fn dec(&self);
}
impl GenericGauge for Gauge {
fn inc(&self) {
Gauge::inc(self);
}
fn dec(&self) {
Gauge::dec(self);
}
}
impl GenericGauge for RangeGauge {
fn inc(&self) {
RangeGauge::inc(self);
}
fn dec(&self) {
RangeGauge::dec(self);
}
}
#[cfg(test)]
mod tests {
use super::*;
use prometheus_client::encoding::text::encode;
use prometheus_client::registry::Registry;
struct MetricValueHelper(Registry<RangeGauge>);
impl MetricValueHelper {
fn new(metric: &RangeGauge) -> Self {
let mut reg = Registry::default();
reg.register("mygauge", "", metric.clone());
Self(reg)
}
#[track_caller]
fn assert_values(&self, val: u64, min: u64, max: u64) {
let mut encoded = vec![];
encode(&mut encoded, &self.0).unwrap();
assert_eq!(
std::str::from_utf8(&encoded).unwrap(),
format!(
"\
# HELP mygauge .
# TYPE mygauge gauge
mygauge {val}
mygauge_min {min}
mygauge_max {max}
# EOF
"
),
);
}
}
#[test]
fn test_rangegauge_values() {
let rg = RangeGauge::default();
let helper = MetricValueHelper::new(&rg);
helper.assert_values(0, 0, 0);
rg.inc();
helper.assert_values(1, 0, 1);
helper.assert_values(1, 1, 1);
rg.dec();
helper.assert_values(0, 0, 1);
helper.assert_values(0, 0, 0);
rg.inc_by(3);
rg.dec_by(2);
helper.assert_values(1, 0, 3);
rg.inc_by(1);
rg.dec_by(2);
helper.assert_values(0, 0, 2);
}
}