prometheus_client/metrics/
histogram.rs1use crate::encoding::{EncodeMetric, MetricEncoder, NoLabelSet};
6
7use super::{MetricType, TypedMetric};
8use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
9use std::iter::{self, once};
10use std::sync::Arc;
11
12#[derive(Debug)]
37pub struct Histogram {
38 inner: Arc<RwLock<Inner>>,
39}
40
41impl Clone for Histogram {
42 fn clone(&self) -> Self {
43 Histogram {
44 inner: self.inner.clone(),
45 }
46 }
47}
48
49#[derive(Debug)]
50pub(crate) struct Inner {
51 sum: f64,
53 count: u64,
54 buckets: Vec<(f64, u64)>,
56}
57
58impl Histogram {
59 pub fn new(buckets: impl IntoIterator<Item = f64>) -> Self {
66 Self {
67 inner: Arc::new(RwLock::new(Inner {
68 sum: Default::default(),
69 count: Default::default(),
70 buckets: buckets
71 .into_iter()
72 .chain(once(f64::MAX))
73 .map(|upper_bound| (upper_bound, 0))
74 .collect(),
75 })),
76 }
77 }
78
79 pub fn observe(&self, v: f64) {
81 self.observe_and_bucket(v);
82 }
83
84 #[cfg(any(test, feature = "test-util"))]
86 pub fn sum(&self) -> f64 {
87 self.inner.read().sum
88 }
89
90 #[cfg(any(test, feature = "test-util"))]
92 pub fn count(&self) -> u64 {
93 self.inner.read().count
94 }
95
96 pub(crate) fn observe_and_bucket(&self, v: f64) -> Option<usize> {
102 let mut inner = self.inner.write();
103 inner.sum += v;
104 inner.count += 1;
105
106 let first_bucket = inner
107 .buckets
108 .iter_mut()
109 .enumerate()
110 .find(|(_i, (upper_bound, _value))| upper_bound >= &v);
111
112 match first_bucket {
113 Some((i, (_upper_bound, value))) => {
114 *value += 1;
115 Some(i)
116 }
117 None => None,
118 }
119 }
120
121 pub(crate) fn get(&self) -> (f64, u64, MappedRwLockReadGuard<'_, Vec<(f64, u64)>>) {
122 let inner = self.inner.read();
123 let sum = inner.sum;
124 let count = inner.count;
125 let buckets = RwLockReadGuard::map(inner, |inner| &inner.buckets);
126 (sum, count, buckets)
127 }
128}
129
130impl TypedMetric for Histogram {
131 const TYPE: MetricType = MetricType::Histogram;
132}
133
134pub fn exponential_buckets(start: f64, factor: f64, length: u16) -> impl Iterator<Item = f64> {
136 iter::repeat(())
137 .enumerate()
138 .map(move |(i, _)| start * factor.powf(i as f64))
139 .take(length.into())
140}
141
142pub fn exponential_buckets_range(min: f64, max: f64, length: u16) -> impl Iterator<Item = f64> {
148 let mut len_observed = length;
149 let mut min_bucket = min;
150 if length < 1 || min <= 0.0 {
154 len_observed = 0;
155 min_bucket = 1.0;
156 }
157 let growth_factor = (max / min_bucket).powf(1.0 / (len_observed as f64 - 1.0));
159
160 iter::repeat(())
161 .enumerate()
162 .map(move |(i, _)| min_bucket * growth_factor.powf(i as f64))
163 .take(len_observed.into())
164}
165
166pub fn linear_buckets(start: f64, width: f64, length: u16) -> impl Iterator<Item = f64> {
168 iter::repeat(())
169 .enumerate()
170 .map(move |(i, _)| start + (width * (i as f64)))
171 .take(length.into())
172}
173
174impl EncodeMetric for Histogram {
175 fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> {
176 let (sum, count, buckets) = self.get();
177 encoder.encode_histogram::<NoLabelSet>(sum, count, &buckets, None)
178 }
179
180 fn metric_type(&self) -> MetricType {
181 Self::TYPE
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn histogram() {
191 let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10));
192 histogram.observe(1.0);
193 }
194
195 #[test]
196 fn exponential() {
197 assert_eq!(
198 vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0],
199 exponential_buckets(1.0, 2.0, 10).collect::<Vec<_>>()
200 );
201 }
202
203 #[test]
204 fn linear() {
205 assert_eq!(
206 vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
207 linear_buckets(0.0, 1.0, 10).collect::<Vec<_>>()
208 );
209 }
210
211 #[test]
212 fn exponential_range() {
213 assert_eq!(
214 vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0],
215 exponential_buckets_range(1.0, 32.0, 6).collect::<Vec<_>>()
216 );
217 }
218
219 #[test]
220 fn exponential_range_incorrect() {
221 let res = exponential_buckets_range(1.0, 32.0, 0).collect::<Vec<_>>();
222 assert!(res.is_empty());
223
224 let res = exponential_buckets_range(0.0, 32.0, 6).collect::<Vec<_>>();
225 assert!(res.is_empty());
226 }
227
228 #[test]
230 fn count() {
231 let histogram = Histogram::new([1.0_f64, 2.0, 3.0, 4.0, 5.0]);
232 assert_eq!(
233 histogram.count(),
234 0,
235 "histogram has zero observations when instantiated"
236 );
237
238 histogram.observe(1.0);
239 assert_eq!(histogram.count(), 1, "histogram has one observation");
240
241 histogram.observe(2.5);
242 assert_eq!(histogram.count(), 2, "histogram has two observations");
243
244 histogram.observe(6.0);
245 assert_eq!(histogram.count(), 3, "histogram has three observations");
246 }
247
248 #[test]
250 fn sum() {
251 const BUCKETS: [f64; 3] = [10.0, 100.0, 1000.0];
252 let histogram = Histogram::new(BUCKETS);
253 assert_eq!(
254 histogram.sum(),
255 0.0,
256 "histogram sum is zero when instantiated"
257 );
258
259 histogram.observe(3.0); histogram.observe(4.0);
261 histogram.observe(15.0);
262 histogram.observe(101.0);
263 assert_eq!(
264 histogram.sum(),
265 123.0,
266 "histogram sum records accurate sum of observations"
267 );
268
269 histogram.observe(1111.0);
270 assert_eq!(
271 histogram.sum(),
272 1234.0,
273 "histogram sum records accurate sum of observations"
274 );
275 }
276}