From bdb0653780d10ef15fa9130a084aa327a978a514 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 5 May 2026 10:37:01 +0000 Subject: [PATCH 1/2] feat: add normalizeMetricName helper Signed-off-by: Gregor Zeitlinger --- .../model/snapshots/PrometheusNaming.java | 35 ++++++++++++++++++- .../model/snapshots/PrometheusNamingTest.java | 24 +++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java index 3380b3fad..c379cb3f5 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java @@ -46,7 +46,9 @@ public class PrometheusNaming { * format, this will be represented as two values: {@code processing_time_seconds_total} for the * counter value, and the optional {@code processing_time_seconds_created} timestamp. * - *

Use {@link #sanitizeMetricName(String)} to convert arbitrary Strings to valid metric names. + *

Use {@link #sanitizeMetricName(String)} for compatibility-preserving sanitization that + * strips reserved suffixes, or {@link #normalizeMetricName(String)} for permissive normalization + * that keeps the original suffixes intact. */ public static boolean isValidMetricName(String name) { return validateMetricName(name) == null; @@ -221,6 +223,37 @@ public static String sanitizeMetricName(String metricName, Unit unit) { return result; } + /** + * Convert an arbitrary string to a valid metric name without stripping reserved suffixes. + * + *

Any non-empty valid UTF-8 string is accepted and returned unchanged. This is the permissive + * normalization behavior introduced in 1.6.0. Use this method for new integrations that want to + * preserve the original metric name and rely on registration-time collision detection instead of + * suffix stripping. + * + * @throws IllegalArgumentException if the input is empty + */ + public static String normalizeMetricName(String metricName) { + if (metricName.isEmpty()) { + throw new IllegalArgumentException("Cannot convert an empty string to a valid metric name."); + } + return metricName; + } + + /** + * Like {@link #normalizeMetricName(String)}, but also makes sure that the unit is appended as a + * suffix if the unit is not {@code null}. + */ + public static String normalizeMetricName(String metricName, Unit unit) { + String result = normalizeMetricName(metricName); + if (unit != null) { + if (!result.endsWith("_" + unit) && !result.endsWith("." + unit)) { + result += "_" + unit; + } + } + return result; + } + /** * Convert an arbitrary string to a name where {@link #isValidLabelName(String) * isValidLabelName(name)} is true. diff --git a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/PrometheusNamingTest.java b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/PrometheusNamingTest.java index 5efe49fc6..ee6d2d027 100644 --- a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/PrometheusNamingTest.java +++ b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/PrometheusNamingTest.java @@ -2,6 +2,7 @@ import static io.prometheus.metrics.model.snapshots.PrometheusNaming.escapeName; import static io.prometheus.metrics.model.snapshots.PrometheusNaming.isValidLabelName; +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.normalizeMetricName; import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName; import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeLabelName; import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeMetricName; @@ -72,6 +73,29 @@ void testSanitizeMetricNameWithUnit() { assertThat(sanitizeMetricName("total", Unit.RATIO)).isEqualTo("total_" + Unit.RATIO); } + @Test + void testNormalizeMetricName() { + assertThat(normalizeMetricName("my_counter_total")).isEqualTo("my_counter_total"); + assertThat(normalizeMetricName("jvm.info")).isEqualTo("jvm.info"); + assertThat(normalizeMetricName("jvm_info")).isEqualTo("jvm_info"); + assertThat(normalizeMetricName("a.b")).isEqualTo("a.b"); + assertThat(normalizeMetricName("_total")).isEqualTo("_total"); + assertThat(normalizeMetricName(".total")).isEqualTo(".total"); + assertThat(normalizeMetricName("my_events_created")).isEqualTo("my_events_created"); + assertThat(normalizeMetricName("my_histogram_bucket")).isEqualTo("my_histogram_bucket"); + } + + @Test + void testNormalizeMetricNameWithUnit() { + assertThat(prometheusName(normalizeMetricName("def", Unit.RATIO))) + .isEqualTo("def_" + Unit.RATIO); + assertThat(prometheusName(normalizeMetricName("my_counter_total", Unit.RATIO))) + .isEqualTo("my_counter_total_" + Unit.RATIO); + assertThat(normalizeMetricName("jvm.info", Unit.RATIO)).isEqualTo("jvm.info_" + Unit.RATIO); + assertThat(normalizeMetricName("_total", Unit.RATIO)).isEqualTo("_total_" + Unit.RATIO); + assertThat(normalizeMetricName("total", Unit.RATIO)).isEqualTo("total_" + Unit.RATIO); + } + @Test void testSanitizeLabelName() { assertThat(prometheusName(sanitizeLabelName("0abc.def"))).isEqualTo("_abc_def"); From 83b5c3fbd0e8db97ef0d83cf4338ed2bebaab9fc Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 5 May 2026 10:40:09 +0000 Subject: [PATCH 2/2] docs: link sanitizeMetricName to normalizeMetricName Signed-off-by: Gregor Zeitlinger --- .../prometheus/metrics/model/snapshots/PrometheusNaming.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java index c379cb3f5..63cbb7879 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java @@ -180,6 +180,9 @@ public static String prometheusName(String name) { * bridge) which relied on {@code sanitizeMetricName} to strip these suffixes before passing names * to the snapshot builders. * + *

If you want permissive normalization that keeps reserved suffixes intact, use {@link + * #normalizeMetricName(String)} instead. + * * @throws IllegalArgumentException if the input is empty */ public static String sanitizeMetricName(String metricName) {