proxy: Make each metric type responsible for formatting (#878)

In order to set up for a refactor that removes the `Metric` type, the
`FmtMetric` trait--implemented by `Counter`, `Gauge`, and
`Histogram`--is introduced to push prometheus formatting down into each
type.

With this change, the `Histogram` type now relies on `Counter` (and its
metric formatting) more heavily.
This commit is contained in:
Oliver Gould 2018-04-30 13:00:21 -07:00 committed by GitHub
parent 29330b0dc1
commit c63f0a1976
4 changed files with 136 additions and 57 deletions

View File

@ -1,5 +1,8 @@
use std::{fmt, ops}; use std::fmt::{self, Display};
use std::num::Wrapping; use std::num::Wrapping;
use std::ops;
use super::FmtMetric;
/// A Prometheus counter is represented by a `Wrapping` unsigned 64-bit int. /// A Prometheus counter is represented by a `Wrapping` unsigned 64-bit int.
/// ///
@ -53,8 +56,26 @@ impl ops::AddAssign<u64> for Counter {
} }
} }
impl fmt::Display for Counter { impl ops::AddAssign<Self> for Counter {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn add_assign(&mut self, Counter(rhs): Self) {
self.0.fmt(f) (*self).0 += rhs
}
}
impl FmtMetric for Counter {
fn fmt_metric<N: Display>(&self, f: &mut fmt::Formatter, name: N) -> fmt::Result {
writeln!(f, "{} {}", name, self.0)
}
fn fmt_metric_labeled<N, L>(&self, f: &mut fmt::Formatter, name: N, labels: L) -> fmt::Result
where
L: Display,
N: Display,
{
writeln!(f, "{name}{{{labels}}} {value}",
name = name,
labels = labels,
value = self.0,
)
} }
} }

View File

@ -1,4 +1,6 @@
use std::fmt; use std::fmt::{self, Display};
use super::FmtMetric;
/// An instaneous metric value. /// An instaneous metric value.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
@ -36,8 +38,20 @@ impl Into<u64> for Gauge {
} }
} }
impl fmt::Display for Gauge { impl FmtMetric for Gauge {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt_metric<N: Display>(&self, f: &mut fmt::Formatter, name: N) -> fmt::Result {
self.0.fmt(f) writeln!(f, "{} {}", name, self.0)
}
fn fmt_metric_labeled<N, L>(&self, f: &mut fmt::Formatter, name: N, labels: L) -> fmt::Result
where
N: Display,
L: Display,
{
writeln!(f, "{name}{{{labels}}} {value}",
name = name,
labels = labels,
value = self.0,
)
} }
} }

View File

