Coverage Report

Created: 2025-10-29 07:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}