use crate::{
Counter, Distribution, DynamicCounter, DynamicDistribution, DynamicGauge, DynamicGaugeI64,
DynamicHistogram, Gauge, GaugeF64, Histogram, LabelEnum, LabeledCounter, LabeledGauge,
LabeledHistogram,
};
use std::fmt::Write as _;
pub trait PrometheusExport {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str);
}
fn push_display(output: &mut String, value: impl std::fmt::Display) {
let _ = write!(output, "{}", value);
}
fn write_dynamic_labels(output: &mut String, labels: &[(String, String)]) {
for (idx, (k, v)) in labels.iter().enumerate() {
if idx > 0 {
output.push(',');
}
output.push_str(k);
output.push_str("=\"");
output.push_str(v);
output.push('"');
}
}
impl PrometheusExport for Counter {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
output.push_str("# HELP ");
output.push_str(name);
output.push(' ');
output.push_str(help);
output.push_str("\n# TYPE ");
output.push_str(name);
output.push_str(" counter\n");
output.push_str(name);
output.push(' ');
push_display(output, self.sum());
output.push('\n');
}
}
impl PrometheusExport for Gauge {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
output.push_str("# HELP ");
output.push_str(name);
output.push(' ');
output.push_str(help);
output.push_str("\n# TYPE ");
output.push_str(name);
output.push_str(" gauge\n");
output.push_str(name);
output.push(' ');
push_display(output, self.get());
output.push('\n');
}
}
impl PrometheusExport for GaugeF64 {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
output.push_str("# HELP ");
output.push_str(name);
output.push(' ');
output.push_str(help);
output.push_str("\n# TYPE ");
output.push_str(name);
output.push_str(" gauge\n");
output.push_str(name);
output.push(' ');
push_display(output, self.get());
output.push('\n');
}
}
impl PrometheusExport for Histogram {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
output.push_str("# HELP ");
output.push_str(name);
output.push(' ');
output.push_str(help);
output.push_str("\n# TYPE ");
output.push_str(name);
output.push_str(" histogram\n");
for (bound, count) in self.buckets_cumulative() {
output.push_str(name);
output.push_str("_bucket{le=\"");
if bound == u64::MAX {
output.push_str("+Inf");
} else {
push_display(output, bound);
}
output.push_str("\"} ");
push_display(output, count);
output.push('\n');
}
output.push_str(name);
output.push_str("_sum ");
push_display(output, self.sum());
output.push('\n');
output.push_str(name);
output.push_str("_count ");
push_display(output, self.count());
output.push('\n');
}
}
impl PrometheusExport for Distribution {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
output.push_str("# HELP ");
output.push_str(name);
output.push(' ');
output.push_str(help);
output.push_str("\n# TYPE ");
output.push_str(name);
output.push_str(" summary\n");
output.push_str(name);
output.push_str("_sum ");
push_display(output, self.sum());
output.push('\n');
output.push_str(name);
output.push_str("_count ");
push_display(output, self.count());
output.push('\n');
}
}
impl<L: LabelEnum> PrometheusExport for LabeledCounter<L> {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
output.push_str("# HELP ");
output.push_str(name);
output.push(' ');
output.push_str(help);
output.push_str("\n# TYPE ");
output.push_str(name);
output.push_str(" counter\n");
for (label, count) in self.iter() {
output.push_str(name);
output.push('{');
output.push_str(L::LABEL_NAME);
output.push_str("=\"");
output.push_str(label.variant_name());
output.push_str("\"} ");
push_display(output, count);
output.push('\n');
}
}
}
impl<L: LabelEnum> PrometheusExport for LabeledGauge<L> {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
output.push_str("# HELP ");
output.push_str(name);
output.push(' ');
output.push_str(help);
output.push_str("\n# TYPE ");
output.push_str(name);
output.push_str(" gauge\n");
for (label, value) in self.iter() {
output.push_str(name);
output.push('{');
output.push_str(L::LABEL_NAME);
output.push_str("=\"");
output.push_str(label.variant_name());
output.push_str("\"} ");
push_display(output, value);
output.push('\n');
}
}
}
impl<L: LabelEnum> PrometheusExport for LabeledHistogram<L> {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
output.push_str("# HELP ");
output.push_str(name);
output.push(' ');
output.push_str(help);
output.push_str("\n# TYPE ");
output.push_str(name);
output.push_str(" histogram\n");
for (label, buckets, sum, count) in self.iter() {
let variant = label.variant_name();
for (bound, bucket_count) in buckets {
output.push_str(name);
output.push_str("_bucket{");
output.push_str(L::LABEL_NAME);
output.push_str("=\"");
output.push_str(variant);
output.push_str("\",le=\"");
if bound == u64::MAX {
output.push_str("+Inf");
} else {
push_display(output, bound);
}
output.push_str("\"} ");
push_display(output, bucket_count);
output.push('\n');
}
output.push_str(name);
output.push_str("_sum{");
output.push_str(L::LABEL_NAME);
output.push_str("=\"");
output.push_str(variant);
output.push_str("\"} ");
push_display(output, sum);
output.push('\n');
output.push_str(name);
output.push_str("_count{");
output.push_str(L::LABEL_NAME);
output.push_str("=\"");
output.push_str(variant);
output.push_str("\"} ");
push_display(output, count);
output.push('\n');
}
}
}
impl PrometheusExport for DynamicCounter {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
output.push_str("# HELP ");
output.push_str(name);
output.push(' ');
output.push_str(help);
output.push_str("\n# TYPE ");
output.push_str(name);
output.push_str(" counter\n");
self.visit_series(|labels, count| {
output.push_str(name);
output.push('{');
write_dynamic_labels(output, labels);
output.push_str("} ");
push_display(output, count);
output.push('\n');
});
}
}
impl PrometheusExport for DynamicGauge {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
output.push_str("# HELP ");
output.push_str(name);
output.push(' ');
output.push_str(help);
output.push_str("\n# TYPE ");
output.push_str(name);
output.push_str(" gauge\n");
self.visit_series(|labels, value| {
output.push_str(name);
output.push('{');
write_dynamic_labels(output, labels);
output.push_str("} ");
push_display(output, value);
output.push('\n');
});
}
}
impl PrometheusExport for DynamicGaugeI64 {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
output.push_str("# HELP ");
output.push_str(name);
output.push(' ');
output.push_str(help);
output.push_str("\n# TYPE ");
output.push_str(name);
output.push_str(" gauge\n");
self.visit_series(|labels, value| {
output.push_str(name);
output.push('{');
write_dynamic_labels(output, labels);
output.push_str("} ");
push_display(output, value);
output.push('\n');
});
}
}
impl PrometheusExport for DynamicHistogram {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
output.push_str("# HELP ");
output.push_str(name);
output.push(' ');
output.push_str(help);
output.push_str("\n# TYPE ");
output.push_str(name);
output.push_str(" histogram\n");
self.visit_series(|labels, series| {
for (bound, bucket_count) in series.buckets_cumulative_iter() {
output.push_str(name);
output.push_str("_bucket{");
write_dynamic_labels(output, labels);
if !labels.is_empty() {
output.push(',');
}
output.push_str("le=\"");
if bound == u64::MAX {
output.push_str("+Inf");
} else {
push_display(output, bound);
}
output.push_str("\"} ");
push_display(output, bucket_count);
output.push('\n');
}
output.push_str(name);
output.push_str("_sum{");
write_dynamic_labels(output, labels);
output.push_str("} ");
push_display(output, series.sum());
output.push('\n');
output.push_str(name);
output.push_str("_count{");
write_dynamic_labels(output, labels);
output.push_str("} ");
push_display(output, series.count());
output.push('\n');
});
}
}
impl PrometheusExport for DynamicDistribution {
fn export_prometheus(&self, output: &mut String, name: &str, help: &str) {
output.push_str("# HELP ");
output.push_str(name);
output.push(' ');
output.push_str(help);
output.push_str("\n# TYPE ");
output.push_str(name);
output.push_str(" summary\n");
self.visit_series(|labels, count, sum, _snap| {
output.push_str(name);
output.push_str("_sum{");
write_dynamic_labels(output, labels);
output.push_str("} ");
push_display(output, sum);
output.push('\n');
output.push_str(name);
output.push_str("_count{");
write_dynamic_labels(output, labels);
output.push_str("} ");
push_display(output, count);
output.push('\n');
});
}
}
#[cfg(test)]
mod tests {
use super::PrometheusExport;
use crate::{Counter, Distribution, DynamicCounter, DynamicHistogram, Gauge, Histogram};
#[test]
fn test_prometheus_counter() {
let counter = Counter::new(4);
counter.inc();
counter.inc();
let mut output = String::new();
counter.export_prometheus(&mut output, "test_counter", "A test counter");
assert!(output.contains("# HELP test_counter A test counter"));
assert!(output.contains("# TYPE test_counter counter"));
assert!(output.contains("test_counter 2"));
}
#[test]
fn test_prometheus_gauge() {
let gauge = Gauge::new();
gauge.set(42);
let mut output = String::new();
gauge.export_prometheus(&mut output, "test_gauge", "A test gauge");
assert!(output.contains("# HELP test_gauge A test gauge"));
assert!(output.contains("# TYPE test_gauge gauge"));
assert!(output.contains("test_gauge 42"));
}
#[test]
fn test_prometheus_histogram() {
let histogram = Histogram::new(&[10, 100], 4);
histogram.record(5);
histogram.record(50);
histogram.record(500);
let mut output = String::new();
histogram.export_prometheus(&mut output, "test_hist", "A test histogram");
assert!(output.contains("# HELP test_hist A test histogram"));
assert!(output.contains("# TYPE test_hist histogram"));
assert!(output.contains("test_hist_bucket{le=\"10\"} 1"));
assert!(output.contains("test_hist_bucket{le=\"100\"} 2"));
assert!(output.contains("test_hist_bucket{le=\"+Inf\"} 3"));
assert!(output.contains("test_hist_count 3"));
}
#[test]
fn test_prometheus_distribution() {
let dist = Distribution::new(4);
dist.record(100);
dist.record(200);
dist.record(300);
let mut output = String::new();
dist.export_prometheus(&mut output, "latency", "Request latency");
assert!(output.contains("# HELP latency Request latency"));
assert!(output.contains("# TYPE latency summary"));
assert!(output.contains("latency_sum 600"));
assert!(output.contains("latency_count 3"));
}
#[test]
fn test_prometheus_dynamic_counter() {
let counter = DynamicCounter::new(4);
counter.add(&[("endpoint", "ep1"), ("method", "GET")], 3);
let mut output = String::new();
counter.export_prometheus(&mut output, "requests", "Requests by endpoint");
assert!(output.contains("# HELP requests Requests by endpoint"));
assert!(output.contains("# TYPE requests counter"));
assert!(output.contains("requests{endpoint=\"ep1\",method=\"GET\"} 3"));
}
#[test]
fn test_prometheus_dynamic_histogram() {
let h = DynamicHistogram::new(&[100], 4);
h.record(&[("endpoint", "ep1")], 50);
h.record(&[("endpoint", "ep1")], 150);
let mut output = String::new();
h.export_prometheus(&mut output, "latency", "Latency by endpoint");
assert!(output.contains("# TYPE latency histogram"));
assert!(output.contains("latency_bucket{endpoint=\"ep1\",le=\"100\"} 1"));
assert!(output.contains("latency_bucket{endpoint=\"ep1\",le=\"+Inf\"} 2"));
assert!(output.contains("latency_sum{endpoint=\"ep1\"} 200"));
assert!(output.contains("latency_count{endpoint=\"ep1\"} 2"));
}
}