Skip to content

Commit

Permalink
feat(metrics): add Summary implementation
Browse files Browse the repository at this point in the history
This commit introduces the `Summary` metric type for the Rust Prometheus client, addressing the requirements outlined in prometheus#40.

- Implements the `Summary` type to represent summary statistics such as sum, count, and quantiles.
- The implementation does not include a specific quantile calculation algorithm. Users are expected to provide precomputed quantiles based on their chosen algorithm.
- Supports encoding summary metrics following the OpenMetrics standard.

- `Summary` provides:
  - `reset` method to update its internal state with sum, count, and quantiles.
  - Integration with the `TypedMetric` and `EncodeMetric` traits to support encoding and registration with the Prometheus client.

- Documentation and tests have been added to ensure correct usage and behavior.
- This implementation is designed to give users flexibility in handling quantile computation.

- Future work could explore integrating popular quantile calculation algorithms to offer built-in support while retaining the option for custom implementations.

Signed-off-by: zth <[email protected]>
  • Loading branch information
zhangtianhao committed Nov 29, 2024
1 parent 12923ca commit ac3878d
Show file tree
Hide file tree
Showing 5 changed files with 500 additions and 1 deletion.
16 changes: 16 additions & 0 deletions src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub use prometheus_client_derive_encode::*;

use crate::metrics::exemplar::Exemplar;
use crate::metrics::MetricType;
use crate::metrics::summary::Numeric;
use crate::registry::{Prefix, Unit};
use std::borrow::Cow;
use std::collections::HashMap;
Expand Down Expand Up @@ -186,6 +187,21 @@ impl MetricEncoder<'_> {
)
}

/// Encode a summary.
pub fn encode_summary<T: ToString + Numeric>(
&mut self,
sum: f64,
count: u64,
quantiles: &[(f64, T)],
) -> Result<(), std::fmt::Error> {
for_both_mut!(
self,
MetricEncoderInner,
e,
e.encode_summary(sum, count, quantiles)
)
}

/// Encode a metric family.
pub fn encode_family<'s, S: EncodeLabelSet>(
&'s mut self,
Expand Down
125 changes: 125 additions & 0 deletions src/encoding/protobuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub mod openmetrics_data_model {
use std::{borrow::Cow, collections::HashMap};

use crate::metrics::MetricType;
use crate::metrics::summary::Numeric;
use crate::registry::{Registry, Unit};
use crate::{metrics::exemplar::Exemplar, registry::Prefix};

Expand All @@ -53,6 +54,7 @@ impl From<MetricType> for openmetrics_data_model::MetricType {
MetricType::Counter => openmetrics_data_model::MetricType::Counter,
MetricType::Gauge => openmetrics_data_model::MetricType::Gauge,
MetricType::Histogram => openmetrics_data_model::MetricType::Histogram,
MetricType::Summary => openmetrics_data_model::MetricType::Summary,
MetricType::Info => openmetrics_data_model::MetricType::Info,
MetricType::Unknown => openmetrics_data_model::MetricType::Unknown,
}
Expand Down Expand Up @@ -288,6 +290,41 @@ impl MetricEncoder<'_> {

Ok(())
}

pub fn encode_summary<T: Numeric>(
&mut self,
sum: f64,
count: u64,
quantiles: &[(f64, T)],
) -> Result<(), std::fmt::Error> {
let quantiles = quantiles.iter()
.map(|(q, v)| {
openmetrics_data_model::summary_value::Quantile {
quantile: *q,
value: v.as_f64(),
}
})
.collect::<Vec<_>>();

self.family.push(openmetrics_data_model::Metric {
labels: self.labels.clone(),
metric_points: vec![openmetrics_data_model::MetricPoint {
value: Some(openmetrics_data_model::metric_point::Value::SummaryValue(
openmetrics_data_model::SummaryValue {
count,
created: None,
quantile: quantiles,
sum: Some(openmetrics_data_model::summary_value::Sum::DoubleValue(
sum,
)),
},
)),
..Default::default()
}],
});

Ok(())
}
}

impl<S: EncodeLabelSet, V: EncodeExemplarValue> TryFrom<&Exemplar<S, V>>
Expand Down Expand Up @@ -448,6 +485,7 @@ mod tests {
use crate::metrics::family::Family;
use crate::metrics::gauge::Gauge;
use crate::metrics::histogram::{exponential_buckets, Histogram};
use crate::metrics::summary::Summary;
use crate::metrics::info::Info;
use crate::registry::Unit;
use std::borrow::Cow;
Expand Down Expand Up @@ -817,6 +855,93 @@ mod tests {
}
}

