use std::collections::HashMap;
#[cfg(feature = "push-gateway")]
use std::convert::TryFrom;
#[cfg(feature = "http-listener")]
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::num::NonZeroU32;
use std::sync::RwLock;
#[cfg(any(feature = "http-listener", feature = "push-gateway"))]
use std::thread;
use std::time::Duration;
#[cfg(feature = "push-gateway")]
use hyper::Uri;
use indexmap::IndexMap;
#[cfg(feature = "http-listener")]
use ipnet::IpNet;
use quanta::Clock;
use metrics_util::{
parse_quantiles,
registry::{GenerationalStorage, Recency, Registry},
MetricKindMask, Quantile,
};
use crate::common::Matcher;
use crate::distribution::DistributionBuilder;
use crate::recorder::{Inner, PrometheusRecorder};
use crate::registry::AtomicStorage;
use crate::{common::BuildError, PrometheusHandle};
use super::ExporterConfig;
#[cfg(any(feature = "http-listener", feature = "push-gateway"))]
use super::ExporterFuture;
#[derive(Debug)]
pub struct PrometheusBuilder {
#[cfg_attr(not(any(feature = "http-listener", feature = "push-gateway")), allow(dead_code))]
exporter_config: ExporterConfig,
#[cfg(feature = "http-listener")]
allowed_addresses: Option<Vec<IpNet>>,
quantiles: Vec<Quantile>,
bucket_duration: Option<Duration>,
bucket_count: Option<NonZeroU32>,
buckets: Option<Vec<f64>>,
bucket_overrides: Option<HashMap<Matcher, Vec<f64>>>,
idle_timeout: Option<Duration>,
upkeep_timeout: Duration,
recency_mask: MetricKindMask,
global_labels: Option<IndexMap<String, String>>,
}
impl PrometheusBuilder {
pub fn new() -> Self {
let quantiles = parse_quantiles(&[0.0, 0.5, 0.9, 0.95, 0.99, 0.999, 1.0]);
#[cfg(feature = "http-listener")]
let exporter_config = ExporterConfig::HttpListener {
destination: super::ListenDestination::Tcp(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
9000,
)),
};
#[cfg(not(feature = "http-listener"))]
let exporter_config = ExporterConfig::Unconfigured;
let upkeep_timeout = Duration::from_secs(5);
Self {
exporter_config,
#[cfg(feature = "http-listener")]
allowed_addresses: None,
quantiles,
bucket_duration: None,
bucket_count: None,
buckets: None,
bucket_overrides: None,
idle_timeout: None,
upkeep_timeout,
recency_mask: MetricKindMask::NONE,
global_labels: None,
}
}
#[cfg(feature = "http-listener")]
#[cfg_attr(docsrs, doc(cfg(feature = "http-listener")))]
#[must_use]
pub fn with_http_listener(mut self, addr: impl Into<SocketAddr>) -> Self {
self.exporter_config = ExporterConfig::HttpListener {
destination: super::ListenDestination::Tcp(addr.into()),
};
self
}
#[cfg(feature = "push-gateway")]
#[cfg_attr(docsrs, doc(cfg(feature = "push-gateway")))]
pub fn with_push_gateway<T>(
mut self,
endpoint: T,
interval: Duration,
username: Option<String>,
password: Option<String>,
) -> Result<Self, BuildError>
where
T: AsRef<str>,
{
self.exporter_config = ExporterConfig::PushGateway {
endpoint: Uri::try_from(endpoint.as_ref())
.map_err(|e| BuildError::InvalidPushGatewayEndpoint(e.to_string()))?,
interval,
username,
password,
};
Ok(self)
}
#[cfg(feature = "remote-write")]
#[cfg_attr(docsrs, doc(cfg(feature = "remote-write")))]
pub fn with_remote_write<T>(
mut self,
endpoint: T,
interval: Duration,
user_agent: &str,
) -> Result<Self, BuildError>
where
T: AsRef<str>,
{
self.exporter_config = ExporterConfig::RemoteWrite {
endpoint: Uri::try_from(endpoint.as_ref())
.map_err(|e| BuildError::InvalidRemoteWriteEndpoint(e.to_string()))?,
interval,
user_agent: user_agent.to_string(),
};
Ok(self)
}
#[cfg(feature = "uds-listener")]
#[cfg_attr(docsrs, doc(cfg(feature = "uds-listener")))]
#[must_use]
pub fn with_http_uds_listener(mut self, addr: impl Into<std::path::PathBuf>) -> Self {
self.exporter_config = ExporterConfig::HttpListener {
destination: super::ListenDestination::Uds(addr.into()),
};
self
}
#[cfg(feature = "http-listener")]
#[cfg_attr(docsrs, doc(cfg(feature = "http-listener")))]
pub fn add_allowed_address<A>(mut self, address: A) -> Result<Self, BuildError>
where
A: AsRef<str>,
{
use std::str::FromStr;
let address = IpNet::from_str(address.as_ref())
.map_err(|e| BuildError::InvalidAllowlistAddress(e.to_string()))?;
self.allowed_addresses.get_or_insert(vec![]).push(address);
Ok(self)
}
pub fn set_quantiles(mut self, quantiles: &[f64]) -> Result<Self, BuildError> {
if quantiles.is_empty() {
return Err(BuildError::EmptyBucketsOrQuantiles);
}
self.quantiles = parse_quantiles(quantiles);
Ok(self)
}
pub fn set_bucket_duration(mut self, value: Duration) -> Result<Self, BuildError> {
if value.is_zero() {
return Err(BuildError::ZeroBucketDuration);
}
self.bucket_duration = Some(value);
Ok(self)
}
#[must_use]
pub fn set_bucket_count(mut self, count: NonZeroU32) -> Self {
self.bucket_count = Some(count);
self
}
pub fn set_buckets(mut self, values: &[f64]) -> Result<Self, BuildError> {
if values.is_empty() {
return Err(BuildError::EmptyBucketsOrQuantiles);
}
self.buckets = Some(values.to_vec());
Ok(self)
}
pub fn set_buckets_for_metric(
mut self,
matcher: Matcher,
values: &[f64],
) -> Result<Self, BuildError> {
if values.is_empty() {
return Err(BuildError::EmptyBucketsOrQuantiles);
}
let buckets = self.bucket_overrides.get_or_insert_with(HashMap::new);
buckets.insert(matcher.sanitized(), values.to_vec());
Ok(self)
}
#[must_use]
pub fn idle_timeout(mut self, mask: MetricKindMask, timeout: Option<Duration>) -> Self {
self.idle_timeout = timeout;
self.recency_mask = if self.idle_timeout.is_none() { MetricKindMask::NONE } else { mask };
self
}
#[must_use]
pub fn upkeep_timeout(mut self, timeout: Duration) -> Self {
self.upkeep_timeout = timeout;
self
}
#[must_use]
pub fn add_global_label<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Into<String>,
{
let labels = self.global_labels.get_or_insert_with(IndexMap::new);
labels.insert(key.into(), value.into());
self
}
#[cfg(any(feature = "http-listener", feature = "push-gateway"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "http-listener", feature = "push-gateway"))))]
pub fn install(self) -> Result<(), BuildError> {
use tokio::runtime;
let recorder = if let Ok(handle) = runtime::Handle::try_current() {
let (recorder, exporter) = {
let _g = handle.enter();
self.build()?
};
handle.spawn(exporter);
recorder
} else {
let thread_name =
format!("metrics-exporter-prometheus-{}", self.exporter_config.as_type_str());
let runtime = runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| BuildError::FailedToCreateRuntime(e.to_string()))?;
let (recorder, exporter) = {
let _g = runtime.enter();
self.build()?
};
thread::Builder::new()
.name(thread_name)
.spawn(move || runtime.block_on(exporter))
.map_err(|e| BuildError::FailedToCreateRuntime(e.to_string()))?;
recorder
};
metrics::set_global_recorder(recorder)?;
Ok(())
}
pub fn install_recorder(self) -> Result<PrometheusHandle, BuildError> {
let recorder = self.build_recorder();
let handle = recorder.handle();
metrics::set_global_recorder(recorder)?;
Ok(handle)
}
#[warn(clippy::too_many_lines)]
#[cfg(any(feature = "http-listener", feature = "push-gateway"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "http-listener", feature = "push-gateway"))))]
#[cfg_attr(not(feature = "http-listener"), allow(unused_mut))]
pub fn build(mut self) -> Result<(PrometheusRecorder, ExporterFuture), BuildError> {
#[cfg(feature = "http-listener")]
let allowed_addresses = self.allowed_addresses.take();
let exporter_config = self.exporter_config.clone();
let upkeep_timeout = self.upkeep_timeout;
let recorder = self.build_recorder();
let handle = recorder.handle();
let recorder_handle = handle.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(upkeep_timeout).await;
recorder_handle.run_upkeep();
}
});
Ok((
recorder,
match exporter_config {
ExporterConfig::Unconfigured => Err(BuildError::MissingExporterConfiguration)?,
#[cfg(feature = "http-listener")]
ExporterConfig::HttpListener { destination } => match destination {
super::ListenDestination::Tcp(listen_address) => {
super::http_listener::new_http_listener(
handle,
listen_address,
allowed_addresses,
)?
}
#[cfg(feature = "uds-listener")]
super::ListenDestination::Uds(listen_path) => {
super::http_listener::new_http_uds_listener(handle, listen_path)?
}
},
#[cfg(feature = "push-gateway")]
ExporterConfig::PushGateway { endpoint, interval, username, password } => {
super::push_gateway::new_push_gateway(
endpoint, interval, username, password, handle,
)
}
#[cfg(feature = "remote-write")]
ExporterConfig::RemoteWrite { endpoint, interval, user_agent } => {
super::remote_write::new_remote_write(endpoint, interval, handle, &user_agent)
}
},
))
}
pub fn build_recorder(self) -> PrometheusRecorder {
self.build_with_clock(Clock::new())
}
pub(crate) fn build_with_clock(self, clock: Clock) -> PrometheusRecorder {
let inner = Inner {
registry: Registry::new(GenerationalStorage::new(AtomicStorage)),
recency: Recency::new(clock, self.recency_mask, self.idle_timeout),
distributions: RwLock::new(HashMap::new()),
distribution_builder: DistributionBuilder::new(
self.quantiles,
self.bucket_duration,
self.buckets,
self.bucket_count,
self.bucket_overrides,
),
descriptions: RwLock::new(HashMap::new()),
global_labels: self.global_labels.unwrap_or_default(),
};
PrometheusRecorder::from(inner)
}
}
impl Default for PrometheusBuilder {
fn default() -> Self {
PrometheusBuilder::new()
}
}
#[cfg(test)]
#[allow(clippy::approx_constant)]
mod tests {
use std::time::Duration;
use quanta::Clock;
use metrics::{Key, KeyName, Label, Recorder};
use metrics_util::MetricKindMask;
use super::{Matcher, PrometheusBuilder};
static METADATA: metrics::Metadata =
metrics::Metadata::new(module_path!(), metrics::Level::INFO, Some(module_path!()));
#[test]
fn test_render() {
let recorder =
PrometheusBuilder::new().set_quantiles(&[0.0, 1.0]).unwrap().build_recorder();
let key = Key::from_name("basic_counter");
let counter1 = recorder.register_counter(&key, &METADATA);
counter1.increment(42);
let handle = recorder.handle();
let rendered = handle.render();
let expected_counter = "# TYPE basic_counter counter\nbasic_counter 42\n\n";
assert_eq!(rendered, expected_counter);
let labels = vec![Label::new("wutang", "forever")];
let key = Key::from_parts("basic_gauge", labels);
let gauge1 = recorder.register_gauge(&key, &METADATA);
gauge1.set(-3.14);
let rendered = handle.render();
let expected_gauge = format!(
"{expected_counter}# TYPE basic_gauge gauge\nbasic_gauge{{wutang=\"forever\"}} -3.14\n\n",
);
assert_eq!(rendered, expected_gauge);
let key = Key::from_name("basic_histogram");
let histogram1 = recorder.register_histogram(&key, &METADATA);
histogram1.record(12.0);
let rendered = handle.render();
let histogram_data = concat!(
"# TYPE basic_histogram summary\n",
"basic_histogram{quantile=\"0\"} 12\n",
"basic_histogram{quantile=\"1\"} 12\n",
"basic_histogram_sum 12\n",
"basic_histogram_count 1\n",
"\n"
);
let expected_histogram = format!("{expected_gauge}{histogram_data}");
assert_eq!(rendered, expected_histogram);
}
#[test]
fn test_buckets() {
const DEFAULT_VALUES: [f64; 3] = [10.0, 100.0, 1000.0];
const PREFIX_VALUES: [f64; 3] = [15.0, 105.0, 1005.0];
const SUFFIX_VALUES: [f64; 3] = [20.0, 110.0, 1010.0];
const FULL_VALUES: [f64; 3] = [25.0, 115.0, 1015.0];
let recorder = PrometheusBuilder::new()
.set_buckets_for_metric(
Matcher::Full("metrics.testing foo".to_owned()),
&FULL_VALUES[..],
)
.expect("bounds should not be empty")
.set_buckets_for_metric(
Matcher::Prefix("metrics.testing".to_owned()),
&PREFIX_VALUES[..],
)
.expect("bounds should not be empty")
.set_buckets_for_metric(Matcher::Suffix("foo".to_owned()), &SUFFIX_VALUES[..])
.expect("bounds should not be empty")
.set_buckets(&DEFAULT_VALUES[..])
.expect("bounds should not be empty")
.build_recorder();
let full_key = Key::from_name("metrics.testing_foo");
let full_key_histo = recorder.register_histogram(&full_key, &METADATA);
full_key_histo.record(FULL_VALUES[0]);
let prefix_key = Key::from_name("metrics.testing_bar");
let prefix_key_histo = recorder.register_histogram(&prefix_key, &METADATA);
prefix_key_histo.record(PREFIX_VALUES[1]);
let suffix_key = Key::from_name("metrics_testin_foo");
let suffix_key_histo = recorder.register_histogram(&suffix_key, &METADATA);
suffix_key_histo.record(SUFFIX_VALUES[2]);
let default_key = Key::from_name("metrics.wee");
let default_key_histo = recorder.register_histogram(&default_key, &METADATA);
default_key_histo.record(DEFAULT_VALUES[2] + 1.0);
let full_data = concat!(
"# TYPE metrics_testing_foo histogram\n",
"metrics_testing_foo_bucket{le=\"25\"} 1\n",
"metrics_testing_foo_bucket{le=\"115\"} 1\n",
"metrics_testing_foo_bucket{le=\"1015\"} 1\n",
"metrics_testing_foo_bucket{le=\"+Inf\"} 1\n",
"metrics_testing_foo_sum 25\n",
"metrics_testing_foo_count 1\n",
);
let prefix_data = concat!(
"# TYPE metrics_testing_bar histogram\n",
"metrics_testing_bar_bucket{le=\"15\"} 0\n",
"metrics_testing_bar_bucket{le=\"105\"} 1\n",
"metrics_testing_bar_bucket{le=\"1005\"} 1\n",
"metrics_testing_bar_bucket{le=\"+Inf\"} 1\n",
"metrics_testing_bar_sum 105\n",
"metrics_testing_bar_count 1\n",
);
let suffix_data = concat!(
"# TYPE metrics_testin_foo histogram\n",
"metrics_testin_foo_bucket{le=\"20\"} 0\n",
"metrics_testin_foo_bucket{le=\"110\"} 0\n",
"metrics_testin_foo_bucket{le=\"1010\"} 1\n",
"metrics_testin_foo_bucket{le=\"+Inf\"} 1\n",
"metrics_testin_foo_sum 1010\n",
"metrics_testin_foo_count 1\n",
);
let default_data = concat!(
"# TYPE metrics_wee histogram\n",
"metrics_wee_bucket{le=\"10\"} 0\n",
"metrics_wee_bucket{le=\"100\"} 0\n",
"metrics_wee_bucket{le=\"1000\"} 0\n",
"metrics_wee_bucket{le=\"+Inf\"} 1\n",
"metrics_wee_sum 1001\n",
"metrics_wee_count 1\n",
);
let handle = recorder.handle();
let rendered = handle.render();
assert!(rendered.contains(full_data));
assert!(rendered.contains(prefix_data));
assert!(rendered.contains(suffix_data));
assert!(rendered.contains(default_data));
}
#[test]
fn test_idle_timeout_all() {
let (clock, mock) = Clock::mock();
let recorder = PrometheusBuilder::new()
.idle_timeout(MetricKindMask::ALL, Some(Duration::from_secs(10)))
.set_quantiles(&[0.0, 1.0])
.unwrap()
.build_with_clock(clock);
let key = Key::from_name("basic_counter");
let counter1 = recorder.register_counter(&key, &METADATA);
counter1.increment(42);
let key = Key::from_name("basic_gauge");
let gauge1 = recorder.register_gauge(&key, &METADATA);
gauge1.set(-3.14);
let key = Key::from_name("basic_histogram");
let histo1 = recorder.register_histogram(&key, &METADATA);
histo1.record(1.0);
let handle = recorder.handle();
let rendered = handle.render();
let expected = concat!(
"# TYPE basic_counter counter\n",
"basic_counter 42\n\n",
"# TYPE basic_gauge gauge\n",
"basic_gauge -3.14\n\n",
"# TYPE basic_histogram summary\n",
"basic_histogram{quantile=\"0\"} 1\n",
"basic_histogram{quantile=\"1\"} 1\n",
"basic_histogram_sum 1\n",
"basic_histogram_count 1\n\n",
);
assert_eq!(rendered, expected);
mock.increment(Duration::from_secs(9));
let rendered = handle.render();
assert_eq!(rendered, expected);
mock.increment(Duration::from_secs(2));
let rendered = handle.render();
assert_eq!(rendered, "");
}
#[test]
fn test_idle_timeout_partial() {
let (clock, mock) = Clock::mock();
let recorder = PrometheusBuilder::new()
.idle_timeout(
MetricKindMask::COUNTER | MetricKindMask::HISTOGRAM,
Some(Duration::from_secs(10)),
)
.set_quantiles(&[0.0, 1.0])
.unwrap()
.build_with_clock(clock);
let key = Key::from_name("basic_counter");
let counter1 = recorder.register_counter(&key, &METADATA);
counter1.increment(42);
let key = Key::from_name("basic_gauge");
let gauge1 = recorder.register_gauge(&key, &METADATA);
gauge1.set(-3.14);
let key = Key::from_name("basic_histogram");
let histo1 = recorder.register_histogram(&key, &METADATA);
histo1.record(1.0);
let handle = recorder.handle();
let rendered = handle.render();
let expected = concat!(
"# TYPE basic_counter counter\n",
"basic_counter 42\n\n",
"# TYPE basic_gauge gauge\n",
"basic_gauge -3.14\n\n",
"# TYPE basic_histogram summary\n",
"basic_histogram{quantile=\"0\"} 1\n",
"basic_histogram{quantile=\"1\"} 1\n",
"basic_histogram_sum 1\n",
"basic_histogram_count 1\n\n",
);
assert_eq!(rendered, expected);
mock.increment(Duration::from_secs(9));
let rendered = handle.render();
assert_eq!(rendered, expected);
mock.increment(Duration::from_secs(2));
let rendered = handle.render();
let expected = "# TYPE basic_gauge gauge\nbasic_gauge -3.14\n\n";
assert_eq!(rendered, expected);
}
#[test]
fn test_idle_timeout_staggered_distributions() {
let (clock, mock) = Clock::mock();
let recorder = PrometheusBuilder::new()
.idle_timeout(MetricKindMask::ALL, Some(Duration::from_secs(10)))
.set_quantiles(&[0.0, 1.0])
.unwrap()
.build_with_clock(clock);
let key = Key::from_name("basic_counter");
let counter1 = recorder.register_counter(&key, &METADATA);
counter1.increment(42);
let key = Key::from_name("basic_gauge");
let gauge1 = recorder.register_gauge(&key, &METADATA);
gauge1.set(-3.14);
let key = Key::from_name("basic_histogram");
let histo1 = recorder.register_histogram(&key, &METADATA);
histo1.record(1.0);
let handle = recorder.handle();
let rendered = handle.render();
let expected = concat!(
"# TYPE basic_counter counter\n",
"basic_counter 42\n\n",
"# TYPE basic_gauge gauge\n",
"basic_gauge -3.14\n\n",
"# TYPE basic_histogram summary\n",
"basic_histogram{quantile=\"0\"} 1\n",
"basic_histogram{quantile=\"1\"} 1\n",
"basic_histogram_sum 1\n",
"basic_histogram_count 1\n\n",
);
assert_eq!(rendered, expected);
mock.increment(Duration::from_secs(9));
let rendered = handle.render();
assert_eq!(rendered, expected);
let key = Key::from_parts("basic_histogram", vec![Label::new("type", "special")]);
let histo2 = recorder.register_histogram(&key, &METADATA);
histo2.record(2.0);
let expected_second = concat!(
"# TYPE basic_counter counter\n",
"basic_counter 42\n\n",
"# TYPE basic_gauge gauge\n",
"basic_gauge -3.14\n\n",
"# TYPE basic_histogram summary\n",
"basic_histogram{quantile=\"0\"} 1\n",
"basic_histogram{quantile=\"1\"} 1\n",
"basic_histogram_sum 1\n",
"basic_histogram_count 1\n",
"basic_histogram{type=\"special\",quantile=\"0\"} 2\n",
"basic_histogram{type=\"special\",quantile=\"1\"} 2\n",
"basic_histogram_sum{type=\"special\"} 2\n",
"basic_histogram_count{type=\"special\"} 1\n\n",
);
let rendered = handle.render();
assert_eq!(rendered, expected_second);
let expected_after = concat!(
"# TYPE basic_histogram summary\n",
"basic_histogram{type=\"special\",quantile=\"0\"} 2\n",
"basic_histogram{type=\"special\",quantile=\"1\"} 2\n",
"basic_histogram_sum{type=\"special\"} 2\n",
"basic_histogram_count{type=\"special\"} 1\n\n",
);
mock.increment(Duration::from_secs(2));
let rendered = handle.render();
assert_eq!(rendered, expected_after);
}
#[test]
fn test_idle_timeout_doesnt_remove_recents() {
let (clock, mock) = Clock::mock();
let recorder = PrometheusBuilder::new()
.idle_timeout(MetricKindMask::ALL, Some(Duration::from_secs(10)))
.build_with_clock(clock);
let key = Key::from_name("basic_counter");
let counter1 = recorder.register_counter(&key, &METADATA);
counter1.increment(42);
let key = Key::from_name("basic_gauge");
let gauge1 = recorder.register_gauge(&key, &METADATA);
gauge1.set(-3.14);
let handle = recorder.handle();
let rendered = handle.render();
let expected = concat!(
"# TYPE basic_counter counter\n",
"basic_counter 42\n\n",
"# TYPE basic_gauge gauge\n",
"basic_gauge -3.14\n\n",
);
assert_eq!(rendered, expected);
mock.increment(Duration::from_secs(9));
let rendered = handle.render();
assert_eq!(rendered, expected);
let expected_second = concat!(
"# TYPE basic_counter counter\n",
"basic_counter 42\n\n",
"# TYPE basic_gauge gauge\n",
"basic_gauge -3.14\n\n",
);
let rendered = handle.render();
assert_eq!(rendered, expected_second);
counter1.increment(1);
let expected_after = concat!("# TYPE basic_counter counter\n", "basic_counter 43\n\n",);
mock.increment(Duration::from_secs(2));
let rendered = handle.render();
assert_eq!(rendered, expected_after);
}
#[test]
fn test_idle_timeout_catches_delayed_idle() {
let (clock, mock) = Clock::mock();
let recorder = PrometheusBuilder::new()
.idle_timeout(MetricKindMask::ALL, Some(Duration::from_secs(10)))
.build_with_clock(clock);
let key = Key::from_name("basic_counter");
let counter1 = recorder.register_counter(&key, &METADATA);
counter1.increment(42);
let handle = recorder.handle();
let rendered = handle.render();
let expected = concat!("# TYPE basic_counter counter\n", "basic_counter 42\n\n",);
assert_eq!(rendered, expected);
mock.increment(Duration::from_secs(9));
let rendered = handle.render();
assert_eq!(rendered, expected);
counter1.increment(1);
let expected_after = concat!("# TYPE basic_counter counter\n", "basic_counter 43\n\n",);
mock.increment(Duration::from_secs(2));
let rendered = handle.render();
assert_eq!(rendered, expected_after);
mock.increment(Duration::from_secs(11));
let rendered = handle.render();
assert_eq!(rendered, "");
}
#[test]
pub fn test_global_labels() {
let recorder = PrometheusBuilder::new()
.add_global_label("foo", "foo")
.add_global_label("foo", "bar")
.build_recorder();
let key = Key::from_name("basic_counter");
let counter1 = recorder.register_counter(&key, &METADATA);
counter1.increment(42);
let handle = recorder.handle();
let rendered = handle.render();
let expected_counter = "# TYPE basic_counter counter\nbasic_counter{foo=\"bar\"} 42\n\n";
assert_eq!(rendered, expected_counter);
}
#[test]
pub fn test_global_labels_overrides() {
let recorder = PrometheusBuilder::new().add_global_label("foo", "foo").build_recorder();
let key =
Key::from_name("overridden").with_extra_labels(vec![Label::new("foo", "overridden")]);
let counter1 = recorder.register_counter(&key, &METADATA);
counter1.increment(1);
let handle = recorder.handle();
let rendered = handle.render();
let expected_counter = "# TYPE overridden counter\noverridden{foo=\"overridden\"} 1\n\n";
assert_eq!(rendered, expected_counter);
}
#[test]
pub fn test_sanitized_render() {
let recorder = PrometheusBuilder::new().add_global_label("foo:", "foo").build_recorder();
let key_name = KeyName::from("yee_haw:lets go");
let key = Key::from_name(key_name.clone())
.with_extra_labels(vec![Label::new("øhno", "\"yeet\nies\\\"")]);
recorder.describe_counter(key_name, None, "\"Simplë stuff.\nRëally.\"".into());
let counter1 = recorder.register_counter(&key, &METADATA);
counter1.increment(1);
let handle = recorder.handle();
let rendered = handle.render();
let expected_counter = "# HELP yee_haw:lets_go \"Simplë stuff.\\nRëally.\"\n# TYPE yee_haw:lets_go counter\nyee_haw:lets_go{foo_=\"foo\",_hno=\"\\\"yeet\\nies\\\"\"} 1\n\n";
assert_eq!(rendered, expected_counter);
}
}