/rust/registry/src/index.crates.io-1949cf8c6b5b557f/prometheus-client-0.24.0/src/metrics/histogram.rs
Line | Count | Source |
1 | | //! Module implementing an Open Metrics histogram. |
2 | | //! |
3 | | //! See [`Histogram`] for details. |
4 | | |
5 | | use crate::encoding::{EncodeMetric, MetricEncoder, NoLabelSet}; |
6 | | |
7 | | use super::{MetricType, TypedMetric}; |
8 | | use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; |
9 | | use std::iter::{self, once}; |
10 | | use std::sync::Arc; |
11 | | |
12 | | /// Open Metrics [`Histogram`] to measure distributions of discrete events. |
13 | | /// |
14 | | /// ``` |
15 | | /// # use prometheus_client::metrics::histogram::{Histogram, exponential_buckets}; |
16 | | /// let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10)); |
17 | | /// histogram.observe(4.2); |
18 | | /// ``` |
19 | | /// |
20 | | /// [`Histogram`] does not implement [`Default`], given that the choice of |
21 | | /// bucket values depends on the situation [`Histogram`] is used in. As an |
22 | | /// example, to measure HTTP request latency, the values suggested in the |
23 | | /// Golang implementation might work for you: |
24 | | /// |
25 | | /// ``` |
26 | | /// # use prometheus_client::metrics::histogram::Histogram; |
27 | | /// // Default values from go client(https://github.com/prometheus/client_golang/blob/5d584e2717ef525673736d72cd1d12e304f243d7/prometheus/histogram.go#L68) |
28 | | /// let custom_buckets = [ |
29 | | /// 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, |
30 | | /// ]; |
31 | | /// let histogram = Histogram::new(custom_buckets); |
32 | | /// histogram.observe(4.2); |
33 | | /// ``` |
34 | | // TODO: Consider using atomics. See |
35 | | // https://github.com/tikv/rust-prometheus/pull/314. |
36 | | #[derive(Debug)] |
37 | | pub struct Histogram { |
38 | | inner: Arc<RwLock<Inner>>, |
39 | | } |
40 | | |
41 | | impl Clone for Histogram { |
42 | 0 | fn clone(&self) -> Self { |
43 | 0 | Histogram { |
44 | 0 | inner: self.inner.clone(), |
45 | 0 | } |
46 | 0 | } |
47 | | } |
48 | | |
49 | | #[derive(Debug)] |
50 | | pub(crate) struct Inner { |
51 | | // TODO: Consider allowing integer observe values. |
52 | | sum: f64, |
53 | | count: u64, |
54 | | // TODO: Consider being generic over the bucket length. |
55 | | buckets: Vec<(f64, u64)>, |
56 | | } |
57 | | |
58 | | impl Histogram { |
59 | | /// Create a new [`Histogram`]. |
60 | | /// |
61 | | /// ```rust |
62 | | /// # use prometheus_client::metrics::histogram::Histogram; |
63 | | /// let histogram = Histogram::new([10.0, 100.0, 1_000.0]); |
64 | | /// ``` |
65 | 0 | pub fn new(buckets: impl IntoIterator<Item = f64>) -> Self { |
66 | | Self { |
67 | 0 | inner: Arc::new(RwLock::new(Inner { |
68 | 0 | sum: Default::default(), |
69 | 0 | count: Default::default(), |
70 | 0 | buckets: buckets |
71 | 0 | .into_iter() |
72 | 0 | .chain(once(f64::MAX)) |
73 | 0 | .map(|upper_bound| (upper_bound, 0)) Unexecuted instantiation: <prometheus_client::metrics::histogram::Histogram>::new::<alloc::vec::Vec<f64>>::{closure#0}Unexecuted instantiation: <prometheus_client::metrics::histogram::Histogram>::new::<_>::{closure#0} |
74 | 0 | .collect(), |
75 | | })), |
76 | | } |
77 | 0 | } Unexecuted instantiation: <prometheus_client::metrics::histogram::Histogram>::new::<alloc::vec::Vec<f64>> Unexecuted instantiation: <prometheus_client::metrics::histogram::Histogram>::new::<_> |
78 | | |
79 | | /// Observe the given value. |
80 | 0 | pub fn observe(&self, v: f64) { |
81 | 0 | self.observe_and_bucket(v); |
82 | 0 | } |
83 | | |
84 | | /// Observes the given value, returning the index of the first bucket the |
85 | | /// value is added to. |
86 | | /// |
87 | | /// Needed in |
88 | | /// [`HistogramWithExemplars`](crate::metrics::exemplar::HistogramWithExemplars). |
89 | 0 | pub(crate) fn observe_and_bucket(&self, v: f64) -> Option<usize> { |
90 | 0 | let mut inner = self.inner.write(); |
91 | 0 | inner.sum += v; |
92 | 0 | inner.count += 1; |
93 | | |
94 | 0 | let first_bucket = inner |
95 | 0 | .buckets |
96 | 0 | .iter_mut() |
97 | 0 | .enumerate() |
98 | 0 | .find(|(_i, (upper_bound, _value))| upper_bound >= &v); |
99 | | |
100 | 0 | match first_bucket { |
101 | 0 | Some((i, (_upper_bound, value))) => { |
102 | 0 | *value += 1; |
103 | 0 | Some(i) |
104 | | } |
105 | 0 | None => None, |
106 | | } |
107 | 0 | } |
108 | | |
109 | 0 | pub(crate) fn get(&self) -> (f64, u64, MappedRwLockReadGuard<'_, Vec<(f64, u64)>>) { |
110 | 0 | let inner = self.inner.read(); |
111 | 0 | let sum = inner.sum; |
112 | 0 | let count = inner.count; |
113 | 0 | let buckets = RwLockReadGuard::map(inner, |inner| &inner.buckets); |
114 | 0 | (sum, count, buckets) |
115 | 0 | } |
116 | | } |
117 | | |
118 | | impl TypedMetric for Histogram { |
119 | | const TYPE: MetricType = MetricType::Histogram; |
120 | | } |
121 | | |
122 | | /// Exponential bucket distribution. |
123 | 0 | pub fn exponential_buckets(start: f64, factor: f64, length: u16) -> impl Iterator<Item = f64> { |
124 | 0 | iter::repeat(()) |
125 | 0 | .enumerate() |
126 | 0 | .map(move |(i, _)| start * factor.powf(i as f64)) |
127 | 0 | .take(length.into()) |
128 | 0 | } |
129 | | |
130 | | /// Exponential bucket distribution within a range |
131 | | /// |
132 | | /// Creates `length` buckets, where the lowest bucket is `min` and the highest bucket is `max`. |
133 | | /// |
134 | | /// If `length` is less than 1, or `min` is less than or equal to 0, an empty iterator is returned. |
135 | 0 | pub fn exponential_buckets_range(min: f64, max: f64, length: u16) -> impl Iterator<Item = f64> { |
136 | 0 | let mut len_observed = length; |
137 | 0 | let mut min_bucket = min; |
138 | | // length needs a positive length and min needs to be greater than 0 |
139 | | // set len_observed to 0 and min_bucket to 1.0 |
140 | | // this will return an empty iterator in the result |
141 | 0 | if length < 1 || min <= 0.0 { |
142 | 0 | len_observed = 0; |
143 | 0 | min_bucket = 1.0; |
144 | 0 | } |
145 | | // We know max/min and highest bucket. Solve for growth_factor. |
146 | 0 | let growth_factor = (max / min_bucket).powf(1.0 / (len_observed as f64 - 1.0)); |
147 | | |
148 | 0 | iter::repeat(()) |
149 | 0 | .enumerate() |
150 | 0 | .map(move |(i, _)| min_bucket * growth_factor.powf(i as f64)) |
151 | 0 | .take(len_observed.into()) |
152 | 0 | } |
153 | | |
154 | | /// Linear bucket distribution. |
155 | 0 | pub fn linear_buckets(start: f64, width: f64, length: u16) -> impl Iterator<Item = f64> { |
156 | 0 | iter::repeat(()) |
157 | 0 | .enumerate() |
158 | 0 | .map(move |(i, _)| start + (width * (i as f64))) |
159 | 0 | .take(length.into()) |
160 | 0 | } |
161 | | |
162 | | impl EncodeMetric for Histogram { |
163 | 0 | fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> { |
164 | 0 | let (sum, count, buckets) = self.get(); |
165 | 0 | encoder.encode_histogram::<NoLabelSet>(sum, count, &buckets, None) |
166 | 0 | } |
167 | | |
168 | 0 | fn metric_type(&self) -> MetricType { |
169 | 0 | Self::TYPE |
170 | 0 | } |
171 | | } |
172 | | |
173 | | #[cfg(test)] |
174 | | mod tests { |
175 | | use super::*; |
176 | | |
177 | | #[test] |
178 | | fn histogram() { |
179 | | let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10)); |
180 | | histogram.observe(1.0); |
181 | | } |
182 | | |
183 | | #[test] |
184 | | fn exponential() { |
185 | | assert_eq!( |
186 | | vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0], |
187 | | exponential_buckets(1.0, 2.0, 10).collect::<Vec<_>>() |
188 | | ); |
189 | | } |
190 | | |
191 | | #[test] |
192 | | fn linear() { |
193 | | assert_eq!( |
194 | | vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], |
195 | | linear_buckets(0.0, 1.0, 10).collect::<Vec<_>>() |
196 | | ); |
197 | | } |
198 | | |
199 | | #[test] |
200 | | fn exponential_range() { |
201 | | assert_eq!( |
202 | | vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0], |
203 | | exponential_buckets_range(1.0, 32.0, 6).collect::<Vec<_>>() |
204 | | ); |
205 | | } |
206 | | |
207 | | #[test] |
208 | | fn exponential_range_incorrect() { |
209 | | let res = exponential_buckets_range(1.0, 32.0, 0).collect::<Vec<_>>(); |
210 | | assert!(res.is_empty()); |
211 | | |
212 | | let res = exponential_buckets_range(0.0, 32.0, 6).collect::<Vec<_>>(); |
213 | | assert!(res.is_empty()); |
214 | | } |
215 | | } |