@ -1,8 +1,8 @@
use std::{cmp, fmt, iter, slice}; use std::{cmp, iter, slice};
use std::num::Wrapping; use std::fmt::{self, Display};
use std::marker::PhantomData; use std::marker::PhantomData;
use super::Counter; use super::{Counter, FmtMetric};
/// A series of latency values and counts. /// A series of latency values and counts.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -30,7 +30,7 @@ pub struct Histogram<V: Into<u64>> {
// TODO: Implement Prometheus reset semantics correctly, taking into consideration // TODO: Implement Prometheus reset semantics correctly, taking into consideration
// that Prometheus represents this as `f64` and so there are only 52 significant // that Prometheus represents this as `f64` and so there are only 52 significant
// bits. // bits.
pub sum: Wrapping<u64>, sum: Counter,
_p: PhantomData<V>, _p: PhantomData<V>,
} }
@ -45,6 +45,15 @@ pub enum Bucket {
#[derive(Debug)] #[derive(Debug)]
pub struct Bounds(pub &'static [Bucket]); pub struct Bounds(pub &'static [Bucket]);
/// Helper that lazily formats metric keys as {0}_{1}.
struct Key<A: Display, B: Display>(A, B);
/// Helper that lazily formats comma-separated labels `A,B`.
struct Labels<A: Display, B: Display>(A, B);
/// Helper that lazily formats an `{K}="{V}"`" label.
struct Label<K: Display, V: Display>(K, V);
// ===== impl Histogram ===== // ===== impl Histogram =====
impl<V: Into<u64>> Histogram<V> { impl<V: Into<u64>> Histogram<V> {
@ -60,7 +69,7 @@ impl<V: Into<u64>> Histogram<V> {
Self { Self {
bounds, bounds,
buckets: buckets.into_boxed_slice(), buckets: buckets.into_boxed_slice(),
sum: Wrapping(0), sum: Counter::default(),
_p: PhantomData, _p: PhantomData,
} }
} }
@ -77,11 +86,7 @@ impl<V: Into<u64>> Histogram<V> {
.expect("all values must fit into a bucket"); .expect("all values must fit into a bucket");
self.buckets[idx].incr(); self.buckets[idx].incr();
self.sum += Wrapping(value); self.sum += value;
}
pub fn sum(&self) -> u64 {
self.sum.0
} }
} }
@ -97,6 +102,60 @@ impl<'a, V: Into<u64>> IntoIterator for &'a Histogram<V> {
} }
} }
impl<V: Into<u64>> FmtMetric for Histogram<V> {
fn fmt_metric<N: Display>(&self, f: &mut fmt::Formatter, name: N) -> fmt::Result {
let mut total = Counter::default();
for (le, count) in self {
total += *count;
total.fmt_metric_labeled(f, Key(&name, "bucket"), Label("le", le))?;
}
total.fmt_metric(f, Key(&name, "count"))?;
self.sum.fmt_metric(f, Key(&name, "sum"))?;
Ok(())
}
fn fmt_metric_labeled<N, L>(&self, f: &mut fmt::Formatter, name: N, labels: L) -> fmt::Result
where
N: Display,
L: Display,
{
let mut total = Counter::default();
for (le, count) in self {
total += *count;
total.fmt_metric_labeled(f, Key(&name, "bucket"), Labels(&labels, Label("le", le)))?;
}
total.fmt_metric_labeled(f, Key(&name, "count"), &labels)?;
self.sum.fmt_metric_labeled(f, Key(&name, "sum"), &labels)?;
Ok(())
}
}
// ===== impl Key =====
impl<A: Display, B: Display> fmt::Display for Key<A, B> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}_{}", self.0, self.1)
}
}
// ===== impl Label =====
impl<K: Display, V: Display> fmt::Display for Label<K, V> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}=\"{}\"", self.0, self.1)
}
}
// ===== impl Labels =====
impl<A: Display, B: Display> fmt::Display for Labels<A, B> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{},{}", self.0, self.1)
}
}
// ===== impl Bucket ===== // ===== impl Bucket =====
impl fmt::Display for Bucket { impl fmt::Display for Bucket {
@ -129,8 +188,8 @@ impl cmp::Ord for Bucket {
mod tests { mod tests {
use super::*; use super::*;
use std::collections::HashMap;
use std::u64; use std::u64;
use std::collections::HashMap;
const NUM_BUCKETS: usize = 47; const NUM_BUCKETS: usize = 47;
static BOUNDS: &'static Bounds = &Bounds(&[ static BOUNDS: &'static Bounds = &Bounds(&[
@ -204,9 +263,9 @@ mod tests {
fn sum_equals_total_of_observations(observations: Vec<u64>) -> bool { fn sum_equals_total_of_observations(observations: Vec<u64>) -> bool {
let mut hist = Histogram::<u64>::new(&BOUNDS); let mut hist = Histogram::<u64>::new(&BOUNDS);
let mut expected_sum = Wrapping(0u64); let mut expected_sum = Counter::default();
for obs in observations { for obs in observations {
expected_sum += Wrapping(obs); expected_sum += obs;
hist.add(obs); hist.add(obs);
} }

View File

@ -27,9 +27,10 @@
//! to worry about missing commas, double commas, or trailing commas at the //! to worry about missing commas, double commas, or trailing commas at the
//! end of the label set (all of which will make Prometheus angry). //! end of the label set (all of which will make Prometheus angry).
use std::default::Default; use std::default::Default;
use std::{fmt, time}; use std::fmt::{self, Display};
use std::hash::Hash; use std::hash::Hash;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time;
use indexmap::IndexMap; use indexmap::IndexMap;
@ -56,6 +57,22 @@ pub use self::labels::DstLabels;
pub use self::record::Record; pub use self::record::Record;
pub use self::serve::Serve; pub use self::serve::Serve;
/// Writes a metric in prometheus-formatted output.
///
/// This trait is implemented by `Counter`, `Gauge`, and `Histogram` to account for the
/// differences in formatting each type of metric. Specifically, `Histogram` formats a
/// counter for each bucket, as well as a count and total sum.
trait FmtMetric {
/// Writes a metric with the given name and no labels.
fn fmt_metric<N: Display>(&self, f: &mut fmt::Formatter, name: N) -> fmt::Result;
/// Writes a metric with the given name and labels.
fn fmt_metric_labeled<N, L>(&self, f: &mut fmt::Formatter, name: N, labels: L) -> fmt::Result
where
N: Display,
L: Display;
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Metrics { struct Metrics {
request_total: Metric<Counter, RequestLabels>, request_total: Metric<Counter, RequestLabels>,
@ -300,11 +317,7 @@ where
)?; )?;
for (labels, value) in &self.values { for (labels, value) in &self.values {
write!(f, "{name}{{{labels}}} {value}\n", value.fmt_metric_labeled(f, self.name, labels)?;
name = self.name,
labels = labels,
value = value,
)?;
} }
Ok(()) Ok(())
@ -324,11 +337,7 @@ where
)?; )?;
for (labels, value) in &self.values { for (labels, value) in &self.values {
write!(f, "{name}{{{labels}}} {value}\n", value.fmt_metric_labeled(f, self.name, labels)?;
name = self.name,
labels = labels,
value = value,
)?;
} }
Ok(()) Ok(())
@ -348,31 +357,7 @@ impl<L, V> fmt::Display for Metric<Histogram<V>, L> where
)?; )?;
for (labels, histogram) in &self.values { for (labels, histogram) in &self.values {
// Since Prometheus expects each bucket's value to be the sum of the number of histogram.fmt_metric_labeled(f, self.name, labels)?;
// values in this bucket and all lower buckets, track the total count here.
let mut total_count = 0u64;
for (le, count) in histogram.into_iter() {
// Add this bucket's count to the total count.
let c: u64 = (*count).into();
total_count += c;
write!(f, "{name}_bucket{{{labels},le=\"{le}\"}} {count}\n",
name = self.name,
labels = labels,
le = le,
// Print the total count *as of this iteration*.
count = total_count,
)?;
}
// Print the total count and histogram sum stats.
write!(f,
"{name}_count{{{labels}}} {count}\n\
{name}_sum{{{labels}}} {sum}\n",
name = self.name,
labels = labels,
count = total_count,
sum = histogram.sum(),
)?;
} }
Ok(()) Ok(())