Skip to content

Commit 29661c0

Browse files
committed
Add Summary metric
Signed-off-by: Palash Nigam <[email protected]>
1 parent 6cd0dba commit 29661c0

File tree

4 files changed

+234
-0
lines changed

4 files changed

+234
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ prometheus-client-derive-encode = { version = "0.3.0", path = "derive-encode" }
2424
prost = { version = "0.11.0", optional = true }
2525
prost-types = { version = "0.11.0", optional = true }
2626
void = { version = "1.0", optional = true }
27+
quantiles = "0.7.1"
2728

2829
[dev-dependencies]
2930
async-std = { version = "1", features = ["attributes"] }

src/encoding/text.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use crate::metrics::exemplar::{CounterWithExemplar, Exemplar, HistogramWithExemp
2929
use crate::metrics::family::{Family, MetricConstructor};
3030
use crate::metrics::gauge::{self, Gauge};
3131
use crate::metrics::histogram::Histogram;
32+
use crate::metrics::summary::Summary;
3233
use crate::metrics::info::Info;
3334
use crate::metrics::{MetricType, TypedMetric};
3435
use crate::registry::{Registry, Unit};
@@ -186,6 +187,7 @@ impl Encode for MetricType {
186187
MetricType::Histogram => "histogram",
187188
MetricType::Info => "info",
188189
MetricType::Unknown => "unknown",
190+
MetricType::Summary => "summary",
189191
};
190192

191193
writer.write_all(t.as_bytes())?;
@@ -323,6 +325,23 @@ impl<'a> BucketEncoder<'a> {
323325
})
324326
}
325327

