Skip to content

Commit 3c5350d

Browse files
committed
Add a maximum over interval gauge
Signed-off-by: Kushagra Udai <[email protected]>
1 parent 6e81890 commit 3c5350d

File tree

4 files changed

+263
-14
lines changed

4 files changed

+263
-14
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ procfs = { version = "^0.14", optional = true, default-features = false }
4242
criterion = "0.4"
4343
getopts = "^0.2"
4444
hyper = { version = "^0.14", features = ["server", "http1", "tcp"] }
45+
mock_instant = { version = "0.3", features = ["sync"] }
4546
tokio = { version = "^1.0", features = ["macros", "rt-multi-thread"] }
4647

4748
[build-dependencies]

src/atomic64.rs

+25-14
Original file line numberDiff line numberDiff line change
@@ -107,24 +107,12 @@ impl Atomic for AtomicF64 {
107107

108108
#[inline]
109109
fn inc_by(&self, delta: Self::T) {
110-
loop {
111-
let current = self.inner.load(Ordering::Acquire);
112-
let new = u64_to_f64(current) + delta;
113-
let result = self.inner.compare_exchange_weak(
114-
current,
115-
f64_to_u64(new),
116-
Ordering::Release,
117-
Ordering::Relaxed,
118-
);
119-
if result.is_ok() {
120-
return;
121-
}
122-
}
110+
self.fetch_add(delta);
123111
}
124112

125113
#[inline]
126114
fn dec_by(&self, delta: Self::T) {
127-
self.inc_by(-delta);
115+
self.fetch_add(-delta);
128116
}
129117
}
130118

@@ -133,6 +121,29 @@ impl AtomicF64 {
133121
pub fn swap(&self, val: f64, ordering: Ordering) -> f64 {
134122
u64_to_f64(self.inner.swap(f64_to_u64(val), ordering))
135123
}
124+
125+
/// Fetches the maximum of the current value and the provided value.
126+
pub fn fetch_max(&self, val: f64, ordering: Ordering) -> f64 {
127+
u64_to_f64(self.inner.fetch_max(f64_to_u64(val) ,ordering))
128+
}
129+
130+
/// Fetches the old value after summing the provided value.
131+
pub fn fetch_add(&self, val: f64) -> f64 {
132+
let mut current: u64;
133+
loop {
134+
current = self.inner.load(Ordering::Acquire);
135+
let new = u64_to_f64(current) + val;
136+
let result = self.inner.compare_exchange_weak(
137+
current,
138+
f64_to_u64(new),
139+
Ordering::Release,
140+
Ordering::Relaxed,
141+
);
142+
if result.is_ok() {
143+
return u64_to_f64(current);
144+
}
145+
}
146+
}
136147
}
137148

138149
/// A atomic signed integer.

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ mod encoder;
157157
mod errors;
158158
mod gauge;
159159
mod histogram;
160+
mod maximum_over_interval_gauge;
160161
mod metrics;
161162
mod pulling_gauge;
162163
#[cfg(feature = "push")]

src/maximum_over_interval_gauge.rs

