Skip to content

Commit 2d99f82

Browse files
authored
fix: ensure counter metrics include _total suffix in HELP and TYPE lines (#597)
1 parent fa45c69 commit 2d99f82

File tree

3 files changed

+149
-61
lines changed

3 files changed

+149
-61
lines changed

metrics-exporter-prometheus/src/exporter/builder.rs

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -631,40 +631,101 @@ mod tests {
631631
}
632632

633633
#[test]
634-
fn test_render_with_recommended_naming() {
635-
// test 1 - no unit or description
634+
fn test_render_recommended_naming_no_unit_or_description() {
636635
let recorder = PrometheusBuilder::new().with_recommended_naming(true).build_recorder();
637636

638637
let key = Key::from_name("basic_counter");
639-
let counter1 = recorder.register_counter(&key, &METADATA);
640-
counter1.increment(42);
638+
let counter = recorder.register_counter(&key, &METADATA);
639+
counter.increment(42);
641640

642641
let handle = recorder.handle();
643642
let rendered = handle.render();
644-
let expected_counter = "# TYPE basic_counter counter\nbasic_counter_total 42\n\n";
643+
let expected = "# TYPE basic_counter_total counter\nbasic_counter_total 42\n\n";
645644

646-
assert_eq!(rendered, expected_counter);
645+
assert_eq!(rendered, expected);
646+
}
647647

648-
// test 2 - with unit and description
648+
#[test]
649+
fn test_render_recommended_naming_with_unit_and_description() {
649650
// Note: we need to create a new recorder, as the render order is not deterministic
650651
let recorder = PrometheusBuilder::new().with_recommended_naming(true).build_recorder();
651652

652653
let key_name = KeyName::from_const_str("counter_with_unit");
653654
let key = Key::from_name(key_name.clone());
654655
recorder.describe_counter(key_name, Some(Unit::Bytes), "A counter with a unit".into());
655-
let counter2 = recorder.register_counter(&key, &METADATA);
656-
counter2.increment(42);
656+
let counter = recorder.register_counter(&key, &METADATA);
657+
counter.increment(42);
657658

658659
let handle = recorder.handle();
659660
let rendered = handle.render();
660-
let expected_counter = concat!(
661-
"# HELP counter_with_unit_bytes A counter with a unit\n",
662-
"# TYPE counter_with_unit_bytes counter\n",
661+
let expected: &'static str = concat!(
662+
"# HELP counter_with_unit_bytes_total A counter with a unit\n",
663+
"# TYPE counter_with_unit_bytes_total counter\n",
663664
"counter_with_unit_bytes_total 42\n",
664665
"\n",
665666
);
667+
assert_eq!(rendered, expected);
668+
}
666669

667-
assert_eq!(rendered, expected_counter);
670+
#[test]
671+
fn test_render_recommended_naming_manual_total_suffix_with_unit() {
672+
let recorder = PrometheusBuilder::new().with_recommended_naming(true).build_recorder();
673+
let key_name = KeyName::from_const_str("foo_total");
674+
let key = Key::from_name(key_name.clone());
675+
recorder.describe_counter(key_name, Some(Unit::Bytes), "Some help".into());
676+
let counter = recorder.register_counter(&key, &METADATA);
677+
counter.increment(42);
678+
679+
let handle = recorder.handle();
680+
let rendered = handle.render();
681+
let expected = concat!(
682+
"# HELP foo_bytes_total Some help\n",
683+
"# TYPE foo_bytes_total counter\n",
684+
"foo_bytes_total 42\n",
685+
"\n",
686+
);
687+
assert_eq!(rendered, expected);
688+
}
689+
690+
#[test]
691+
fn test_render_recommended_naming_manual_counter_suffixes() {
692+
let recorder = PrometheusBuilder::new().with_recommended_naming(true).build_recorder();
693+
let key_name = KeyName::from_const_str("foo_bytes_total");
694+
let key = Key::from_name(key_name.clone());
695+
recorder.describe_counter(key_name, Some(Unit::Bytes), "Some help".into());
696+
let counter = recorder.register_counter(&key, &METADATA);
697+
counter.increment(42);
698+
699+
let handle = recorder.handle();
700+
let rendered = handle.render();
701+
let expected = concat!(
702+
"# HELP foo_bytes_total Some help\n",
703+
"# TYPE foo_bytes_total counter\n",
704+
"foo_bytes_total 42\n",
705+
"\n",
706+
);
707+
assert_eq!(rendered, expected);
708+
}
709+
710+
#[test]
711+
fn test_render_recommended_naming_gauge_with_unit_in_name() {
712+
let recorder = PrometheusBuilder::new().with_recommended_naming(true).build_recorder();
713+
714+
let key_name = KeyName::from_const_str("gauge_with_unit_bytes");
715+
let key = Key::from_name(key_name.clone());
716+
recorder.describe_gauge(key_name, Some(Unit::Bytes), "A gauge with a unit".into());
717+
let gauge = recorder.register_gauge(&key, &METADATA);
718+
gauge.set(42.0);
719+
720+
let handle = recorder.handle();
721+
let rendered = handle.render();
722+
let expected = concat!(
723+
"# HELP gauge_with_unit_bytes A gauge with a unit\n",
724+
"# TYPE gauge_with_unit_bytes gauge\n",
725+
"gauge_with_unit_bytes 42\n",
726+
"\n",
727+
);
728+
assert_eq!(rendered, expected);
668729
}
669730

670731
#[test]

metrics-exporter-prometheus/src/formatting.rs

Lines changed: 69 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ pub fn key_to_parts(
2929
/// Writes a help (description) line in the Prometheus [exposition format].
3030
///
3131
/// [exposition format]: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#text-format-details
32-
pub fn write_help_line(buffer: &mut String, name: &str, unit: Option<Unit>, desc: &str) {
32+
pub fn write_help_line(
33+
buffer: &mut String,
34+
name: &str,
35+
unit: Option<Unit>,
36+
suffix: Option<&'static str>,
37+
desc: &str,
38+
) {
3339
buffer.push_str("# HELP ");
34-
buffer.push_str(name);
35-
if let Some(unit) = unit {
36-
buffer.push('_');
37-
buffer.push_str(unit.as_str());
38-
}
40+
add_metric_name(buffer, name, unit, suffix);
3941
buffer.push(' ');
4042
let desc = sanitize_description(desc);
4143
buffer.push_str(&desc);
@@ -45,13 +47,15 @@ pub fn write_help_line(buffer: &mut String, name: &str, unit: Option<Unit>, desc
4547
/// Writes a metric type line in the Prometheus [exposition format].
4648
///
4749
/// [exposition format]: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#text-format-details
48-
pub fn write_type_line(buffer: &mut String, name: &str, unit: Option<Unit>, metric_type: &str) {
50+
pub fn write_type_line(
51+
buffer: &mut String,
52+
name: &str,
53+
unit: Option<Unit>,
54+
suffix: Option<&'static str>,
55+
metric_type: &str,
56+
) {
4957
buffer.push_str("# TYPE ");
50-
buffer.push_str(name);
51-
if let Some(unit) = unit {
52-
buffer.push('_');
53-
buffer.push_str(unit.as_str());
54-
}
58+
add_metric_name(buffer, name, unit, suffix);
5559
buffer.push(' ');
5660
buffer.push_str(metric_type);
5761
buffer.push('\n');
@@ -77,18 +81,7 @@ pub fn write_metric_line<T, T2>(
7781
T: std::fmt::Display,
7882
T2: std::fmt::Display,
7983
{
80-
buffer.push_str(name);
81-
82-
match unit {
83-
Some(Unit::Count) | None => {}
84-
Some(Unit::Percent) => add_unit(buffer, "ratio"),
85-
Some(unit) => add_unit(buffer, unit.as_str()),
86-
}
87-
88-
if let Some(suffix) = suffix {
89-
buffer.push('_');
90-
buffer.push_str(suffix);
91-
}
84+
add_metric_name(buffer, name, unit, suffix);
9285

9386
if !labels.is_empty() || additional_label.is_some() {
9487
buffer.push('{');
@@ -121,24 +114,58 @@ pub fn write_metric_line<T, T2>(
121114
buffer.push('\n');
122115
}
123116

124-
fn add_unit(buffer: &mut String, unit: &str) {
125-
const SUM_SUFFIX: &str = "_sum";
126-
const COUNT_SUFFIX: &str = "_count";
127-
const BUCKET_SUFFIX: &str = "_bucket";
128-
129-
if buffer.ends_with(SUM_SUFFIX) {
130-
let suffix_pos = buffer.len() - SUM_SUFFIX.len();
131-
buffer.insert(suffix_pos, '_');
132-
buffer.insert_str(suffix_pos + 1, unit);
133-
} else if buffer.ends_with(COUNT_SUFFIX) {
134-
let suffix_pos = buffer.len() - COUNT_SUFFIX.len();
135-
buffer.insert(suffix_pos, '_');
136-
buffer.insert_str(suffix_pos + 1, unit);
137-
} else if buffer.ends_with(BUCKET_SUFFIX) {
138-
let suffix_pos = buffer.len() - BUCKET_SUFFIX.len();
139-
buffer.insert(suffix_pos, '_');
140-
buffer.insert_str(suffix_pos + 1, unit);
141-
} else {
117+
fn add_metric_name(
118+
buffer: &mut String,
119+
name: &str,
120+
unit: Option<Unit>,
121+
suffix: Option<&'static str>,
122+
) {
123+
buffer.push_str(name);
124+
if let Some(unit) = unit {
125+
add_unit_if_missing(buffer, unit);
126+
}
127+
if let Some(suffix) = suffix {
128+
add_suffix_if_missing(buffer, suffix);
129+
}
130+
}
131+
132+
/// Adds a suffix to the metric name if it is not already in the name.
133+
fn add_suffix_if_missing(buffer: &mut String, suffix: &str) {
134+
if !buffer.ends_with(suffix) {
135+
buffer.push('_');
136+
buffer.push_str(suffix);
137+
}
138+
}
139+
140+
/// Adds a unit to the metric name if it is not already in the name.
141+
/// If the metric ends with a known suffix, we try to insert the unit before the suffix.
142+
/// Otherwise, we append the unit to the end of the metric name.
143+
fn add_unit_if_missing(buffer: &mut String, unit: Unit) {
144+
const KNOWN_SUFFIXES: [&str; 4] = ["_sum", "_count", "_bucket", "_total"];
145+
146+
let unit = match unit {
147+
Unit::Count => {
148+
// For count, we don't suffix the unit.
149+
return;
150+
}
151+
Unit::Percent => "ratio",
152+
unit => unit.as_str(),
153+
};
154+
155+
let mut handled = false;
156+
for suffix in KNOWN_SUFFIXES {
157+
if buffer.ends_with(suffix) {
158+
let suffix_pos = buffer.len() - suffix.len();
159+
// Check if name before suffix already has the unit
160+
if !&buffer[..suffix_pos].ends_with(unit) {
161+
buffer.insert(suffix_pos, '_');
162+
buffer.insert_str(suffix_pos + 1, unit);
163+
}
164+
handled = true;
165+
break;
166+
}
167+
}
168+
if !handled && !buffer.ends_with(unit) {
142169
buffer.push('_');
143170
buffer.push_str(unit);
144171
}

metrics-exporter-prometheus/src/recorder.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,11 @@ impl Inner {
120120
for (name, mut by_labels) in counters.drain() {
121121
let unit = descriptions.get(name.as_str()).and_then(|(desc, unit)| {
122122
let unit = unit.filter(|_| self.enable_unit_suffix);
123-
write_help_line(&mut output, name.as_str(), unit, desc);
123+
write_help_line(&mut output, name.as_str(), unit, self.counter_suffix, desc);
124124
unit
125125
});
126126

127-
write_type_line(&mut output, name.as_str(), unit, "counter");
127+
write_type_line(&mut output, name.as_str(), unit, self.counter_suffix, "counter");
128128
for (labels, value) in by_labels.drain() {
129129
write_metric_line::<&str, u64>(
130130
&mut output,
@@ -142,11 +142,11 @@ impl Inner {
142142
for (name, mut by_labels) in gauges.drain() {
143143
let unit = descriptions.get(name.as_str()).and_then(|(desc, unit)| {
144144
let unit = unit.filter(|_| self.enable_unit_suffix);
145-
write_help_line(&mut output, name.as_str(), unit, desc);
145+
write_help_line(&mut output, name.as_str(), unit, None, desc);
146146
unit
147147
});
148148

149-
write_type_line(&mut output, name.as_str(), unit, "gauge");
149+
write_type_line(&mut output, name.as_str(), unit, None, "gauge");
150150
for (labels, value) in by_labels.drain() {
151151
write_metric_line::<&str, f64>(
152152
&mut output,
@@ -164,12 +164,12 @@ impl Inner {
164164
for (name, mut by_labels) in distributions.drain() {
165165
let unit = descriptions.get(name.as_str()).and_then(|(desc, unit)| {
166166
let unit = unit.filter(|_| self.enable_unit_suffix);
167-
write_help_line(&mut output, name.as_str(), unit, desc);
167+
write_help_line(&mut output, name.as_str(), unit, None, desc);
168168
unit
169169
});
170170

171171
let distribution_type = self.distribution_builder.get_distribution_type(name.as_str());
172-
write_type_line(&mut output, name.as_str(), unit, distribution_type);
172+
write_type_line(&mut output, name.as_str(), unit, None, distribution_type);
173173
for (labels, distribution) in by_labels.drain(..) {
174174
let (sum, count) = match distribution {
175175
Distribution::Summary(summary, quantiles, sum) => {

0 commit comments

Comments
 (0)