328+
/// Encode a quantile. Used for the [`Summary`] metric type.
329+
pub fn encode_quantile(&mut self, quantile: f64) -> Result<ValueEncoder, std::io::Error> {
330+
if self.opened_curly_brackets {
331+
self.writer.write_all(b",")?;
332+
} else {
333+
self.writer.write_all(b"{")?;
334+
}
335+
336+
self.writer.write_all(b"quantile=\"")?;
337+
quantile.encode(self.writer)?;
338+
self.writer.write_all(b"\"}")?;
339+
340+
Ok(ValueEncoder {
341+
writer: self.writer,
342+
})
343+
}
344+
326345
/// Signal that the metric type has no bucket.
327346
pub fn no_bucket(&mut self) -> Result<ValueEncoder, std::io::Error> {
328347
if self.opened_curly_brackets {
@@ -579,6 +598,41 @@ fn encode_histogram_with_maybe_exemplars<S: Encode>(
579598
Ok(())
580599
}
581600

601+
602+
/////////////////////////////////////////////////////////////////////////////////
603+
// Summary
604+
605+
impl EncodeMetric for Summary {
606+
fn encode(&self, mut encoder: Encoder) -> Result<(), std::io::Error> {
607+
let (sum, count, quantiles) = self.get();
608+
609+
encoder
610+
.encode_suffix("sum")?
611+
.no_bucket()?
612+
.encode_value(sum)?
613+
.no_exemplar()?;
614+
encoder
615+
.encode_suffix("count")?
616+
.no_bucket()?
617+
.encode_value(count)?
618+
.no_exemplar()?;
619+
620+
for (_, (quantile, result)) in quantiles.iter().enumerate() {
621+
let mut bucket_encoder = encoder.no_suffix()?;
622+
let mut value_encoder = bucket_encoder.encode_quantile(*quantile)?;
623+
let mut exemplar_encoder = value_encoder.encode_value(*result)?;
624+
exemplar_encoder.no_exemplar()?
625+
}
626+
627+
Result::Ok(())
628+
}
629+
630+
fn metric_type(&self) -> MetricType {
631+
Self::TYPE
632+
}
633+
}
634+
635+
582636
/////////////////////////////////////////////////////////////////////////////////
583637
// Info
584638

@@ -818,6 +872,32 @@ mod tests {
818872
parse_with_python_client(String::from_utf8(encoded).unwrap());
819873
}
820874

875+
#[test]
876+
fn encode_summary() {
877+
let mut registry = Registry::default();
878+
let summary = Summary::new(3, 10, vec![0.5, 0.9, 0.99], 0.0);
879+
registry.register("my_summary", "My summary", summary.clone());
880+
summary.observe(0.10);
881+
summary.observe(0.20);
882+
summary.observe(0.30);
883+
884+
let mut encoded = Vec::new();
885+
886+
encode(&mut encoded, &registry).unwrap();
887+
888+
let expected = "# HELP my_summary My summary.\n".to_owned()
889+
+ "# TYPE my_summary summary\n"
890+
+ "my_summary_sum 0.6000000000000001\n"
891+
+ "my_summary_count 3\n"
892+
+ "my_summary{quantile=\"0.5\"} 0.2\n"
893+
+ "my_summary{quantile=\"0.9\"} 0.3\n"
894+
+ "my_summary{quantile=\"0.99\"} 0.3\n"
895+
+ "# EOF\n";
896+
assert_eq!(expected, String::from_utf8(encoded.clone()).unwrap());
897+
898+
parse_with_python_client(String::from_utf8(encoded).unwrap());
899+
}
900+
821901
fn parse_with_python_client(input: String) {
822902
pyo3::prepare_freethreaded_python();
823903

src/metrics.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod exemplar;
55
pub mod family;
66
pub mod gauge;
77
pub mod histogram;
8+
pub mod summary;
89
pub mod info;
910

1011
/// A metric that is aware of its Open Metrics metric type.
@@ -22,6 +23,7 @@ pub enum MetricType {
2223
Histogram,
2324
Info,
2425
Unknown,
26+
Summary,
2527
// Not (yet) supported metric types.
2628
//
2729
// GaugeHistogram,

src/metrics/summary.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//! Module implementing an Open Metrics histogram.
2+
//!
3+
//! See [`Summary`] for details.
4+
5+
use super::{MetricType, TypedMetric};
6+
//use owning_ref::OwningRef;
7+
//use std::iter::{self, once};
8+
use std::sync::{Arc, Mutex};
9+
use std::time::{Duration, Instant};
10+
11+
use quantiles::ckms::CKMS;
12+
13+
/// Open Metrics [`Summary`] to measure distributions of discrete events.
14+
#[derive(Debug)]
15+
pub struct Summary {
16+
target_quantile: Vec<f64>,
17+
target_error: f64,
18+
max_age_buckets: u64,
19+
max_age_seconds: u64,
20+
stream_duration: Duration,
21+
inner: Arc<Mutex<InnerSummary>>,
22+
}
23+
24+
impl Clone for Summary {
25+
fn clone(&self) -> Self {
26+
Summary {
27+
target_quantile: self.target_quantile.clone(),
28+
target_error: self.target_error,
29+
max_age_buckets: self.max_age_buckets,
30+
max_age_seconds: self.max_age_seconds,
31+
stream_duration: self.stream_duration,
32+
inner: self.inner.clone(),
33+
}
34+
}
35+
}
36+
37+
#[derive(Debug)]
38+
pub(crate) struct InnerSummary {
39+
sum: f64,
40+
count: u64,
41+
quantile_streams: Vec<CKMS<f64>>,
42+
// head_stream is like a cursor which carries the index
43+
// of the stream in the quantile_streams that we want to query.
44+
head_stream_idx: u64,
45+
// timestamp at which the head_stream_idx was last rotated.
46+
last_rotated_timestamp: Instant,
47+
}
48+
49+
impl Summary {
50+
/// Create a new [`Summary`].
51+
pub fn new(max_age_buckets: u64, max_age_seconds: u64, target_quantile: Vec<f64>, target_error: f64) -> Self {
52+
let mut streams: Vec<CKMS<f64>> = Vec::new();
53+
for _ in 0..max_age_buckets {
54+
streams.push(CKMS::new(target_error));
55+
}
56+
57+
let stream_duration = Duration::from_secs(max_age_seconds / max_age_buckets);
58+
let last_rotated_timestamp = Instant::now();
59+
60+
if target_quantile.iter().any(|&x| x > 1.0 || x < 0.0) {
61+
panic!("Quantile value out of range");
62+
}
63+
64+
Summary{
65+
max_age_buckets,
66+
max_age_seconds,
67+
stream_duration,
68+
target_quantile,
69+
target_error,
70+
inner: Arc::new(Mutex::new(InnerSummary {
71+
sum: Default::default(),
72+
count: Default::default(),
73+
quantile_streams: streams,
74+
head_stream_idx: 0,
75+
last_rotated_timestamp,
76+
}))
77+
}
78+
}
79+
80+
/// Observe the given value.
81+
pub fn observe(&self, v: f64) {
82+
self.rotate_buckets();
83+
84+
let mut inner = self.inner.lock().unwrap();
85+
inner.sum += v;
86+
inner.count += 1;
87+
88+
// insert quantiles into all streams/buckets.
89+
for stream in inner.quantile_streams.iter_mut() {
90+
stream.insert(v);
91+
}
92+
}
93+
94+
/// Retrieve the values of the summary metric.
95+
pub fn get(&self) -> (f64, u64, Vec<(f64, f64)>) {
96+
self.rotate_buckets();
97+
98+
let inner = self.inner.lock().unwrap();
99+
let sum = inner.sum;
100+
let count = inner.count;
101+
let mut quantile_values: Vec<(f64, f64)> = Vec::new();
102+
103+
for q in self.target_quantile.iter() {
104+
match inner.quantile_streams[inner.head_stream_idx as usize].query(*q) {
105+
Some((_, v)) => quantile_values.push((*q, v)),
106+
None => continue,
107+
};
108+
}
109+
(sum, count, quantile_values)
110+
}
111+
112+
fn rotate_buckets(&self) {
113+
let mut inner = self.inner.lock().unwrap();
114+
if inner.last_rotated_timestamp.elapsed() >= self.stream_duration {
115+
inner.last_rotated_timestamp = Instant::now();
116+
if inner.head_stream_idx == self.max_age_buckets {
117+
inner.head_stream_idx = 0;
118+
} else {
119+
inner.head_stream_idx += 1;
120+
}
121+
};
122+
}
123+
}
124+
125+
impl TypedMetric for Summary {
126+
const TYPE: MetricType = MetricType::Summary;
127+
}
128+
129+
#[cfg(test)]
130+
mod tests {
131+
use super::*;
132+
133+
#[test]
134+
fn summary() {
135+
let summary = Summary::new(5, 10, vec![0.5, 0.9, 0.99], 0.01);
136+
summary.observe(1.0);
137+
summary.observe(5.0);
138+
summary.observe(10.0);
139+
140+
let (s, c, q) = summary.get();
141+
assert_eq!(16.0, s);
142+
assert_eq!(3, c);
143+
assert_eq!(vec![(0.5, 5.0), (0.9, 10.0), (0.99, 10.0)], q);
144+
}
145+
146+
#[test]
147+
#[should_panic(expected="Quantile value out of range")]
148+
fn summary_panic() {
149+
Summary::new(5, 10, vec![1.0, 5.0, 9.0], 0.01);
150+
}
151+
}

0 commit comments

Comments
 (0)