+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
2+
use std::{
3+
sync::{
4+
atomic::Ordering,
5+
Arc,
6+
},
7+
time::Duration,
8+
};
9+
10+
#[cfg(test)]
11+
use mock_instant::Instant;
12+
13+
#[cfg(not(test))]
14+
use std::time::Instant;
15+
16+
use crate::{core::{Collector, AtomicF64, Atomic}, Error, PullingGauge};
17+
18+
/// A prometheus gauge that exposes the maximum value of a gauge over an interval.
19+
///
20+
/// Used to expose instantaneous values that tend to move a lot within a small interval.
21+
///
22+
/// # Examples
23+
/// ```
24+
/// # use std::time::Duration;
25+
/// # use prometheus::{Registry, MaximumOverIntervalGauge};
26+
///
27+
/// let registry = Registry::new();
28+
/// let gauge = MaximumOverIntervalGauge::new(
29+
/// "maximum_queue_size_30s",
30+
/// "The high watermark queue size in the last 30 seconds.",
31+
/// Duration::from_secs(30)
32+
/// ).unwrap();
33+
/// registry.register(Box::new(gauge.clone()));
34+
///
35+
/// gauge.inc_by(30);
36+
/// gauge.dec_by(10);
37+
///
38+
/// // For the next 30 seconds, the metric will be 30 as that was the maximum value.
39+
/// // Afterwards, it will drop to 10.
40+
/// ```
41+
#[derive(Clone, Debug)]
42+
pub struct MaximumOverIntervalGauge {
43+
// The current real-time value.
44+
value: Arc<AtomicF64>,
45+
// The maximum value in the current interval.
46+
maximum_value: Arc<AtomicF64>,
47+
48+
// The length of a given interval.
49+
interval_duration: Duration,
50+
// The time at which the current interval will expose.
51+
interval_expiry: Arc<RwLock<Instant>>,
52+
53+
gauge: PullingGauge,
54+
}
55+
56+
impl MaximumOverIntervalGauge {
57+
/// Create a new [`MaximumOverIntervalGauge`].
58+
pub fn new<S1: Into<String>, S2: Into<String>>(
59+
name: S1,
60+
help: S2,
61+
interval: Duration,
62+
) -> Result<Self, Error> {
63+
let maximum_value = Arc::new(AtomicF64::new(0.0));
64+
65+
Ok(Self {
66+
value: Arc::new(AtomicF64::new(0.0)),
67+
maximum_value: maximum_value.clone(),
68+
69+
interval_expiry: Arc::new(RwLock::new(Instant::now() + interval)),
70+
interval_duration: interval,
71+
gauge: PullingGauge::new(
72+
name,
73+
help,
74+
Box::new(move || maximum_value.get()),
75+
)?,
76+
})
77+
}
78+
79+
/// Increments the gauge by 1.
80+
pub fn inc(&self) {
81+
self.apply_delta(1.0);
82+
}
83+
84+
/// Decrements the gauge by 1.
85+
pub fn dec(&self) {
86+
self.apply_delta(-1.0);
87+
}
88+
89+
/// Add the given value to the gauge.
90+
///
91+
/// (The value can be negative, resulting in a decrement of the gauge.)
92+
pub fn inc_by(&self, v: f64) {
93+
self.apply_delta(v);
94+
}
95+
96+
/// Subtract the given value from the gauge.
97+
///
98+
/// (The value can be negative, resulting in an increment of the gauge.)
99+
pub fn dec_by(&self, v: f64) {
100+
self.apply_delta(-v);
101+
}
102+
103+
pub fn observe(&self, v: f64) {
104+
let previous_value = self.value.swap(v, Ordering::AcqRel);
105+
if self.maximum_value.get() < previous_value {
106+
self.maximum_value.set(previous_value);
107+
}
108+
}
109+
110+
fn apply_delta(&self, delta: f64) {
111+
let previous_value = self.value.fetch_add(delta);
112+
let new_value = previous_value + delta;
113+
114+
let now = Instant::now();
115+
let interval_expiry = self.interval_expiry.upgradable_read();
116+
let loaded_interval_expiry = *interval_expiry;
117+
118+
// Check whether we've crossed into the new interval.
119+
if loaded_interval_expiry < now {
120+
// There's a possible optimization here of using try_upgrade in a loop. Need to write
121+
// benchmarks to verify.
122+
let mut interval_expiry = RwLockUpgradableReadGuard::upgrade(interval_expiry);
123+
124+
// Did we get to be the thread that actually started the new interval? Other threads
125+
// could have updated the value before we got the exclusive lock.
126+
if *interval_expiry == loaded_interval_expiry {
127+
*interval_expiry = now + self.interval_duration;
128+
self.maximum_value.set(new_value);
129+
130+
return;
131+
}
132+
}
133+
134+
// Set the maximum_value to the max of the current value & previous max.
135+
self.maximum_value.fetch_max(new_value, Ordering::Relaxed);
136+
}
137+
}
138+
139+
impl Collector for MaximumOverIntervalGauge {
140+
fn desc(&self) -> Vec<&crate::core::Desc> {
141+
self.gauge.desc()
142+
}
143+
144+
fn collect(&self) -> Vec<crate::proto::MetricFamily> {
145+
// Apply a delta of '0' to ensure that the reset-value-if-interval-expired-logic kicks in.
146+
self.apply_delta(0.0);
147+
148+
self.gauge.collect()
149+
}
150+
}
151+
152+
#[cfg(test)]
153+
mod test {
154+
use mock_instant::MockClock;
155+
156+
use super::*;
157+
158+
static INTERVAL: Duration = Duration::from_secs(30);
159+
160+
#[test]
161+
fn test_correct_behaviour() {
162+
let gauge = MaximumOverIntervalGauge::new(
163+
"test_counter".to_string(),
164+
"This won't help you".to_string(),
165+
INTERVAL,
166+
)
167+
.unwrap();
168+
169+
assert_metric_value(&gauge, 0.0);
170+
171+
gauge.inc_by(5.0);
172+
173+
assert_metric_value(&gauge, 5.0);
174+
175+
gauge.dec();
176+
177+
// The value should still be five after we decreased it as the max within the interval was 5.
178+
assert_metric_value(&gauge, 5.0);
179+
180+
MockClock::advance(INTERVAL + Duration::from_secs(1));
181+
182+
// The value should be 4 now as the next interval has started.
183+
assert_metric_value(&gauge, 4.0);
184+
185+
gauge.observe(3.0);
186+
187+
// The value should still be five after we decreased it as the max within the interval was 5.
188+
assert_metric_value(&gauge, 4.0);
189+
190+
gauge.observe(6.0);
191+
192+
// The value should be six after we inreased it as the max within the interval was 6.
193+
assert_metric_value(&gauge, 6.0);
194+
195+
gauge.observe(2.0);
196+
197+
MockClock::advance(INTERVAL + Duration::from_secs(1));
198+
199+
// The value should be 2 now as the next interval has started.
200+
assert_metric_value(&gauge, 2.0);
201+
}
202+
203+
#[test]
204+
fn test_cloning() {
205+
let gauge = MaximumOverIntervalGauge::new(
206+
"test_counter".to_string(),
207+
"This won't help you".to_string(),
208+
INTERVAL,
209+
)
210+
.unwrap();
211+
212+
let same_gauge = gauge.clone();
213+
214+
assert_metric_value(&gauge, 0.0);
215+
216+
gauge.inc_by(5.0);
217+
218+
// Read from the cloned gauge to veriy that they share data.
219+
assert_metric_value(&same_gauge, 5.0);
220+
}
221+
222+
fn assert_metric_value(gauge: &MaximumOverIntervalGauge, val: f64) {
223+
let result = gauge.collect();
224+
225+
let metric_family = result
226+
.first()
227+
.expect("expected one MetricFamily to be returned");
228+
229+
let metric = metric_family
230+
.get_metric()
231+
.first()
232+
.expect("expected one Metric to be returned");
233+
234+
assert_eq!(val, metric.get_gauge().get_value());
235+
}
236+
}

0 commit comments

Comments
 (0)