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