influxdb/metric_exporters/src/lib.rs

292 lines
9.9 KiB
Rust

#![deny(rustdoc::broken_intra_doc_links, rustdoc::bare_urls, rust_2018_idioms)]
#![warn(
missing_debug_implementations,
clippy::explicit_iter_loop,
clippy::use_self,
clippy::clone_on_ref_ptr
)]
use metric::{Attributes, MetricKind, Observation};
use std::io::Write;
use observability_deps::tracing::error;
use prometheus::proto::{Bucket, Histogram};
use prometheus::{
proto::{Counter, Gauge, LabelPair, Metric, MetricFamily, MetricType},
Encoder, TextEncoder,
};
/// A `metric::Reporter` that writes data in the prometheus text exposition format
///
/// In order to comply with the prometheus naming best-practices, certain metrics may have
/// a unit and/or "_total" suffix applied - <https://prometheus.io/docs/practices/naming/>
///
/// Note: this is done after the metric sort order is established - this means the output
/// order is guaranteed to be stable, but not necessarily sorted.
///
/// For example a counter named "metric" and a gauge named "metric_a" will be exported as
/// "metric_total" and "metric_a" in that order
///
#[derive(Debug)]
pub struct PrometheusTextEncoder<'a, W: Write> {
/// metric family together with a flag indicating that it was used
metric: Option<(MetricFamily, bool)>,
encoder: TextEncoder,
writer: &'a mut W,
}
impl<'a, W: Write> PrometheusTextEncoder<'a, W> {
pub fn new(writer: &'a mut W) -> Self {
Self {
metric: None,
encoder: TextEncoder::new(),
writer,
}
}
}
impl<'a, W: Write> metric::Reporter for PrometheusTextEncoder<'a, W> {
fn start_metric(
&mut self,
metric_name: &'static str,
description: &'static str,
kind: MetricKind,
) {
assert!(self.metric.is_none(), "metric already in progress");
let (name, metric_type) = match kind {
MetricKind::U64Counter => (format!("{}_total", metric_name), MetricType::COUNTER),
MetricKind::U64Gauge => (metric_name.to_string(), MetricType::GAUGE),
MetricKind::U64Histogram => (metric_name.to_string(), MetricType::HISTOGRAM),
MetricKind::DurationCounter => (
format!("{}_seconds_total", metric_name),
MetricType::COUNTER,
),
MetricKind::DurationGauge => (format!("{}_seconds", metric_name), MetricType::GAUGE),
MetricKind::DurationHistogram => {
(format!("{}_seconds", metric_name), MetricType::HISTOGRAM)
}
};
let mut metric = MetricFamily::default();
metric.set_name(name);
metric.set_help(description.to_string());
metric.set_field_type(metric_type);
self.metric = Some((metric, false))
}
fn report_observation(&mut self, attributes: &Attributes, observation: Observation) {
let (metrics, used) = self.metric.as_mut().expect("no metric in progress");
let metrics = metrics.mut_metric();
let mut metric = Metric::default();
metric.set_label(
attributes
.iter()
.map(|(name, value)| {
let mut pair = LabelPair::default();
pair.set_name(name.to_string());
pair.set_value(value.to_string());
pair
})
.collect(),
);
match observation {
Observation::U64Counter(v) => {
let mut counter = Counter::default();
counter.set_value(v as f64);
metric.set_counter(counter)
}
Observation::U64Gauge(v) => {
let mut gauge = Gauge::default();
gauge.set_value(v as f64);
metric.set_gauge(gauge)
}
Observation::DurationCounter(v) => {
let mut counter = Counter::default();
counter.set_value(v.as_secs_f64());
metric.set_counter(counter)
}
Observation::DurationGauge(v) => {
let mut gauge = Gauge::default();
gauge.set_value(v.as_secs_f64());
metric.set_gauge(gauge)
}
Observation::U64Histogram(v) => {
let mut histogram = Histogram::default();
let mut cumulative_count = 0;
histogram.set_bucket(
v.buckets
.into_iter()
.map(|observation| {
cumulative_count += observation.count;
let mut bucket = Bucket::default();
let le = match observation.le {
u64::MAX => f64::INFINITY,
v => v as f64,
};
bucket.set_upper_bound(le);
bucket.set_cumulative_count(cumulative_count);
bucket
})
.collect(),
);
histogram.set_sample_count(cumulative_count);
histogram.set_sample_sum(v.total as f64);
metric.set_histogram(histogram)
}
Observation::DurationHistogram(v) => {
let mut histogram = Histogram::default();
let mut cumulative_count = 0;
histogram.set_bucket(
v.buckets
.into_iter()
.map(|observation| {
cumulative_count += observation.count;
let mut bucket = Bucket::default();
let le = match observation.le {
metric::DURATION_MAX => f64::INFINITY,
v => v.as_secs_f64(),
};
bucket.set_upper_bound(le);
bucket.set_cumulative_count(cumulative_count);
bucket
})
.collect(),
);
histogram.set_sample_count(cumulative_count);
histogram.set_sample_sum(v.total.as_secs_f64());
metric.set_histogram(histogram)
}
};
metrics.push(metric);
*used = true;
}
fn finish_metric(&mut self) {
if let Some((family, used)) = self.metric.take() {
if !used {
// just don't report the metric
return;
}
match self.encoder.encode(&[family], self.writer) {
Ok(_) => {}
Err(e) => error!(%e, "error encoding metric family"),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use metric::{
DurationCounter, DurationGauge, DurationHistogram, Metric, Registry, U64Counter,
U64Histogram, U64HistogramOptions,
};
use std::time::Duration;
use test_helpers::assert_not_contains;
#[test]
fn test_encode() {
// tap tracing to check for errors
let tracing_capture = test_helpers::tracing::TracingCapture::new();
let registry = Registry::new();
let counter: Metric<U64Counter> = registry.register_metric("foo", "a counter metric");
let counter_value = counter.recorder(&[("tag1", "value"), ("tag2", "value")]);
counter_value.inc(5);
let counter_value2 = counter.recorder(&[("tag1", "value"), ("tag2", "value2")]);
counter_value2.inc(7);
let histogram: Metric<U64Histogram> =
registry.register_metric_with_options("bar", "a histogram metric", || {
U64HistogramOptions::new([5, 10, 50])
});
let histogram_r1 = histogram.recorder(&[("tag1", "value1")]);
let histogram_r2 = histogram.recorder(&[("tag1", "value1")]);
let histogram_r3 = histogram.recorder(&[("tag1", "value2")]);
histogram_r1.record(10);
histogram_r2.record(3);
histogram_r2.record(40);
histogram_r3.record(8);
histogram_r3.record(40);
let duration: Metric<DurationGauge> =
registry.register_metric("duration_gauge", "a duration gauge");
duration
.recorder(&[("tag1", "value1")])
.set(Duration::from_millis(100));
let duration_counter: Metric<DurationCounter> =
registry.register_metric("duration_counter", "a duration counter");
duration_counter
.recorder(&[("tag1", "value1")])
.inc(Duration::from_millis(1200));
// unused metrics must not result in an error
let _unused: Metric<DurationHistogram> = registry.register_metric("unused", "unused");
let mut buffer = Vec::new();
let mut encoder = PrometheusTextEncoder::new(&mut buffer);
registry.report(&mut encoder);
let buffer = String::from_utf8(buffer).unwrap();
let expected = r#"
# HELP bar a histogram metric
# TYPE bar histogram
bar_bucket{tag1="value1",le="5"} 1
bar_bucket{tag1="value1",le="10"} 2
bar_bucket{tag1="value1",le="50"} 3
bar_bucket{tag1="value1",le="+Inf"} 3
bar_sum{tag1="value1"} 53
bar_count{tag1="value1"} 3
bar_bucket{tag1="value2",le="5"} 0
bar_bucket{tag1="value2",le="10"} 1
bar_bucket{tag1="value2",le="50"} 2
bar_bucket{tag1="value2",le="+Inf"} 2
bar_sum{tag1="value2"} 48
bar_count{tag1="value2"} 2
# HELP duration_counter_seconds_total a duration counter
# TYPE duration_counter_seconds_total counter
duration_counter_seconds_total{tag1="value1"} 1.2
# HELP duration_gauge_seconds a duration gauge
# TYPE duration_gauge_seconds gauge
duration_gauge_seconds{tag1="value1"} 0.1
# HELP foo_total a counter metric
# TYPE foo_total counter
foo_total{tag1="value",tag2="value"} 5
foo_total{tag1="value",tag2="value2"} 7
"#
.trim_start();
assert_eq!(&buffer, expected, "{}", buffer);
// no errors
assert_not_contains!(tracing_capture.to_string(), "error");
}
}