#[test]
fn encode_summary() {
let mut registry = Registry::default();
let summary = Summary::default();
registry.register("my_summary", "My Summary", summary.clone());
let quantiles = vec![
(0.5, 100.1),
(0.9, 300.2),
(0.99, 700.3),
];
let _ = summary.reset(123.0, 10, quantiles);

let metric_set = encode(&registry).unwrap();

let family = metric_set.metric_families.first().unwrap();
assert_eq!("my_summary", family.name);
assert_eq!("My Summary.", family.help);
assert_eq!(
openmetrics_data_model::MetricType::Summary as i32,
extract_metric_type(&metric_set)
);
match extract_metric_point_value(&metric_set) {
openmetrics_data_model::metric_point::Value::SummaryValue(value) => {
assert_eq!(
Some(openmetrics_data_model::summary_value::Sum::DoubleValue(
123.0
)),
value.sum
);
assert_eq!(10, value.count);
assert_eq!(3, value.quantile.len());
}
_ => panic!("wrong value type"),
}
}

#[test]
fn encode_summary_family() {
let mut registry = Registry::default();
let family =
Family::new_with_constructor(|| Summary::default());
registry.register("my_summary", "My Summary", family.clone());
let quantiles = vec![
(0.5, 100_u64),
(0.9, 300),
(0.99, 700),
];
let _ = family
.get_or_create(&vec![
("method".to_string(), "POST".to_string()),
("status".to_string(), "200".to_string()),
])
.reset(123.0, 10, quantiles);

let metric_set = encode(&registry).unwrap();

let family = metric_set.metric_families.first().unwrap();
assert_eq!("my_summary", family.name);
assert_eq!("My Summary.", family.help);
assert_eq!(
openmetrics_data_model::MetricType::Summary as i32,
extract_metric_type(&metric_set)
);

let metric = family.metrics.first().unwrap();
assert_eq!(2, metric.labels.len());
assert_eq!("method", metric.labels[0].name);
assert_eq!("POST", metric.labels[0].value);
assert_eq!("status", metric.labels[1].name);
assert_eq!("200", metric.labels[1].value);

match extract_metric_point_value(&metric_set) {
openmetrics_data_model::metric_point::Value::SummaryValue(value) => {
assert_eq!(None, value.created);
assert_eq!(
Some(openmetrics_data_model::summary_value::Sum::DoubleValue(
123.0
)),
value.sum
);
assert_eq!(10, value.count);
assert_eq!(3, value.quantile.len());
}
_ => panic!("wrong value type"),
}
}

