use std::fmt;
use std::io;
use std::iter::once;
#[cfg(test)]
mod tests;
struct FormattedValue(f64);
impl fmt::Display for FormattedValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = self.0;
if value.is_nan() {
write!(f, "NaN")
} else if value == f64::INFINITY {
write!(f, "+Inf")
} else if value == f64::NEG_INFINITY {
write!(f, "-Inf")
} else {
write!(f, "{}", value)
}
}
}
pub struct LabeledMetricsBuilder<'a, W>
where
W: io::Write,
{
encoder: &'a mut MetricsEncoder<W>,
name: &'a str,
}
impl<W: io::Write> LabeledMetricsBuilder<'_, W> {
pub fn value(self, labels: &[(&str, &str)], value: f64) -> io::Result<Self> {
self.encoder
.encode_value_with_labels(self.name, labels, value)?;
Ok(self)
}
}
pub struct LabeledHistogramBuilder<'a, W>
where
W: io::Write,
{
encoder: &'a mut MetricsEncoder<W>,
name: &'a str,
}
impl<W: io::Write> LabeledHistogramBuilder<'_, W> {
pub fn histogram(
self,
labels: &[(&str, &str)],
buckets: impl Iterator<Item = (f64, f64)>,
sum: f64,
) -> io::Result<Self> {
for (label, _) in labels.iter() {
validate_prometheus_name(label);
}
let mut total: f64 = 0.0;
let mut saw_infinity = false;
for (bucket, v) in buckets {
total += v;
if bucket == std::f64::INFINITY {
saw_infinity = true;
writeln!(
self.encoder.writer,
"{}_bucket{{{}}} {} {}",
self.name,
MetricsEncoder::<W>::encode_labels(labels.iter().chain(once(&("le", "+Inf")))),
FormattedValue(total),
self.encoder.now_millis
)?;
} else {
let bucket_str = bucket.to_string();
writeln!(
self.encoder.writer,
"{}_bucket{{{}}} {} {}",
self.name,
MetricsEncoder::<W>::encode_labels(
labels.iter().chain(once(&("le", bucket_str.as_str())))
),
FormattedValue(total),
self.encoder.now_millis
)?;
}
}
if !saw_infinity {
writeln!(
self.encoder.writer,
"{}_bucket{{{}}} {} {}",
self.name,
MetricsEncoder::<W>::encode_labels(labels.iter().chain(once(&("le", "+Inf")))),
total,
self.encoder.now_millis
)?;
}
if labels.is_empty() {
writeln!(
self.encoder.writer,
"{}_sum {} {}",
self.name,
FormattedValue(sum),
self.encoder.now_millis
)?;
writeln!(
self.encoder.writer,
"{}_count {} {}",
self.name,
FormattedValue(total),
self.encoder.now_millis
)?;
} else {
writeln!(
self.encoder.writer,
"{}_sum{{{}}} {} {}",
self.name,
MetricsEncoder::<W>::encode_labels(labels.iter()),
FormattedValue(sum),
self.encoder.now_millis
)?;
writeln!(
self.encoder.writer,
"{}_count{{{}}} {} {}",
self.name,
MetricsEncoder::<W>::encode_labels(labels.iter()),
FormattedValue(total),
self.encoder.now_millis
)?;
}
Ok(self)
}
}
pub struct MetricsEncoder<W: io::Write> {
writer: W,
now_millis: i64,
}
impl<W: io::Write> MetricsEncoder<W> {
pub fn new(writer: W, now_millis: i64) -> Self {
Self { writer, now_millis }
}
pub fn into_inner(self) -> W {
self.writer
}
fn encode_header(&mut self, name: &str, help: &str, typ: &str) -> io::Result<()> {
writeln!(self.writer, "# HELP {} {}", name, help)?;
writeln!(self.writer, "# TYPE {} {}", name, typ)
}
pub fn encode_histogram(
&mut self,
name: &str,
buckets: impl Iterator<Item = (f64, f64)>,
sum: f64,
help: &str,
) -> io::Result<()> {
self.histogram_vec(name, help)?
.histogram(&[], buckets, sum)?;
Ok(())
}
pub fn histogram_vec<'a>(
&'a mut self,
name: &'a str,
help: &'a str,
) -> io::Result<LabeledHistogramBuilder<'a, W>> {
validate_prometheus_name(name);
self.encode_header(name, help, "histogram")?;
Ok(LabeledHistogramBuilder {
encoder: self,
name,
})
}
pub fn encode_single_value(
&mut self,
typ: &str,
name: &str,
value: f64,
help: &str,
) -> io::Result<()> {
validate_prometheus_name(name);
self.encode_header(name, help, typ)?;
writeln!(
self.writer,
"{} {} {}",
name,
FormattedValue(value),
self.now_millis
)
}
pub fn encode_counter(&mut self, name: &str, value: f64, help: &str) -> io::Result<()> {
self.encode_single_value("counter", name, value, help)
}
pub fn encode_gauge(&mut self, name: &str, value: f64, help: &str) -> io::Result<()> {
self.encode_single_value("gauge", name, value, help)
}
pub fn counter_vec<'a>(
&'a mut self,
name: &'a str,
help: &'a str,
) -> io::Result<LabeledMetricsBuilder<'a, W>> {
validate_prometheus_name(name);
self.encode_header(name, help, "counter")?;
Ok(LabeledMetricsBuilder {
encoder: self,
name,
})
}
pub fn gauge_vec<'a>(
&'a mut self,
name: &'a str,
help: &'a str,
) -> io::Result<LabeledMetricsBuilder<'a, W>> {
validate_prometheus_name(name);
self.encode_header(name, help, "gauge")?;
Ok(LabeledMetricsBuilder {
encoder: self,
name,
})
}
fn encode_labels<'a>(labels: impl Iterator<Item = &'a (&'a str, &'a str)>) -> String {
let mut buf = String::new();
for (i, (k, v)) in labels.enumerate() {
validate_prometheus_name(k);
if i > 0 {
buf.push(',')
}
buf.push_str(k);
buf.push('=');
buf.push('"');
for c in v.chars() {
match c {
'\\' => {
buf.push('\\');
buf.push('\\');
}
'\n' => {
buf.push('\\');
buf.push('n');
}
'"' => {
buf.push('\\');
buf.push('"');
}
_ => buf.push(c),
}
}
buf.push('"');
}
buf
}
fn encode_value_with_labels(
&mut self,
name: &str,
label_values: &[(&str, &str)],
value: f64,
) -> io::Result<()> {
writeln!(
self.writer,
"{}{{{}}} {} {}",
name,
Self::encode_labels(label_values.iter()),
FormattedValue(value),
self.now_millis
)
}
}
fn validate_prometheus_name(name: &str) {
if name.is_empty() {
panic!("Empty names are not allowed");
}
let bytes = name.as_bytes();
if (!bytes[0].is_ascii_alphabetic() && bytes[0] != b'_')
|| !bytes[1..]
.iter()
.all(|c| c.is_ascii_alphanumeric() || *c == b'_')
{
panic!(
"Name '{}' does not match pattern [a-zA-Z_][a-zA-Z0-9_]",
name
);
}
}