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