From dcd67cde8df61afc93900de0c33dd7ad5f5dbb6d Mon Sep 17 00:00:00 2001 From: Michel Heily Date: Thu, 17 Jul 2025 11:31:40 +0300 Subject: [PATCH 1/4] Prometheus: Add compatibility mode for counter names This commit adds a setting to PrometheusBuilder to add the '_total' suffix to counter names, compatible with [Prometheus naming best practices](https://prometheus.io/docs/practices/naming/). This is useful for migrating from the [prometheus-client] crate, (where the counter suffix is enforced) without breaking existing dashboards. [prometheus-client](https://docs.rs/prometheus-client/latest/prometheus_client/) Signed-off-by: Michel Heily --- .../src/exporter/builder.rs | 31 +++++++++++++++++++ metrics-exporter-prometheus/src/recorder.rs | 3 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/metrics-exporter-prometheus/src/exporter/builder.rs b/metrics-exporter-prometheus/src/exporter/builder.rs index 33353b46..b7229446 100644 --- a/metrics-exporter-prometheus/src/exporter/builder.rs +++ b/metrics-exporter-prometheus/src/exporter/builder.rs @@ -49,6 +49,7 @@ pub struct PrometheusBuilder { recency_mask: MetricKindMask, global_labels: Option>, enable_unit_suffix: bool, + enable_compat_counter_names: bool, } impl PrometheusBuilder { @@ -82,6 +83,7 @@ impl PrometheusBuilder { recency_mask: MetricKindMask::NONE, global_labels: None, enable_unit_suffix: false, + enable_compat_counter_names: false, } } @@ -296,6 +298,18 @@ impl PrometheusBuilder { self } + /// Sets whether counter names are suffixed with `_total` + /// + /// Setting this to true will make the formatted metrics compatible with + /// the [Prometheus Best Practices](https://prometheus.io/docs/practices/naming/). + /// + /// Defaults to false. + #[must_use] + pub fn set_enable_compat_metric_names(mut self, enabled: bool) -> Self { + self.enable_compat_counter_names = enabled; + self + } + /// Sets the bucket for a specific pattern. /// /// The match pattern can be a full match (equality), prefix match, or suffix match. The matchers are applied in @@ -539,6 +553,7 @@ impl PrometheusBuilder { descriptions: RwLock::new(HashMap::new()), global_labels: self.global_labels.unwrap_or_default(), enable_unit_suffix: self.enable_unit_suffix, + enable_compat_counter_names: self.enable_compat_counter_names, }; PrometheusRecorder::from(inner) @@ -610,6 +625,22 @@ mod tests { assert_eq!(rendered, expected_histogram); } + #[test] + fn test_render_compat_counter_names() { + let recorder = + PrometheusBuilder::new().set_enable_compat_metric_names(true).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_total 42\n\n"; + + assert_eq!(rendered, expected_counter); + } + #[test] fn test_buckets() { const DEFAULT_VALUES: [f64; 3] = [10.0, 100.0, 1000.0]; diff --git a/metrics-exporter-prometheus/src/recorder.rs b/metrics-exporter-prometheus/src/recorder.rs index 450f4573..688d9143 100644 --- a/metrics-exporter-prometheus/src/recorder.rs +++ b/metrics-exporter-prometheus/src/recorder.rs @@ -24,6 +24,7 @@ pub(crate) struct Inner { pub descriptions: RwLock)>>, pub global_labels: IndexMap, pub enable_unit_suffix: bool, + pub enable_compat_counter_names: bool, } impl Inner { @@ -127,7 +128,7 @@ impl Inner { write_metric_line::<&str, u64>( &mut output, &name, - None, + Some("total").filter(|_| self.enable_compat_counter_names), &labels, None, value, From 80960de51c081a09570e2e24dfe0eddfec893101 Mon Sep 17 00:00:00 2001 From: Michel Heily Date: Fri, 18 Jul 2025 23:31:24 +0300 Subject: [PATCH 2/4] fix: unit before suffix --- metrics-exporter-prometheus/src/formatting.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/metrics-exporter-prometheus/src/formatting.rs b/metrics-exporter-prometheus/src/formatting.rs index 41c810ff..d397cd9d 100644 --- a/metrics-exporter-prometheus/src/formatting.rs +++ b/metrics-exporter-prometheus/src/formatting.rs @@ -70,10 +70,6 @@ pub fn write_metric_line( T2: std::fmt::Display, { buffer.push_str(name); - if let Some(suffix) = suffix { - buffer.push('_'); - buffer.push_str(suffix); - } match unit { Some(Unit::Count) | None => {} @@ -81,6 +77,11 @@ pub fn write_metric_line( Some(unit) => add_unit(buffer, unit.as_str()), } + if let Some(suffix) = suffix { + buffer.push('_'); + buffer.push_str(suffix); + } + if !labels.is_empty() || additional_label.is_some() { buffer.push('{'); From 0d1c2eef2b8245a57882874e032097968691d65d Mon Sep 17 00:00:00 2001 From: Michel Heily Date: Fri, 18 Jul 2025 23:54:16 +0300 Subject: [PATCH 3/4] fix: Unit needs to be in HELP and TYPE --- metrics-exporter-prometheus/src/formatting.rs | 12 +++++-- metrics-exporter-prometheus/src/recorder.rs | 35 ++++++++++--------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/metrics-exporter-prometheus/src/formatting.rs b/metrics-exporter-prometheus/src/formatting.rs index d397cd9d..8c71bbe9 100644 --- a/metrics-exporter-prometheus/src/formatting.rs +++ b/metrics-exporter-prometheus/src/formatting.rs @@ -29,9 +29,13 @@ pub fn key_to_parts( /// Writes a help (description) line in the Prometheus [exposition format]. /// /// [exposition format]: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#text-format-details -pub fn write_help_line(buffer: &mut String, name: &str, desc: &str) { +pub fn write_help_line(buffer: &mut String, name: &str, unit: Option, desc: &str) { buffer.push_str("# HELP "); buffer.push_str(name); + if let Some(unit) = unit { + buffer.push('_'); + buffer.push_str(unit.as_str()); + } buffer.push(' '); let desc = sanitize_description(desc); buffer.push_str(&desc); @@ -41,9 +45,13 @@ pub fn write_help_line(buffer: &mut String, name: &str, desc: &str) { /// Writes a metric type line in the Prometheus [exposition format]. /// /// [exposition format]: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#text-format-details -pub fn write_type_line(buffer: &mut String, name: &str, metric_type: &str) { +pub fn write_type_line(buffer: &mut String, name: &str, unit: Option, metric_type: &str) { buffer.push_str("# TYPE "); buffer.push_str(name); + if let Some(unit) = unit { + buffer.push('_'); + buffer.push_str(unit.as_str()); + } buffer.push(' '); buffer.push_str(metric_type); buffer.push('\n'); diff --git a/metrics-exporter-prometheus/src/recorder.rs b/metrics-exporter-prometheus/src/recorder.rs index 688d9143..95cb7e0a 100644 --- a/metrics-exporter-prometheus/src/recorder.rs +++ b/metrics-exporter-prometheus/src/recorder.rs @@ -119,11 +119,12 @@ impl Inner { for (name, mut by_labels) in counters.drain() { let unit = descriptions.get(name.as_str()).and_then(|(desc, unit)| { - write_help_line(&mut output, name.as_str(), desc); - *unit + let unit = unit.filter(|_| self.enable_unit_suffix); + write_help_line(&mut output, name.as_str(), unit, desc); + unit }); - write_type_line(&mut output, name.as_str(), "counter"); + write_type_line(&mut output, name.as_str(), unit, "counter"); for (labels, value) in by_labels.drain() { write_metric_line::<&str, u64>( &mut output, @@ -132,7 +133,7 @@ impl Inner { &labels, None, value, - unit.filter(|_| self.enable_unit_suffix), + unit, ); } output.push('\n'); @@ -140,11 +141,12 @@ impl Inner { for (name, mut by_labels) in gauges.drain() { let unit = descriptions.get(name.as_str()).and_then(|(desc, unit)| { - write_help_line(&mut output, name.as_str(), desc); - *unit + let unit = unit.filter(|_| self.enable_unit_suffix); + write_help_line(&mut output, name.as_str(), unit, desc); + unit }); - write_type_line(&mut output, name.as_str(), "gauge"); + write_type_line(&mut output, name.as_str(), unit, "gauge"); for (labels, value) in by_labels.drain() { write_metric_line::<&str, f64>( &mut output, @@ -153,7 +155,7 @@ impl Inner { &labels, None, value, - unit.filter(|_| self.enable_unit_suffix), + unit, ); } output.push('\n'); @@ -161,12 +163,13 @@ impl Inner { for (name, mut by_labels) in distributions.drain() { let unit = descriptions.get(name.as_str()).and_then(|(desc, unit)| { - write_help_line(&mut output, name.as_str(), desc); - *unit + let unit = unit.filter(|_| self.enable_unit_suffix); + write_help_line(&mut output, name.as_str(), unit, desc); + unit }); let distribution_type = self.distribution_builder.get_distribution_type(name.as_str()); - write_type_line(&mut output, name.as_str(), distribution_type); + write_type_line(&mut output, name.as_str(), unit, distribution_type); for (labels, distribution) in by_labels.drain(..) { let (sum, count) = match distribution { Distribution::Summary(summary, quantiles, sum) => { @@ -180,7 +183,7 @@ impl Inner { &labels, Some(("quantile", quantile.value())), value, - unit.filter(|_| self.enable_unit_suffix), + unit, ); } @@ -195,7 +198,7 @@ impl Inner { &labels, Some(("le", le)), count, - unit.filter(|_| self.enable_unit_suffix), + unit, ); } write_metric_line( @@ -205,7 +208,7 @@ impl Inner { &labels, Some(("le", "+Inf")), histogram.count(), - unit.filter(|_| self.enable_unit_suffix), + unit, ); (histogram.sum(), histogram.count()) @@ -219,7 +222,7 @@ impl Inner { &labels, None, sum, - unit.filter(|_| self.enable_unit_suffix), + unit, ); write_metric_line::<&str, u64>( &mut output, @@ -228,7 +231,7 @@ impl Inner { &labels, None, count, - unit.filter(|_| self.enable_unit_suffix), + unit, ); } From 396f6e345faf5ccd0b4d027b7aa18e2f7119ce6b Mon Sep 17 00:00:00 2001 From: Michel Heily Date: Sat, 19 Jul 2025 00:08:44 +0300 Subject: [PATCH 4/4] Refactor to with_recommended_naming --- .../src/exporter/builder.rs | 54 ++++++++++++++----- metrics-exporter-prometheus/src/recorder.rs | 4 +- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/metrics-exporter-prometheus/src/exporter/builder.rs b/metrics-exporter-prometheus/src/exporter/builder.rs index b7229446..bf69100b 100644 --- a/metrics-exporter-prometheus/src/exporter/builder.rs +++ b/metrics-exporter-prometheus/src/exporter/builder.rs @@ -48,8 +48,9 @@ pub struct PrometheusBuilder { upkeep_timeout: Duration, recency_mask: MetricKindMask, global_labels: Option>, + enable_recommended_naming: bool, + /// TODO Remove this field in next version and merge with `enable_recommended_naming` enable_unit_suffix: bool, - enable_compat_counter_names: bool, } impl PrometheusBuilder { @@ -82,8 +83,8 @@ impl PrometheusBuilder { upkeep_timeout, recency_mask: MetricKindMask::NONE, global_labels: None, + enable_recommended_naming: false, enable_unit_suffix: false, - enable_compat_counter_names: false, } } @@ -293,20 +294,24 @@ impl PrometheusBuilder { /// /// Defaults to false. #[must_use] + #[deprecated( + since = "0.18.0", + note = "users should prefer `with_recommended_naming` which automatically enables unit suffixes" + )] pub fn set_enable_unit_suffix(mut self, enabled: bool) -> Self { self.enable_unit_suffix = enabled; self } - /// Sets whether counter names are suffixed with `_total` + /// Enables Prometheus naming best practices for metrics. /// - /// Setting this to true will make the formatted metrics compatible with - /// the [Prometheus Best Practices](https://prometheus.io/docs/practices/naming/). + /// When set to `true`, counter names are suffixed with `_total` and unit suffixes are appended to metric names, + /// following [Prometheus Best Practices](https://prometheus.io/docs/practices/naming/). /// - /// Defaults to false. + /// Defaults to `false`. #[must_use] - pub fn set_enable_compat_metric_names(mut self, enabled: bool) -> Self { - self.enable_compat_counter_names = enabled; + pub fn with_recommended_naming(mut self, enabled: bool) -> Self { + self.enable_recommended_naming = enabled; self } @@ -552,8 +557,8 @@ impl PrometheusBuilder { ), descriptions: RwLock::new(HashMap::new()), global_labels: self.global_labels.unwrap_or_default(), - enable_unit_suffix: self.enable_unit_suffix, - enable_compat_counter_names: self.enable_compat_counter_names, + enable_unit_suffix: self.enable_recommended_naming || self.enable_unit_suffix, + counter_suffix: self.enable_recommended_naming.then_some("total"), }; PrometheusRecorder::from(inner) @@ -573,7 +578,7 @@ mod tests { use quanta::Clock; - use metrics::{Key, KeyName, Label, Recorder}; + use metrics::{Key, KeyName, Label, Recorder, Unit}; use metrics_util::MetricKindMask; use super::{Matcher, PrometheusBuilder}; @@ -626,9 +631,9 @@ mod tests { } #[test] - fn test_render_compat_counter_names() { - let recorder = - PrometheusBuilder::new().set_enable_compat_metric_names(true).build_recorder(); + fn test_render_with_recommended_naming() { + // test 1 - no unit or description + let recorder = PrometheusBuilder::new().with_recommended_naming(true).build_recorder(); let key = Key::from_name("basic_counter"); let counter1 = recorder.register_counter(&key, &METADATA); @@ -639,6 +644,27 @@ mod tests { let expected_counter = "# TYPE basic_counter counter\nbasic_counter_total 42\n\n"; assert_eq!(rendered, expected_counter); + + // test 2 - with unit and description + // Note: we need to create a new recorder, as the render order is not deterministic + let recorder = PrometheusBuilder::new().with_recommended_naming(true).build_recorder(); + + let key_name = KeyName::from_const_str("counter_with_unit"); + let key = Key::from_name(key_name.clone()); + recorder.describe_counter(key_name, Some(Unit::Bytes), "A counter with a unit".into()); + let counter2 = recorder.register_counter(&key, &METADATA); + counter2.increment(42); + + let handle = recorder.handle(); + let rendered = handle.render(); + let expected_counter = concat!( + "# HELP counter_with_unit_bytes A counter with a unit\n", + "# TYPE counter_with_unit_bytes counter\n", + "counter_with_unit_bytes_total 42\n", + "\n", + ); + + assert_eq!(rendered, expected_counter); } #[test] diff --git a/metrics-exporter-prometheus/src/recorder.rs b/metrics-exporter-prometheus/src/recorder.rs index 95cb7e0a..6f6b9be1 100644 --- a/metrics-exporter-prometheus/src/recorder.rs +++ b/metrics-exporter-prometheus/src/recorder.rs @@ -24,7 +24,7 @@ pub(crate) struct Inner { pub descriptions: RwLock)>>, pub global_labels: IndexMap, pub enable_unit_suffix: bool, - pub enable_compat_counter_names: bool, + pub counter_suffix: Option<&'static str>, } impl Inner { @@ -129,7 +129,7 @@ impl Inner { write_metric_line::<&str, u64>( &mut output, &name, - Some("total").filter(|_| self.enable_compat_counter_names), + self.counter_suffix, &labels, None, value,