#[test]
fn encode_family_counter_histogram() {
let mut registry = Registry::default();
Expand Down
133 changes: 133 additions & 0 deletions src/encoding/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,41 @@ impl<'a> MetricEncoder<'a> {
Ok(())
}

pub fn encode_summary<T: ToString>(
&mut self,
sum: f64,
count: u64,
quantiles: &[(f64, T)],
) -> Result<(), std::fmt::Error> {
self.write_prefix_name_unit()?;
self.write_suffix("sum")?;
self.encode_labels::<NoLabelSet>(None)?;
self.writer.write_str(" ")?;
self.writer.write_str(dtoa::Buffer::new().format(sum))?;
self.newline()?;

self.write_prefix_name_unit()?;
self.write_suffix("count")?;
self.encode_labels::<NoLabelSet>(None)?;
self.writer.write_str(" ")?;
self.writer.write_str(itoa::Buffer::new().format(count))?;
self.newline()?;

for (quantile, value) in quantiles {
self.write_prefix_name_unit()?;

self.encode_labels(Some(&[("quantile", *quantile)]))?;

self.writer.write_str(" ")?;
self.writer
.write_str(&value.to_string())?;

self.newline()?;
}

Ok(())
}

fn newline(&mut self) -> Result<(), std::fmt::Error> {
self.writer.write_str("\n")
}
Expand Down Expand Up @@ -730,6 +765,7 @@ mod tests {
use crate::metrics::family::Family;
use crate::metrics::gauge::Gauge;
use crate::metrics::histogram::{exponential_buckets, Histogram};
use crate::metrics::summary::Summary;
use crate::metrics::info::Info;
use crate::metrics::{counter::Counter, exemplar::CounterWithExemplar};
use pyo3::{prelude::*, types::PyModule};
Expand Down Expand Up @@ -981,6 +1017,103 @@ mod tests {
parse_with_python_client(encoded);
}

#[test]
fn encode_summary() {
let mut registry = Registry::default();
let summary = Summary::default();
registry.register("my_summary", "My Summary", summary.clone());
let quantiles = vec![
(0.5, 100.1),
(0.9, 300.2),
(0.99, 700.3),
];
let _ = summary.reset(123.0, 10, quantiles);

let mut encoded = String::new();
encode(&mut encoded, &registry).unwrap();

let expected = "# HELP my_summary My Summary.\n".to_owned()
+ "# TYPE my_summary summary\n"
+ "my_summary_sum 123.0\n"
+ "my_summary_count 10\n"
+ "my_summary{quantile=\"0.5\"} 100.1\n"
+ "my_summary{quantile=\"0.9\"} 300.2\n"
+ "my_summary{quantile=\"0.99\"} 700.3\n"
+ "# EOF\n";

assert_eq!(expected, encoded);
}

#[test]
fn encode_summary_family() {
let mut registry = Registry::default();
let family =
Family::new_with_constructor(|| Summary::default());
registry.register("my_summary", "My Summary", family.clone());
let quantiles = vec![
(0.5, 100_u64),
(0.9, 300),
(0.99, 700),
];
let _ = family
.get_or_create(&vec![
("method".to_string(), "GET".to_string()),
("status".to_string(), "200".to_string()),
])
.reset(123.0, 10, quantiles);

let mut encoded = String::new();
encode(&mut encoded, &registry).unwrap();

let expected = "# HELP my_summary My Summary.\n".to_owned()
+ "# TYPE my_summary summary\n"
+ "my_summary_sum{method=\"GET\",status=\"200\"} 123.0\n"
+ "my_summary_count{method=\"GET\",status=\"200\"} 10\n"
+ "my_summary{quantile=\"0.5\",method=\"GET\",status=\"200\"} 100\n"
+ "my_summary{quantile=\"0.9\",method=\"GET\",status=\"200\"} 300\n"
+ "my_summary{quantile=\"0.99\",method=\"GET\",status=\"200\"} 700\n"
+ "# EOF\n";

assert_eq!(expected, encoded);
}

#[test]
fn encode_summary_family_with_empty_struct_family_labels() {
let mut registry = Registry::default();
let family =
Family::new_with_constructor(|| Summary::default());
registry.register("my_summary", "My Summary", family.clone());

#[derive(Eq, PartialEq, Hash, Debug, Clone)]
struct EmptyLabels {}

impl EncodeLabelSet for EmptyLabels {
fn encode(&self, _encoder: crate::encoding::LabelSetEncoder) -> Result<(), Error> {
Ok(())
}
}

let quantiles = vec![
(0.5, 100.1),
(0.9, 300.2),
(0.99, 700.3),
];
let _ = family.get_or_create(&EmptyLabels {}).reset(123.0, 10, quantiles);
let mut encoded = String::new();
encode(&mut encoded, &registry).unwrap();

let expected = "# HELP my_summary My Summary.\n".to_owned()
+ "# TYPE my_summary summary\n"
+ "my_summary_sum{} 123.0\n"
+ "my_summary_count{} 10\n"
+ "my_summary{quantile=\"0.5\"} 100.1\n"
+ "my_summary{quantile=\"0.9\"} 300.2\n"
+ "my_summary{quantile=\"0.99\"} 700.3\n"
+ "# EOF\n";

assert_eq!(expected, encoded);
}

#[test]
fn sub_registry_with_prefix_and_label() {
let top_level_metric_name = "my_top_level_metric";
Expand Down
4 changes: 3 additions & 1 deletion src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod exemplar;
pub mod family;
pub mod gauge;
pub mod histogram;
pub mod summary;
pub mod info;

/// A metric that is aware of its Open Metrics metric type.
Expand All @@ -20,13 +21,13 @@ pub enum MetricType {
Counter,
Gauge,
Histogram,
Summary,
Info,
Unknown,
// Not (yet) supported metric types.
//
// GaugeHistogram,
// StateSet,
// Summary
}

impl MetricType {
Expand All @@ -36,6 +37,7 @@ impl MetricType {
MetricType::Counter => "counter",
MetricType::Gauge => "gauge",
MetricType::Histogram => "histogram",
MetricType::Summary => "summary",
MetricType::Info => "info",
MetricType::Unknown => "unknown",
}
Expand Down
Loading

0 comments on commit ac3878d

Please sign in to comment.