From adf2270769d5648a4b8ee1b21bc19ab929719c1c Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Fri, 26 Jan 2024 12:24:04 -0300 Subject: [PATCH 01/11] [WIP] UTF-8 support in metric and label names Signed-off-by: Federico Torres --- .../OpenMetricsTextFormatWriter.java | 56 ++++++++----------- .../PrometheusTextFormatWriter.java | 51 ++++++----------- .../expositionformats/TextFormatUtil.java | 27 ++++++--- .../model/snapshots/PrometheusNaming.java | 34 ++++++++++- 4 files changed, 90 insertions(+), 78 deletions(-) diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java index eae5c1b8c..7469fde36 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java @@ -1,22 +1,6 @@ package io.prometheus.metrics.expositionformats; -import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets; -import io.prometheus.metrics.model.snapshots.CounterSnapshot; -import io.prometheus.metrics.model.snapshots.DataPointSnapshot; -import io.prometheus.metrics.model.snapshots.DistributionDataPointSnapshot; -import io.prometheus.metrics.model.snapshots.Exemplar; -import io.prometheus.metrics.model.snapshots.Exemplars; -import io.prometheus.metrics.model.snapshots.GaugeSnapshot; -import io.prometheus.metrics.model.snapshots.HistogramSnapshot; -import io.prometheus.metrics.model.snapshots.InfoSnapshot; -import io.prometheus.metrics.model.snapshots.Labels; -import io.prometheus.metrics.model.snapshots.MetricMetadata; -import io.prometheus.metrics.model.snapshots.MetricSnapshot; -import io.prometheus.metrics.model.snapshots.MetricSnapshots; -import io.prometheus.metrics.model.snapshots.Quantile; -import io.prometheus.metrics.model.snapshots.StateSetSnapshot; -import io.prometheus.metrics.model.snapshots.SummarySnapshot; -import io.prometheus.metrics.model.snapshots.UnknownSnapshot; +import io.prometheus.metrics.model.snapshots.*; import java.io.IOException; import java.io.OutputStream; @@ -24,11 +8,7 @@ import java.nio.charset.StandardCharsets; import java.util.List; -import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeDouble; -import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeEscapedLabelValue; -import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLabels; -import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong; -import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeTimestamp; +import static io.prometheus.metrics.expositionformats.TextFormatUtil.*; /** * Write the OpenMetrics text format as defined on https://openmetrics.io. @@ -214,7 +194,7 @@ private void writeStateSet(OutputStreamWriter writer, StateSetSnapshot snapshot) } writer.write(data.getLabels().getPrometheusName(j)); writer.write("=\""); - writeEscapedLabelValue(writer, data.getLabels().getValue(j)); + writeEscapedString(writer, data.getLabels().getValue(j)); writer.write("\""); } if (!data.getLabels().isEmpty()) { @@ -222,7 +202,7 @@ private void writeStateSet(OutputStreamWriter writer, StateSetSnapshot snapshot) } writer.write(metadata.getPrometheusName()); writer.write("=\""); - writeEscapedLabelValue(writer, data.getName(i)); + writeEscapedString(writer, data.getName(i)); writer.write("\"} "); if (data.isTrue(i)) { writer.write("1"); @@ -289,13 +269,21 @@ private void writeNameAndLabels(OutputStreamWriter writer, String name, String s private void writeNameAndLabels(OutputStreamWriter writer, String name, String suffix, Labels labels, String additionalLabelName, double additionalLabelValue) throws IOException { - writer.write(name); - if (suffix != null) { - writer.write(suffix); + boolean metricInsideBraces = false; + if (PrometheusNaming.validateLegacyMetricName(name) != null) { + metricInsideBraces = true; + writer.write('{'); } + writeName(writer, name + (suffix != null ? suffix : "")); + if (!labels.isEmpty() || additionalLabelName != null) { - writeLabels(writer, labels, additionalLabelName, additionalLabelValue); + writeLabels(writer, labels, additionalLabelName, additionalLabelValue, metricInsideBraces); + } + + if (metricInsideBraces) { + writer.write('}'); } + writer.write(' '); } @@ -306,7 +294,7 @@ private void writeScrapeTimestampAndExemplar(OutputStreamWriter writer, DataPoin } if (exemplar != null) { writer.write(" # "); - writeLabels(writer, exemplar.getLabels(), null, 0); + writeLabels(writer, exemplar.getLabels(), null, 0, false); writer.write(' '); writeDouble(writer, exemplar.getValue()); if (exemplar.hasTimestamp()) { @@ -319,22 +307,22 @@ private void writeScrapeTimestampAndExemplar(OutputStreamWriter writer, DataPoin private void writeMetadata(OutputStreamWriter writer, String typeName, MetricMetadata metadata) throws IOException { writer.write("# TYPE "); - writer.write(metadata.getPrometheusName()); + writeName(writer, metadata.getPrometheusName()); writer.write(' '); writer.write(typeName); writer.write('\n'); if (metadata.getUnit() != null) { writer.write("# UNIT "); - writer.write(metadata.getPrometheusName()); + writeName(writer, metadata.getPrometheusName()); writer.write(' '); - writeEscapedLabelValue(writer, metadata.getUnit().toString()); + writeEscapedString(writer, metadata.getUnit().toString()); writer.write('\n'); } if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { writer.write("# HELP "); - writer.write(metadata.getPrometheusName()); + writeName(writer, metadata.getPrometheusName()); writer.write(' '); - writeEscapedLabelValue(writer, metadata.getHelp()); + writeEscapedString(writer, metadata.getHelp()); writer.write('\n'); } } diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java index cf9bc3d10..7a809af43 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java @@ -1,19 +1,6 @@ package io.prometheus.metrics.expositionformats; -import io.prometheus.metrics.model.snapshots.CounterSnapshot; -import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets; -import io.prometheus.metrics.model.snapshots.GaugeSnapshot; -import io.prometheus.metrics.model.snapshots.HistogramSnapshot; -import io.prometheus.metrics.model.snapshots.InfoSnapshot; -import io.prometheus.metrics.model.snapshots.Labels; -import io.prometheus.metrics.model.snapshots.DataPointSnapshot; -import io.prometheus.metrics.model.snapshots.MetricMetadata; -import io.prometheus.metrics.model.snapshots.MetricSnapshot; -import io.prometheus.metrics.model.snapshots.MetricSnapshots; -import io.prometheus.metrics.model.snapshots.Quantile; -import io.prometheus.metrics.model.snapshots.StateSetSnapshot; -import io.prometheus.metrics.model.snapshots.SummarySnapshot; -import io.prometheus.metrics.model.snapshots.UnknownSnapshot; +import io.prometheus.metrics.model.snapshots.*; import java.io.IOException; import java.io.OutputStream; @@ -21,11 +8,7 @@ import java.io.Writer; import java.nio.charset.StandardCharsets; -import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeDouble; -import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeEscapedLabelValue; -import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLabels; -import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong; -import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeTimestamp; +import static io.prometheus.metrics.expositionformats.TextFormatUtil.*; /** * Write the Prometheus text format. This is the default if you view a Prometheus endpoint with your Web browser. @@ -254,7 +237,7 @@ private void writeStateSet(OutputStreamWriter writer, StateSetSnapshot snapshot) } writer.write(data.getLabels().getPrometheusName(j)); writer.write("=\""); - writeEscapedLabelValue(writer, data.getLabels().getValue(j)); + writeEscapedString(writer, data.getLabels().getValue(j)); writer.write("\""); } if (!data.getLabels().isEmpty()) { @@ -262,7 +245,7 @@ private void writeStateSet(OutputStreamWriter writer, StateSetSnapshot snapshot) } writer.write(metadata.getPrometheusName()); writer.write("=\""); - writeEscapedLabelValue(writer, data.getName(i)); + writeEscapedString(writer, data.getName(i)); writer.write("\"} "); if (data.isTrue(i)) { writer.write("1"); @@ -290,32 +273,34 @@ private void writeNameAndLabels(OutputStreamWriter writer, String name, String s private void writeNameAndLabels(OutputStreamWriter writer, String name, String suffix, Labels labels, String additionalLabelName, double additionalLabelValue) throws IOException { - writer.write(name); - if (suffix != null) { - writer.write(suffix); + boolean metricInsideBraces = false; + if (PrometheusNaming.validateLegacyMetricName(name) != null) { + metricInsideBraces = true; + writer.write('{'); } + writeName(writer, name + (suffix != null ? suffix : "")); + if (!labels.isEmpty() || additionalLabelName != null) { - writeLabels(writer, labels, additionalLabelName, additionalLabelValue); + writeLabels(writer, labels, additionalLabelName, additionalLabelValue, metricInsideBraces); + } + + if (metricInsideBraces) { + writer.write('}'); } + writer.write(' '); } private void writeMetadata(OutputStreamWriter writer, String suffix, String typeString, MetricMetadata metadata) throws IOException { if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { writer.write("# HELP "); - writer.write(metadata.getPrometheusName()); - if (suffix != null) { - writer.write(suffix); - } + writeName(writer, metadata.getPrometheusName() + (suffix != null ? suffix : "")); writer.write(' '); writeEscapedHelp(writer, metadata.getHelp()); writer.write('\n'); } writer.write("# TYPE "); - writer.write(metadata.getPrometheusName()); - if (suffix != null) { - writer.write(suffix); - } + writeName(writer, metadata.getPrometheusName() + (suffix != null ? suffix : "")); writer.write(' '); writer.write(typeString); writer.write('\n'); diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java index 423fa6692..0dded838f 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java @@ -1,13 +1,12 @@ package io.prometheus.metrics.expositionformats; import io.prometheus.metrics.model.snapshots.Labels; +import io.prometheus.metrics.model.snapshots.PrometheusNaming; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; -import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName; - public class TextFormatUtil { static void writeLong(OutputStreamWriter writer, long value) throws IOException { @@ -38,7 +37,7 @@ static void writeTimestamp(OutputStreamWriter writer, long timestampMs) throws I writer.write(Long.toString(ms)); } - static void writeEscapedLabelValue(Writer writer, String s) throws IOException { + static void writeEscapedString(Writer writer, String s) throws IOException { for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); switch (c) { @@ -57,15 +56,17 @@ static void writeEscapedLabelValue(Writer writer, String s) throws IOException { } } - static void writeLabels(OutputStreamWriter writer, Labels labels, String additionalLabelName, double additionalLabelValue) throws IOException { - writer.write('{'); + static void writeLabels(OutputStreamWriter writer, Labels labels, String additionalLabelName, double additionalLabelValue, boolean metricInsideBraces) throws IOException { + if (!metricInsideBraces) { + writer.write('{'); + } for (int i = 0; i < labels.size(); i++) { - if (i > 0) { + if (i > 0 || metricInsideBraces) { writer.write(","); } - writer.write(labels.getPrometheusName(i)); + writeName(writer, labels.getPrometheusName(i)); writer.write("=\""); - writeEscapedLabelValue(writer, labels.getValue(i)); + writeEscapedString(writer, labels.getValue(i)); writer.write("\""); } if (additionalLabelName != null) { @@ -79,4 +80,14 @@ static void writeLabels(OutputStreamWriter writer, Labels labels, String additio } writer.write('}'); } + + static void writeName(OutputStreamWriter writer, String name) throws IOException { + if (PrometheusNaming.validateLegacyMetricName(name) == null) { + writer.write(name); + return; + } + writer.write('"'); + writeEscapedString(writer, name); + writer.write('"'); + } } 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 58de0c814..78dcc426f 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 @@ -1,7 +1,13 @@ package io.prometheus.metrics.model.snapshots; +import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; +enum ValidationScheme { + LegacyValidation, + UTF8Validation +} + /** * Utility for Prometheus Metric and Label naming. *

@@ -9,6 +15,7 @@ * in Prometheus exposition formats. However, if metrics are exposed in OpenTelemetry format the dots are retained. */ public class PrometheusNaming { + static ValidationScheme nameValidationScheme = ValidationScheme.LegacyValidation; /** * Legal characters for metric names, including dot. @@ -61,12 +68,26 @@ public static boolean isValidMetricName(String name) { return validateMetricName(name) == null; } + static String validateMetricName(String name) { + switch (nameValidationScheme) { + case LegacyValidation: + return validateLegacyMetricName(name); + case UTF8Validation: + if(!StandardCharsets.UTF_8.newEncoder().canEncode(name)) { + return "The metric name contains unsupported characters"; + } + return null; + default: + throw new RuntimeException("Invalid name validation scheme requested: " + nameValidationScheme); + } + } + /** * Same as {@link #isValidMetricName(String)}, but produces an error message. *

* The name is valid if the error message is {@code null}. */ - static String validateMetricName(String name) { + public static String validateLegacyMetricName(String name) { for (String reservedSuffix : RESERVED_METRIC_NAME_SUFFIXES) { if (name.endsWith(reservedSuffix)) { return "The metric name must not include the '" + reservedSuffix + "' suffix."; @@ -79,8 +100,15 @@ static String validateMetricName(String name) { } public static boolean isValidLabelName(String name) { - return LABEL_NAME_PATTERN.matcher(name).matches() && - !(name.startsWith("__") || name.startsWith("._") || name.startsWith("..") || name.startsWith("_.")); + switch (nameValidationScheme) { + case LegacyValidation: + return LABEL_NAME_PATTERN.matcher(name).matches() && + !(name.startsWith("__") || name.startsWith("._") || name.startsWith("..") || name.startsWith("_.")); + case UTF8Validation: + return StandardCharsets.UTF_8.newEncoder().canEncode(name); + default: + throw new RuntimeException("Invalid name validation scheme requested: " + nameValidationScheme); + } } /** From 65613229dd57777eaed3559b93828db474259737 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Wed, 31 Jan 2024 17:16:10 -0300 Subject: [PATCH 02/11] UTF-8 support in metric and label names Signed-off-by: Federico Torres --- .../OpenMetricsTextFormatWriter.java | 8 +-- .../PrometheusTextFormatWriter.java | 6 +-- .../expositionformats/TextFormatUtil.java | 27 ++++++++-- .../model/snapshots/PrometheusNaming.java | 14 +++-- .../model/snapshots/PrometheusNamingTest.java | 54 +++++++++++++++++-- 5 files changed, 91 insertions(+), 18 deletions(-) diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java index 7469fde36..012c7683b 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java @@ -274,7 +274,7 @@ private void writeNameAndLabels(OutputStreamWriter writer, String name, String s metricInsideBraces = true; writer.write('{'); } - writeName(writer, name + (suffix != null ? suffix : "")); + writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric); if (!labels.isEmpty() || additionalLabelName != null) { writeLabels(writer, labels, additionalLabelName, additionalLabelValue, metricInsideBraces); @@ -307,20 +307,20 @@ private void writeScrapeTimestampAndExemplar(OutputStreamWriter writer, DataPoin private void writeMetadata(OutputStreamWriter writer, String typeName, MetricMetadata metadata) throws IOException { writer.write("# TYPE "); - writeName(writer, metadata.getPrometheusName()); + writeName(writer, metadata.getPrometheusName(), NameType.Metric); writer.write(' '); writer.write(typeName); writer.write('\n'); if (metadata.getUnit() != null) { writer.write("# UNIT "); - writeName(writer, metadata.getPrometheusName()); + writeName(writer, metadata.getPrometheusName(), NameType.Metric); writer.write(' '); writeEscapedString(writer, metadata.getUnit().toString()); writer.write('\n'); } if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { writer.write("# HELP "); - writeName(writer, metadata.getPrometheusName()); + writeName(writer, metadata.getPrometheusName(), NameType.Metric); writer.write(' '); writeEscapedString(writer, metadata.getHelp()); writer.write('\n'); diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java index 7a809af43..1c351c025 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java @@ -278,7 +278,7 @@ private void writeNameAndLabels(OutputStreamWriter writer, String name, String s metricInsideBraces = true; writer.write('{'); } - writeName(writer, name + (suffix != null ? suffix : "")); + writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric); if (!labels.isEmpty() || additionalLabelName != null) { writeLabels(writer, labels, additionalLabelName, additionalLabelValue, metricInsideBraces); @@ -294,13 +294,13 @@ private void writeNameAndLabels(OutputStreamWriter writer, String name, String s private void writeMetadata(OutputStreamWriter writer, String suffix, String typeString, MetricMetadata metadata) throws IOException { if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { writer.write("# HELP "); - writeName(writer, metadata.getPrometheusName() + (suffix != null ? suffix : "")); + writeName(writer, metadata.getPrometheusName() + (suffix != null ? suffix : ""), NameType.Metric); writer.write(' '); writeEscapedHelp(writer, metadata.getHelp()); writer.write('\n'); } writer.write("# TYPE "); - writeName(writer, metadata.getPrometheusName() + (suffix != null ? suffix : "")); + writeName(writer, metadata.getPrometheusName() + (suffix != null ? suffix : ""), NameType.Metric); writer.write(' '); writer.write(typeString); writer.write('\n'); diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java index 0dded838f..c6994a9c9 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java @@ -7,6 +7,11 @@ import java.io.OutputStreamWriter; import java.io.Writer; +enum NameType { + Metric, + Label +} + public class TextFormatUtil { static void writeLong(OutputStreamWriter writer, long value) throws IOException { @@ -64,7 +69,7 @@ static void writeLabels(OutputStreamWriter writer, Labels labels, String additio if (i > 0 || metricInsideBraces) { writer.write(","); } - writeName(writer, labels.getPrometheusName(i)); + writeName(writer, labels.getPrometheusName(i), NameType.Label); writer.write("=\""); writeEscapedString(writer, labels.getValue(i)); writer.write("\""); @@ -81,10 +86,22 @@ static void writeLabels(OutputStreamWriter writer, Labels labels, String additio writer.write('}'); } - static void writeName(OutputStreamWriter writer, String name) throws IOException { - if (PrometheusNaming.validateLegacyMetricName(name) == null) { - writer.write(name); - return; + static void writeName(OutputStreamWriter writer, String name, NameType nameType) throws IOException { + switch (nameType) { + case Metric: + if (PrometheusNaming.isValidLegacyMetricName(name)) { + writer.write(name); + return; + } + break; + case Label: + if (PrometheusNaming.isValidLegacyLabelName(name)) { + writer.write(name); + return; + } + break; + default: + throw new RuntimeException("Invalid name type requested: " + nameType); } writer.write('"'); writeEscapedString(writer, name); 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 78dcc426f..0553a7001 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 @@ -73,7 +73,7 @@ static String validateMetricName(String name) { case LegacyValidation: return validateLegacyMetricName(name); case UTF8Validation: - if(!StandardCharsets.UTF_8.newEncoder().canEncode(name)) { + if(name.isEmpty() || !StandardCharsets.UTF_8.newEncoder().canEncode(name)) { return "The metric name contains unsupported characters"; } return null; @@ -93,16 +93,20 @@ public static String validateLegacyMetricName(String name) { return "The metric name must not include the '" + reservedSuffix + "' suffix."; } } - if (!METRIC_NAME_PATTERN.matcher(name).matches()) { + if (!isValidLegacyMetricName(name)) { return "The metric name contains unsupported characters"; } return null; } + public static boolean isValidLegacyMetricName(String name) { + return METRIC_NAME_PATTERN.matcher(name).matches(); + } + public static boolean isValidLabelName(String name) { switch (nameValidationScheme) { case LegacyValidation: - return LABEL_NAME_PATTERN.matcher(name).matches() && + return isValidLegacyLabelName(name) && !(name.startsWith("__") || name.startsWith("._") || name.startsWith("..") || name.startsWith("_.")); case UTF8Validation: return StandardCharsets.UTF_8.newEncoder().canEncode(name); @@ -111,6 +115,10 @@ public static boolean isValidLabelName(String name) { } } + public static boolean isValidLegacyLabelName(String name) { + return LABEL_NAME_PATTERN.matcher(name).matches(); + } + /** * Get the metric or label name that is used in Prometheus exposition format. * 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 d9ba9339d..f63575721 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 @@ -3,9 +3,7 @@ import org.junit.Assert; import org.junit.Test; -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; +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.*; public class PrometheusNamingTest { @@ -31,4 +29,54 @@ public void testSanitizeLabelName() { Assert.assertEquals("abc.def", sanitizeLabelName("abc.def")); Assert.assertEquals("abc.def2", sanitizeLabelName("abc.def2")); } + + @Test + public void testMetricNameIsValid() { + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + Assert.assertNull(validateMetricName("Avalid_23name")); + Assert.assertNull(validateMetricName("_Avalid_23name")); + Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("1valid_23name")); + Assert.assertNull(validateMetricName("avalid_23name")); + Assert.assertNull(validateMetricName("Ava:lid_23name")); + Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("a lid_23name")); + Assert.assertNull(validateMetricName(":leading_colon")); + Assert.assertNull(validateMetricName("colon:in:the:middle")); + Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("")); + Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("a\ud800z")); + PrometheusNaming.nameValidationScheme = ValidationScheme.UTF8Validation; + Assert.assertNull(validateMetricName("Avalid_23name")); + Assert.assertNull(validateMetricName("_Avalid_23name")); + Assert.assertNull(validateMetricName("1valid_23name")); + Assert.assertNull(validateMetricName("avalid_23name")); + Assert.assertNull(validateMetricName("Ava:lid_23name")); + Assert.assertNull(validateMetricName("a lid_23name")); + Assert.assertNull(validateMetricName(":leading_colon")); + Assert.assertNull(validateMetricName("colon:in:the:middle")); + Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("")); + Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("a\ud800z")); + } + + @Test + public void testLabelNameIsValid() { + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + Assert.assertTrue(isValidLabelName("Avalid_23name")); + Assert.assertTrue(isValidLabelName("_Avalid_23name")); + Assert.assertFalse(isValidLabelName("1valid_23name")); + Assert.assertTrue(isValidLabelName("avalid_23name")); + Assert.assertFalse(isValidLabelName("Ava:lid_23name")); + Assert.assertFalse(isValidLabelName("a lid_23name")); + Assert.assertFalse(isValidLabelName(":leading_colon")); + Assert.assertFalse(isValidLabelName("colon:in:the:middle")); + Assert.assertFalse(isValidLabelName("a\ud800z")); + PrometheusNaming.nameValidationScheme = ValidationScheme.UTF8Validation; + Assert.assertTrue(isValidLabelName("Avalid_23name")); + Assert.assertTrue(isValidLabelName("_Avalid_23name")); + Assert.assertTrue(isValidLabelName("1valid_23name")); + Assert.assertTrue(isValidLabelName("avalid_23name")); + Assert.assertTrue(isValidLabelName("Ava:lid_23name")); + Assert.assertTrue(isValidLabelName("a lid_23name")); + Assert.assertTrue(isValidLabelName(":leading_colon")); + Assert.assertTrue(isValidLabelName("colon:in:the:middle")); + Assert.assertFalse(isValidLabelName("a\ud800z")); + } } From 5f587b85673b913acacd253ea4449f6d6be75e9f Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Mon, 5 Feb 2024 11:46:45 -0300 Subject: [PATCH 03/11] Fix legacy and UTF-8 validations Signed-off-by: Federico Torres --- .../OpenMetricsTextFormatWriter.java | 4 +- .../PrometheusTextFormatWriter.java | 4 +- .../ExpositionFormatsTest.java | 81 +++++++++++++++---- .../model/snapshots/PrometheusNaming.java | 48 +++++++---- .../model/snapshots/ValidationScheme.java | 6 ++ .../model/snapshots/PrometheusNamingTest.java | 2 + 6 files changed, 109 insertions(+), 36 deletions(-) create mode 100644 prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/ValidationScheme.java diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java index 012c7683b..a3445f057 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java @@ -278,9 +278,7 @@ private void writeNameAndLabels(OutputStreamWriter writer, String name, String s if (!labels.isEmpty() || additionalLabelName != null) { writeLabels(writer, labels, additionalLabelName, additionalLabelValue, metricInsideBraces); - } - - if (metricInsideBraces) { + } else if (metricInsideBraces) { writer.write('}'); } diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java index 1c351c025..9e0883321 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java @@ -282,9 +282,7 @@ private void writeNameAndLabels(OutputStreamWriter writer, String name, String s if (!labels.isEmpty() || additionalLabelName != null) { writeLabels(writer, labels, additionalLabelName, additionalLabelValue, metricInsideBraces); - } - - if (metricInsideBraces) { + } else if (metricInsideBraces) { writer.write('}'); } diff --git a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java index 7451e7c10..d0d622cfe 100644 --- a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java +++ b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java @@ -1,26 +1,11 @@ package io.prometheus.metrics.expositionformats; +import io.prometheus.metrics.model.snapshots.*; import io.prometheus.metrics.shaded.com_google_protobuf_3_21_7.TextFormat; import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_3_21_7.Metrics; -import io.prometheus.metrics.model.snapshots.CounterSnapshot; import io.prometheus.metrics.model.snapshots.CounterSnapshot.CounterDataPointSnapshot; -import io.prometheus.metrics.model.snapshots.Exemplar; -import io.prometheus.metrics.model.snapshots.Exemplars; -import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets; -import io.prometheus.metrics.model.snapshots.GaugeSnapshot; import io.prometheus.metrics.model.snapshots.GaugeSnapshot.GaugeDataPointSnapshot; -import io.prometheus.metrics.model.snapshots.HistogramSnapshot; -import io.prometheus.metrics.model.snapshots.InfoSnapshot; -import io.prometheus.metrics.model.snapshots.Labels; -import io.prometheus.metrics.model.snapshots.MetricSnapshot; -import io.prometheus.metrics.model.snapshots.MetricSnapshots; -import io.prometheus.metrics.model.snapshots.NativeHistogramBuckets; -import io.prometheus.metrics.model.snapshots.Quantiles; -import io.prometheus.metrics.model.snapshots.StateSetSnapshot; -import io.prometheus.metrics.model.snapshots.SummarySnapshot; import io.prometheus.metrics.model.snapshots.SummarySnapshot.SummaryDataPointSnapshot; -import io.prometheus.metrics.model.snapshots.Unit; -import io.prometheus.metrics.model.snapshots.UnknownSnapshot; import io.prometheus.metrics.model.snapshots.UnknownSnapshot.UnknownDataPointSnapshot; import org.junit.Assert; import org.junit.Test; @@ -140,6 +125,7 @@ public void testCounterComplete() throws IOException { "timestamp_ms: 1672850585820 " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; CounterSnapshot counter = CounterSnapshot.builder() .name("service_time_seconds") @@ -184,6 +170,7 @@ public void testCounterMinimal() throws IOException { "my_counter_total 1.1\n"; String prometheusProtobuf = "" + "name: \"my_counter_total\" type: COUNTER metric { counter { value: 1.1 } }"; + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; CounterSnapshot counter = CounterSnapshot.builder() .name("my_counter") .dataPoint(CounterDataPointSnapshot.builder().value(1.1).build()) @@ -215,6 +202,7 @@ public void testCounterWithDots() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; CounterSnapshot counter = CounterSnapshot.builder() .name("my.request.count") @@ -260,6 +248,7 @@ public void testGaugeComplete() throws IOException { "timestamp_ms: 1672850585820 " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; GaugeSnapshot gauge = GaugeSnapshot.builder() .name("disk_usage_ratio") .help("percentage used") @@ -299,6 +288,7 @@ public void testGaugeMinimal() throws IOException { "temperature_centigrade 22.3\n"; String prometheusProtobuf = "" + "name: \"temperature_centigrade\" type: GAUGE metric { gauge { value: 22.3 } }"; + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; GaugeSnapshot gauge = GaugeSnapshot.builder() .name("temperature_centigrade") .dataPoint(GaugeDataPointSnapshot.builder() @@ -336,6 +326,7 @@ public void testGaugeWithDots() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; GaugeSnapshot gauge = GaugeSnapshot.builder() .name("my.temperature.celsius") @@ -354,6 +345,36 @@ public void testGaugeWithDots() throws IOException { assertPrometheusProtobuf(prometheusProtobuf, gauge); } + @Test + public void testGaugeUTF8() throws IOException { + String prometheusText = + "# HELP \"gauge.name\" gauge\\ndoc\\nstr\"ing\n" + + "# TYPE \"gauge.name\" gauge\n" + + "{\"gauge.name\",\"name*2\"=\"val with \\\\backslash and \\\"quotes\\\"\",\"name.1\"=\"val with\\nnew line\"} +Inf\n" + + "{\"gauge.name\",\"name*2\"=\"佖佥\",\"name.1\"=\"Björn\"} 3.14E42\n"; + PrometheusNaming.nameValidationScheme = ValidationScheme.UTF8Validation; + + GaugeSnapshot gauge = GaugeSnapshot.builder() + .name("gauge.name") + .help("gauge\ndoc\nstr\"ing") + .dataPoint(GaugeDataPointSnapshot.builder() + .value(Double.POSITIVE_INFINITY) + .labels(Labels.builder() + .label("name.1", "val with\nnew line") + .label("name*2", "val with \\backslash and \"quotes\"") + .build()) + .build()) + .dataPoint(GaugeDataPointSnapshot.builder() + .value(3.14e42) + .labels(Labels.builder() + .label("name.1", "Björn") + .label("name*2", "佖佥") + .build()) + .build()) + .build(); + assertPrometheusText(prometheusText, gauge); + } + @Test public void testSummaryComplete() throws IOException { String openMetricsText = "" + @@ -445,6 +466,7 @@ public void testSummaryComplete() throws IOException { "timestamp_ms: 1672850585820 " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; SummarySnapshot summary = SummarySnapshot.builder() .name("http_request_duration_seconds") .help("request duration") @@ -513,6 +535,7 @@ public void testSummaryWithoutQuantiles() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .help("latency") @@ -548,6 +571,7 @@ public void testSummaryNoCountAndSum() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .dataPoint(SummaryDataPointSnapshot.builder() @@ -580,6 +604,7 @@ public void testSummaryJustCount() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .dataPoint(SummaryDataPointSnapshot.builder() @@ -612,6 +637,7 @@ public void testSummaryJustSum() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .dataPoint(SummaryDataPointSnapshot.builder() @@ -627,6 +653,7 @@ public void testSummaryJustSum() throws IOException { @Test public void testSummaryEmptyData() throws IOException { + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; // SummaryData can be present but empty (no count, no sum, no quantiles). // This should be treated like no data is present. SummarySnapshot summary = SummarySnapshot.builder() @@ -665,6 +692,7 @@ public void testSummaryEmptyAndNonEmpty() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .dataPoint(SummaryDataPointSnapshot.builder() @@ -710,6 +738,7 @@ public void testSummaryWithDots() throws IOException { "summary { sample_count: 1 sample_sum: 0.03 } " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; SummarySnapshot summary = SummarySnapshot.builder() .name("my.request.duration.seconds") @@ -831,6 +860,7 @@ public void testClassicHistogramComplete() throws Exception { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("response_size_bytes") .help("help") @@ -892,6 +922,7 @@ public void testClassicHistogramMinimal() throws Exception { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("request_latency_seconds") .dataPoint(HistogramSnapshot.HistogramDataPointSnapshot.builder() @@ -935,6 +966,7 @@ public void testClassicHistogramCountAndSum() throws Exception { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("request_latency_seconds") .dataPoint(HistogramSnapshot.HistogramDataPointSnapshot.builder() @@ -1054,6 +1086,7 @@ public void testClassicGaugeHistogramComplete() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; HistogramSnapshot gaugeHistogram = HistogramSnapshot.builder() .gaugeHistogram(true) .name("cache_size_bytes") @@ -1117,6 +1150,7 @@ public void testClassicGaugeHistogramMinimal() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; HistogramSnapshot gaugeHistogram = HistogramSnapshot.builder() .gaugeHistogram(true) .name("queue_size_bytes") @@ -1163,6 +1197,7 @@ public void testClassicGaugeHistogramCountAndSum() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; HistogramSnapshot gaugeHistogram = HistogramSnapshot.builder() .gaugeHistogram(true) .name("queue_size_bytes") @@ -1210,6 +1245,7 @@ public void testClassicHistogramWithDots() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("my.request.duration.seconds") @@ -1337,6 +1373,7 @@ public void testNativeHistogramComplete() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; HistogramSnapshot nativeHistogram = HistogramSnapshot.builder() .name("response_size_bytes") .help("help") @@ -1417,6 +1454,7 @@ public void testNativeHistogramMinimal() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; HistogramSnapshot nativeHistogram = HistogramSnapshot.builder() .name("latency_seconds") .dataPoint(HistogramSnapshot.HistogramDataPointSnapshot.builder() @@ -1463,6 +1501,7 @@ public void testNativeHistogramWithDots() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("my.request.duration.seconds") @@ -1499,6 +1538,7 @@ public void testInfo() throws IOException { "# HELP version_info version information\n" + "# TYPE version_info gauge\n" + "version_info{version=\"1.2.3\"} 1\n"; + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; InfoSnapshot info = InfoSnapshot.builder() .name("version") .help("version information") @@ -1533,6 +1573,7 @@ public void testInfoWithDots() throws IOException { "gauge { value: 1.0 } " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; InfoSnapshot info = InfoSnapshot.builder() .name("jvm.status") .help("JVM status info") @@ -1562,6 +1603,7 @@ public void testStateSetComplete() throws IOException { "state{env=\"dev\",state=\"state2\"} 0 " + scrapeTimestamp1s + "\n" + "state{env=\"prod\",state=\"state1\"} 0 " + scrapeTimestamp2s + "\n" + "state{env=\"prod\",state=\"state2\"} 1 " + scrapeTimestamp2s + "\n"; + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; StateSetSnapshot stateSet = StateSetSnapshot.builder() .name("state") .help("complete state set example") @@ -1595,6 +1637,7 @@ public void testStateSetMinimal() throws IOException { "# TYPE state gauge\n" + "state{state=\"a\"} 1\n" + "state{state=\"bb\"} 0\n"; + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; StateSetSnapshot stateSet = StateSetSnapshot.builder() .name("state") .dataPoint(StateSetSnapshot.StateSetDataPointSnapshot.builder() @@ -1636,6 +1679,7 @@ public void testStateSetWithDots() throws IOException { "gauge { value: 0.0 } " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; StateSetSnapshot stateSet = StateSetSnapshot.builder() .name("my.application.state") .help("My application state") @@ -1664,6 +1708,7 @@ public void testUnknownComplete() throws IOException { "# TYPE my_special_thing_bytes untyped\n" + "my_special_thing_bytes{env=\"dev\"} 0.2 " + scrapeTimestamp1s + "\n" + "my_special_thing_bytes{env=\"prod\"} 0.7 " + scrapeTimestamp2s + "\n"; + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; UnknownSnapshot unknown = UnknownSnapshot.builder() .name("my_special_thing_bytes") .help("help message") @@ -1696,6 +1741,7 @@ public void testUnknownMinimal() throws IOException { String prometheus = "" + "# TYPE other untyped\n" + "other 22.3\n"; + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; UnknownSnapshot unknown = UnknownSnapshot.builder() .name("other") .dataPoint(UnknownDataPointSnapshot.builder() @@ -1730,6 +1776,7 @@ public void testUnknownWithDots() throws IOException { "untyped { value: 0.7 } " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; UnknownSnapshot unknown = UnknownSnapshot.builder() .name("some.unknown.metric") .help("help message") @@ -1756,6 +1803,7 @@ public void testHelpEscape() throws IOException { "# HELP test_total Some text and \\n some \" escaping\n" + "# TYPE test_total counter\n" + "test_total 1.0\n"; + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; CounterSnapshot counter = CounterSnapshot.builder() .name("test") .help("Some text and \n some \" escaping") // example from https://openMetrics.io @@ -1776,6 +1824,7 @@ public void testLabelValueEscape() throws IOException { String prometheus = "" + "# TYPE test_total counter\n" + "test_total{a=\"x\",b=\"escaping\\\" example \\n \"} 1.0\n"; + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; CounterSnapshot counter = CounterSnapshot.builder() .name("test") .dataPoint(CounterDataPointSnapshot.builder() 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 0553a7001..9e4e74c9d 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 @@ -3,11 +3,6 @@ import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; -enum ValidationScheme { - LegacyValidation, - UTF8Validation -} - /** * Utility for Prometheus Metric and Label naming. *

@@ -15,17 +10,21 @@ enum ValidationScheme { * in Prometheus exposition formats. However, if metrics are exposed in OpenTelemetry format the dots are retained. */ public class PrometheusNaming { - static ValidationScheme nameValidationScheme = ValidationScheme.LegacyValidation; + public static ValidationScheme nameValidationScheme = ValidationScheme.LegacyValidation; /** * Legal characters for metric names, including dot. */ - private static final Pattern METRIC_NAME_PATTERN = Pattern.compile("^[a-zA-Z_.:][a-zA-Z0-9_.:]+$"); + private static final Pattern LEGACY_METRIC_NAME_PATTERN = Pattern.compile("^[a-zA-Z_.:][a-zA-Z0-9_.:]+$"); + + private static final Pattern METRIC_NAME_PATTERN = Pattern.compile("^[a-zA-Z_:][a-zA-Z0-9_:]+$"); /** * Legal characters for label names, including dot. */ - private static final Pattern LABEL_NAME_PATTERN = Pattern.compile("^[a-zA-Z_.][a-zA-Z0-9_.]*$"); + private static final Pattern LEGACY_LABEL_NAME_PATTERN = Pattern.compile("^[a-zA-Z_.][a-zA-Z0-9_.]*$"); + + private static final Pattern LABEL_NAME_PATTERN = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$"); /** * According to OpenMetrics {@code _count} and {@code _sum} (and {@code _gcount}, {@code _gsum}) should also be @@ -49,7 +48,7 @@ public class PrometheusNaming { /** * Test if a metric name is valid. Rules: *

* If a metric has a {@link Unit}, the metric name SHOULD end with the unit as a suffix. @@ -100,7 +99,14 @@ public static String validateLegacyMetricName(String name) { } public static boolean isValidLegacyMetricName(String name) { - return METRIC_NAME_PATTERN.matcher(name).matches(); + switch (nameValidationScheme) { + case LegacyValidation: + return LEGACY_METRIC_NAME_PATTERN.matcher(name).matches(); + case UTF8Validation: + return METRIC_NAME_PATTERN.matcher(name).matches(); + default: + throw new RuntimeException("Invalid name validation scheme requested: " + nameValidationScheme); + } } public static boolean isValidLabelName(String name) { @@ -116,7 +122,14 @@ public static boolean isValidLabelName(String name) { } public static boolean isValidLegacyLabelName(String name) { - return LABEL_NAME_PATTERN.matcher(name).matches(); + switch (nameValidationScheme) { + case LegacyValidation: + return LEGACY_LABEL_NAME_PATTERN.matcher(name).matches(); + case UTF8Validation: + return LABEL_NAME_PATTERN.matcher(name).matches(); + default: + throw new RuntimeException("Invalid name validation scheme requested: " + nameValidationScheme); + } } /** @@ -128,7 +141,14 @@ public static boolean isValidLegacyLabelName(String name) { * @return the name with dots replaced by underscores. */ public static String prometheusName(String name) { - return name.replace(".", "_"); + switch (nameValidationScheme) { + case LegacyValidation: + return name.replace(".", "_"); + case UTF8Validation: + return name; + default: + throw new RuntimeException("Invalid name validation scheme requested: " + nameValidationScheme); + } } /** @@ -172,7 +192,7 @@ public static String sanitizeLabelName(String labelName) { } /** - * Returns a string that matches {@link #METRIC_NAME_PATTERN}. + * Returns a string that matches {@link #LEGACY_METRIC_NAME_PATTERN}. */ private static String replaceIllegalCharsInMetricName(String name) { int length = name.length(); @@ -193,7 +213,7 @@ private static String replaceIllegalCharsInMetricName(String name) { } /** - * Returns a string that matches {@link #LABEL_NAME_PATTERN}. + * Returns a string that matches {@link #LEGACY_LABEL_NAME_PATTERN}. */ private static String replaceIllegalCharsInLabelName(String name) { int length = name.length(); diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/ValidationScheme.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/ValidationScheme.java new file mode 100644 index 000000000..4be8933c4 --- /dev/null +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/ValidationScheme.java @@ -0,0 +1,6 @@ +package io.prometheus.metrics.model.snapshots; + +public enum ValidationScheme { + LegacyValidation, + UTF8Validation +} 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 f63575721..5bf137051 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 @@ -9,6 +9,7 @@ public class PrometheusNamingTest { @Test public void testSanitizeMetricName() { + nameValidationScheme = ValidationScheme.LegacyValidation; Assert.assertEquals("_abc_def", prometheusName(sanitizeMetricName("0abc.def"))); Assert.assertEquals("___ab_:c0", prometheusName(sanitizeMetricName("___ab.:c0"))); Assert.assertEquals("my_prefix_my_metric", sanitizeMetricName("my_prefix/my_metric")); @@ -21,6 +22,7 @@ public void testSanitizeMetricName() { @Test public void testSanitizeLabelName() { + PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; Assert.assertEquals("_abc_def", prometheusName(sanitizeLabelName("0abc.def"))); Assert.assertEquals("_abc", prometheusName(sanitizeLabelName("_abc"))); Assert.assertEquals("_abc", prometheusName(sanitizeLabelName("__abc"))); From 5e6f0a82318e1e306419683303acca5657ed6e8e Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Wed, 21 Feb 2024 15:54:08 -0300 Subject: [PATCH 04/11] Add UTF-8 content negotiation changes Signed-off-by: Federico Torres --- .../common/PrometheusScrapeHandler.java | 16 +- .../ExpositionFormatWriter.java | 3 +- .../OpenMetricsTextFormatWriter.java | 6 +- .../PrometheusProtobufWriter.java | 19 +- .../PrometheusTextFormatWriter.java | 10 +- .../ExpositionFormatsTest.java | 70 ++-- .../model/snapshots/EscapingScheme.java | 60 +++ .../model/snapshots/PrometheusNaming.java | 386 +++++++++++++++++- .../model/snapshots/ValidationScheme.java | 4 +- .../model/snapshots/PrometheusNamingTest.java | 12 +- 10 files changed, 502 insertions(+), 84 deletions(-) create mode 100644 prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/EscapingScheme.java diff --git a/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java b/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java index 5155457df..9c6b81533 100644 --- a/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java +++ b/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java @@ -6,6 +6,7 @@ import io.prometheus.metrics.expositionformats.ExpositionFormats; import io.prometheus.metrics.model.registry.MetricNameFilter; import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.model.snapshots.EscapingScheme; import io.prometheus.metrics.model.snapshots.MetricSnapshots; import java.io.ByteArrayOutputStream; @@ -51,15 +52,16 @@ public void handleRequest(PrometheusHttpExchange exchange) throws IOException { PrometheusHttpRequest request = exchange.getRequest(); PrometheusHttpResponse response = exchange.getResponse(); MetricSnapshots snapshots = scrape(request); - if (writeDebugResponse(snapshots, exchange)) { + String acceptHeader = request.getHeader("Accept"); + EscapingScheme escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeader); + if (writeDebugResponse(snapshots, exchange, escapingScheme)) { return; } ByteArrayOutputStream responseBuffer = new ByteArrayOutputStream(lastResponseSize.get() + 1024); - String acceptHeader = request.getHeader("Accept"); ExpositionFormatWriter writer = expositionFormats.findWriter(acceptHeader); - writer.write(responseBuffer, snapshots); + writer.write(responseBuffer, snapshots, escapingScheme); lastResponseSize.set(responseBuffer.size()); - response.setHeader("Content-Type", writer.getContentType()); + response.setHeader("Content-Type", writer.getContentType() + escapingScheme.toHeaderFormat()); if (shouldUseCompression(request)) { response.setHeader("Content-Encoding", "gzip"); @@ -126,7 +128,7 @@ private Predicate makeNameFilter(String[] includedNames) { return result; } - private boolean writeDebugResponse(MetricSnapshots snapshots, PrometheusHttpExchange exchange) throws IOException { + private boolean writeDebugResponse(MetricSnapshots snapshots, PrometheusHttpExchange exchange, EscapingScheme escapingScheme) throws IOException { String debugParam = exchange.getRequest().getParameter("debug"); PrometheusHttpResponse response = exchange.getResponse(); if (debugParam == null) { @@ -138,10 +140,10 @@ private boolean writeDebugResponse(MetricSnapshots snapshots, PrometheusHttpExch OutputStream body = response.sendHeadersAndGetBody(responseStatus, 0); switch (debugParam) { case "openmetrics": - expositionFormats.getOpenMetricsTextFormatWriter().write(body, snapshots); + expositionFormats.getOpenMetricsTextFormatWriter().write(body, snapshots, escapingScheme); break; case "text": - expositionFormats.getPrometheusTextFormatWriter().write(body, snapshots); + expositionFormats.getPrometheusTextFormatWriter().write(body, snapshots, escapingScheme); break; case "prometheus-protobuf": String debugString = expositionFormats.getPrometheusProtobufWriter().toDebugString(snapshots); diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormatWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormatWriter.java index 3db354c56..ea879354c 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormatWriter.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormatWriter.java @@ -1,5 +1,6 @@ package io.prometheus.metrics.expositionformats; +import io.prometheus.metrics.model.snapshots.EscapingScheme; import io.prometheus.metrics.model.snapshots.MetricSnapshots; import java.io.IOException; @@ -11,6 +12,6 @@ public interface ExpositionFormatWriter { /** * Text formats use UTF-8 encoding. */ - void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException; + void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme escapingScheme) throws IOException; String getContentType(); } diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java index a3445f057..020833dbd 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java @@ -40,9 +40,11 @@ public String getContentType() { return CONTENT_TYPE; } - public void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException { + public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme escapingScheme) throws IOException { OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); - for (MetricSnapshot snapshot : metricSnapshots) { + for (MetricSnapshot s : metricSnapshots) { + MetricSnapshot snapshot = PrometheusNaming.escapeMetricSnapshot(s, escapingScheme); + if (snapshot.getDataPoints().size() > 0) { if (snapshot instanceof CounterSnapshot) { writeCounter(writer, (CounterSnapshot) snapshot); diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriter.java index 090ac2a89..a39d03432 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriter.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriter.java @@ -1,24 +1,9 @@ package io.prometheus.metrics.expositionformats; +import io.prometheus.metrics.model.snapshots.*; import io.prometheus.metrics.shaded.com_google_protobuf_3_21_7.TextFormat; import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_3_21_7.Metrics; -import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets; -import io.prometheus.metrics.model.snapshots.CounterSnapshot; import io.prometheus.metrics.model.snapshots.CounterSnapshot.CounterDataPointSnapshot; -import io.prometheus.metrics.model.snapshots.Exemplar; -import io.prometheus.metrics.model.snapshots.GaugeSnapshot; -import io.prometheus.metrics.model.snapshots.HistogramSnapshot; -import io.prometheus.metrics.model.snapshots.InfoSnapshot; -import io.prometheus.metrics.model.snapshots.Labels; -import io.prometheus.metrics.model.snapshots.DataPointSnapshot; -import io.prometheus.metrics.model.snapshots.MetricMetadata; -import io.prometheus.metrics.model.snapshots.MetricSnapshot; -import io.prometheus.metrics.model.snapshots.MetricSnapshots; -import io.prometheus.metrics.model.snapshots.NativeHistogramBuckets; -import io.prometheus.metrics.model.snapshots.Quantiles; -import io.prometheus.metrics.model.snapshots.StateSetSnapshot; -import io.prometheus.metrics.model.snapshots.SummarySnapshot; -import io.prometheus.metrics.model.snapshots.UnknownSnapshot; import java.io.IOException; import java.io.OutputStream; @@ -61,7 +46,7 @@ public String toDebugString(MetricSnapshots metricSnapshots) { } @Override - public void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException { + public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme escapingScheme) throws IOException { for (MetricSnapshot snapshot : metricSnapshots) { if (snapshot.getDataPoints().size() > 0) { convert(snapshot).writeDelimitedTo(out); diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java index 9e0883321..c624510a4 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java @@ -37,11 +37,13 @@ public String getContentType() { return CONTENT_TYPE; } - public void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException { + public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme escapingScheme) throws IOException { // See https://prometheus.io/docs/instrumenting/exposition_formats/ // "unknown", "gauge", "counter", "stateset", "info", "histogram", "gaugehistogram", and "summary". OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); - for (MetricSnapshot snapshot : metricSnapshots) { + for (MetricSnapshot s : metricSnapshots) { + MetricSnapshot snapshot = PrometheusNaming.escapeMetricSnapshot(s, escapingScheme); + if (snapshot.getDataPoints().size() > 0) { if (snapshot instanceof CounterSnapshot) { writeCounter(writer, (CounterSnapshot) snapshot); @@ -61,7 +63,9 @@ public void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOEx } } if (writeCreatedTimestamps) { - for (MetricSnapshot snapshot : metricSnapshots) { + for (MetricSnapshot ms : metricSnapshots) { + MetricSnapshot snapshot = PrometheusNaming.escapeMetricSnapshot(ms, escapingScheme); + if (snapshot.getDataPoints().size() > 0) { if (snapshot instanceof CounterSnapshot) { writeCreated(writer, snapshot); diff --git a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java index d0d622cfe..23af472e6 100644 --- a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java +++ b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java @@ -125,7 +125,7 @@ public void testCounterComplete() throws IOException { "timestamp_ms: 1672850585820 " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; CounterSnapshot counter = CounterSnapshot.builder() .name("service_time_seconds") @@ -170,7 +170,7 @@ public void testCounterMinimal() throws IOException { "my_counter_total 1.1\n"; String prometheusProtobuf = "" + "name: \"my_counter_total\" type: COUNTER metric { counter { value: 1.1 } }"; - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; CounterSnapshot counter = CounterSnapshot.builder() .name("my_counter") .dataPoint(CounterDataPointSnapshot.builder().value(1.1).build()) @@ -202,7 +202,7 @@ public void testCounterWithDots() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; CounterSnapshot counter = CounterSnapshot.builder() .name("my.request.count") @@ -248,7 +248,7 @@ public void testGaugeComplete() throws IOException { "timestamp_ms: 1672850585820 " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; GaugeSnapshot gauge = GaugeSnapshot.builder() .name("disk_usage_ratio") .help("percentage used") @@ -288,7 +288,7 @@ public void testGaugeMinimal() throws IOException { "temperature_centigrade 22.3\n"; String prometheusProtobuf = "" + "name: \"temperature_centigrade\" type: GAUGE metric { gauge { value: 22.3 } }"; - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; GaugeSnapshot gauge = GaugeSnapshot.builder() .name("temperature_centigrade") .dataPoint(GaugeDataPointSnapshot.builder() @@ -326,7 +326,7 @@ public void testGaugeWithDots() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; GaugeSnapshot gauge = GaugeSnapshot.builder() .name("my.temperature.celsius") @@ -352,7 +352,7 @@ public void testGaugeUTF8() throws IOException { "# TYPE \"gauge.name\" gauge\n" + "{\"gauge.name\",\"name*2\"=\"val with \\\\backslash and \\\"quotes\\\"\",\"name.1\"=\"val with\\nnew line\"} +Inf\n" + "{\"gauge.name\",\"name*2\"=\"佖佥\",\"name.1\"=\"Björn\"} 3.14E42\n"; - PrometheusNaming.nameValidationScheme = ValidationScheme.UTF8Validation; + PrometheusNaming.nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; GaugeSnapshot gauge = GaugeSnapshot.builder() .name("gauge.name") @@ -466,7 +466,7 @@ public void testSummaryComplete() throws IOException { "timestamp_ms: 1672850585820 " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("http_request_duration_seconds") .help("request duration") @@ -535,7 +535,7 @@ public void testSummaryWithoutQuantiles() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .help("latency") @@ -571,7 +571,7 @@ public void testSummaryNoCountAndSum() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .dataPoint(SummaryDataPointSnapshot.builder() @@ -604,7 +604,7 @@ public void testSummaryJustCount() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .dataPoint(SummaryDataPointSnapshot.builder() @@ -637,7 +637,7 @@ public void testSummaryJustSum() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .dataPoint(SummaryDataPointSnapshot.builder() @@ -653,7 +653,7 @@ public void testSummaryJustSum() throws IOException { @Test public void testSummaryEmptyData() throws IOException { - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; // SummaryData can be present but empty (no count, no sum, no quantiles). // This should be treated like no data is present. SummarySnapshot summary = SummarySnapshot.builder() @@ -692,7 +692,7 @@ public void testSummaryEmptyAndNonEmpty() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .dataPoint(SummaryDataPointSnapshot.builder() @@ -738,7 +738,7 @@ public void testSummaryWithDots() throws IOException { "summary { sample_count: 1 sample_sum: 0.03 } " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("my.request.duration.seconds") @@ -860,7 +860,7 @@ public void testClassicHistogramComplete() throws Exception { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("response_size_bytes") .help("help") @@ -922,7 +922,7 @@ public void testClassicHistogramMinimal() throws Exception { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("request_latency_seconds") .dataPoint(HistogramSnapshot.HistogramDataPointSnapshot.builder() @@ -966,7 +966,7 @@ public void testClassicHistogramCountAndSum() throws Exception { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("request_latency_seconds") .dataPoint(HistogramSnapshot.HistogramDataPointSnapshot.builder() @@ -1086,7 +1086,7 @@ public void testClassicGaugeHistogramComplete() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot gaugeHistogram = HistogramSnapshot.builder() .gaugeHistogram(true) .name("cache_size_bytes") @@ -1150,7 +1150,7 @@ public void testClassicGaugeHistogramMinimal() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot gaugeHistogram = HistogramSnapshot.builder() .gaugeHistogram(true) .name("queue_size_bytes") @@ -1197,7 +1197,7 @@ public void testClassicGaugeHistogramCountAndSum() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot gaugeHistogram = HistogramSnapshot.builder() .gaugeHistogram(true) .name("queue_size_bytes") @@ -1245,7 +1245,7 @@ public void testClassicHistogramWithDots() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("my.request.duration.seconds") @@ -1373,7 +1373,7 @@ public void testNativeHistogramComplete() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot nativeHistogram = HistogramSnapshot.builder() .name("response_size_bytes") .help("help") @@ -1454,7 +1454,7 @@ public void testNativeHistogramMinimal() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot nativeHistogram = HistogramSnapshot.builder() .name("latency_seconds") .dataPoint(HistogramSnapshot.HistogramDataPointSnapshot.builder() @@ -1501,7 +1501,7 @@ public void testNativeHistogramWithDots() throws IOException { "} " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("my.request.duration.seconds") @@ -1538,7 +1538,7 @@ public void testInfo() throws IOException { "# HELP version_info version information\n" + "# TYPE version_info gauge\n" + "version_info{version=\"1.2.3\"} 1\n"; - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; InfoSnapshot info = InfoSnapshot.builder() .name("version") .help("version information") @@ -1573,7 +1573,7 @@ public void testInfoWithDots() throws IOException { "gauge { value: 1.0 } " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; InfoSnapshot info = InfoSnapshot.builder() .name("jvm.status") .help("JVM status info") @@ -1603,7 +1603,7 @@ public void testStateSetComplete() throws IOException { "state{env=\"dev\",state=\"state2\"} 0 " + scrapeTimestamp1s + "\n" + "state{env=\"prod\",state=\"state1\"} 0 " + scrapeTimestamp2s + "\n" + "state{env=\"prod\",state=\"state2\"} 1 " + scrapeTimestamp2s + "\n"; - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; StateSetSnapshot stateSet = StateSetSnapshot.builder() .name("state") .help("complete state set example") @@ -1637,7 +1637,7 @@ public void testStateSetMinimal() throws IOException { "# TYPE state gauge\n" + "state{state=\"a\"} 1\n" + "state{state=\"bb\"} 0\n"; - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; StateSetSnapshot stateSet = StateSetSnapshot.builder() .name("state") .dataPoint(StateSetSnapshot.StateSetDataPointSnapshot.builder() @@ -1679,7 +1679,7 @@ public void testStateSetWithDots() throws IOException { "gauge { value: 0.0 } " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; StateSetSnapshot stateSet = StateSetSnapshot.builder() .name("my.application.state") .help("My application state") @@ -1708,7 +1708,7 @@ public void testUnknownComplete() throws IOException { "# TYPE my_special_thing_bytes untyped\n" + "my_special_thing_bytes{env=\"dev\"} 0.2 " + scrapeTimestamp1s + "\n" + "my_special_thing_bytes{env=\"prod\"} 0.7 " + scrapeTimestamp2s + "\n"; - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; UnknownSnapshot unknown = UnknownSnapshot.builder() .name("my_special_thing_bytes") .help("help message") @@ -1741,7 +1741,7 @@ public void testUnknownMinimal() throws IOException { String prometheus = "" + "# TYPE other untyped\n" + "other 22.3\n"; - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; UnknownSnapshot unknown = UnknownSnapshot.builder() .name("other") .dataPoint(UnknownDataPointSnapshot.builder() @@ -1776,7 +1776,7 @@ public void testUnknownWithDots() throws IOException { "untyped { value: 0.7 } " + "}"; //@formatter:on - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; UnknownSnapshot unknown = UnknownSnapshot.builder() .name("some.unknown.metric") .help("help message") @@ -1803,7 +1803,7 @@ public void testHelpEscape() throws IOException { "# HELP test_total Some text and \\n some \" escaping\n" + "# TYPE test_total counter\n" + "test_total 1.0\n"; - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; CounterSnapshot counter = CounterSnapshot.builder() .name("test") .help("Some text and \n some \" escaping") // example from https://openMetrics.io @@ -1824,7 +1824,7 @@ public void testLabelValueEscape() throws IOException { String prometheus = "" + "# TYPE test_total counter\n" + "test_total{a=\"x\",b=\"escaping\\\" example \\n \"} 1.0\n"; - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; CounterSnapshot counter = CounterSnapshot.builder() .name("test") .dataPoint(CounterDataPointSnapshot.builder() diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/EscapingScheme.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/EscapingScheme.java new file mode 100644 index 000000000..91566030e --- /dev/null +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/EscapingScheme.java @@ -0,0 +1,60 @@ +package io.prometheus.metrics.model.snapshots; + +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.ESCAPING_KEY; +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.nameEscapingScheme; + +public enum EscapingScheme { + NO_ESCAPING("allow-utf-8"), + UNDERSCORE_ESCAPING("underscores"), + DOTS_ESCAPING("dots"), + VALUE_ENCODING_ESCAPING("values"), + ; + + public final String getValue() { + return value; + } + + private final String value; + + EscapingScheme(String value) { + this.value = value; + } + + public static EscapingScheme fromAcceptHeader(String acceptHeader) { + for (String p : acceptHeader.split(";")) { + String[] toks = p.split("="); + if (toks.length != 2) { + continue; + } + String key = toks[0].trim(); + String value = toks[1].trim(); + if (key.equals(ESCAPING_KEY)) { + try { + return EscapingScheme.forString(value); + } catch (IllegalArgumentException e) { + return nameEscapingScheme; + } + } + } + return nameEscapingScheme; + } + + private static EscapingScheme forString(String value) { + switch(value) { + case "allow-utf-8": + return NO_ESCAPING; + case "underscores": + return UNDERSCORE_ESCAPING; + case "dots": + return DOTS_ESCAPING; + case "values": + return VALUE_ENCODING_ESCAPING; + default: + throw new IllegalArgumentException("Unknown escaping scheme: " + value); + } + } + + public String toHeaderFormat() { + return "; " + ESCAPING_KEY + "=" + value; + } +} 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 9e4e74c9d..740fcfdc7 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 @@ -1,6 +1,9 @@ package io.prometheus.metrics.model.snapshots; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; import java.util.regex.Pattern; /** @@ -10,7 +13,15 @@ * in Prometheus exposition formats. However, if metrics are exposed in OpenTelemetry format the dots are retained. */ public class PrometheusNaming { - public static ValidationScheme nameValidationScheme = ValidationScheme.LegacyValidation; + public static ValidationScheme nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; + + public static EscapingScheme nameEscapingScheme = EscapingScheme.VALUE_ENCODING_ESCAPING; + + public static final String ESCAPING_KEY = "escaping"; + + private static final String LOWERHEX = "0123456789abcdef"; + + private static final String METRIC_NAME_LABEL= "__name__"; /** * Legal characters for metric names, including dot. @@ -69,9 +80,9 @@ public static boolean isValidMetricName(String name) { static String validateMetricName(String name) { switch (nameValidationScheme) { - case LegacyValidation: + case LEGACY_VALIDATION: return validateLegacyMetricName(name); - case UTF8Validation: + case UTF_8_VALIDATION: if(name.isEmpty() || !StandardCharsets.UTF_8.newEncoder().canEncode(name)) { return "The metric name contains unsupported characters"; } @@ -100,9 +111,9 @@ public static String validateLegacyMetricName(String name) { public static boolean isValidLegacyMetricName(String name) { switch (nameValidationScheme) { - case LegacyValidation: + case LEGACY_VALIDATION: return LEGACY_METRIC_NAME_PATTERN.matcher(name).matches(); - case UTF8Validation: + case UTF_8_VALIDATION: return METRIC_NAME_PATTERN.matcher(name).matches(); default: throw new RuntimeException("Invalid name validation scheme requested: " + nameValidationScheme); @@ -111,10 +122,10 @@ public static boolean isValidLegacyMetricName(String name) { public static boolean isValidLabelName(String name) { switch (nameValidationScheme) { - case LegacyValidation: + case LEGACY_VALIDATION: return isValidLegacyLabelName(name) && !(name.startsWith("__") || name.startsWith("._") || name.startsWith("..") || name.startsWith("_.")); - case UTF8Validation: + case UTF_8_VALIDATION: return StandardCharsets.UTF_8.newEncoder().canEncode(name); default: throw new RuntimeException("Invalid name validation scheme requested: " + nameValidationScheme); @@ -123,9 +134,9 @@ public static boolean isValidLabelName(String name) { public static boolean isValidLegacyLabelName(String name) { switch (nameValidationScheme) { - case LegacyValidation: + case LEGACY_VALIDATION: return LEGACY_LABEL_NAME_PATTERN.matcher(name).matches(); - case UTF8Validation: + case UTF_8_VALIDATION: return LABEL_NAME_PATTERN.matcher(name).matches(); default: throw new RuntimeException("Invalid name validation scheme requested: " + nameValidationScheme); @@ -142,9 +153,9 @@ public static boolean isValidLegacyLabelName(String name) { */ public static String prometheusName(String name) { switch (nameValidationScheme) { - case LegacyValidation: + case LEGACY_VALIDATION: return name.replace(".", "_"); - case UTF8Validation: + case UTF_8_VALIDATION: return name; default: throw new RuntimeException("Invalid name validation scheme requested: " + nameValidationScheme); @@ -231,4 +242,357 @@ private static String replaceIllegalCharsInLabelName(String name) { } return new String(sanitized); } + + public static MetricSnapshot escapeMetricSnapshot(MetricSnapshot v, EscapingScheme scheme) { + if (v == null) { + return null; + } + + if (scheme == EscapingScheme.NO_ESCAPING) { + return v; + } + + String outName; + + if (v.getMetadata().getPrometheusName() == null || isValidLegacyMetricName(v.getMetadata().getPrometheusName())) { + outName = v.getMetadata().getPrometheusName(); + } else { + outName = escapeName(v.getMetadata().getPrometheusName(), scheme); + } + + List outDataPoints = new ArrayList<>(); + + for (DataPointSnapshot d : v.getDataPoints()) { + if (!metricNeedsEscaping(d)) { + outDataPoints.add(d); + continue; + } + + Labels.Builder outLabelsBuilder = Labels.builder(); + + for (Label l : d.getLabels()) { + if (METRIC_NAME_LABEL.equals(l.getName())) { + if (l.getValue() == null || isValidLegacyMetricName(l.getValue())) { + outLabelsBuilder.label(l.getName(), l.getValue()); + continue; + } + outLabelsBuilder.label(l.getName(), escapeName(l.getValue(), scheme)); + continue; + } + if (l.getName() == null || isValidLegacyMetricName(l.getName())) { + outLabelsBuilder.label(l.getName(), l.getValue()); + } + outLabelsBuilder.label(escapeName(l.getName(), scheme), l.getValue()); + } + + Labels outLabels = outLabelsBuilder.build(); + DataPointSnapshot outDataPointSnapshot = null; + + if (v instanceof CounterSnapshot) { + outDataPointSnapshot = CounterSnapshot.CounterDataPointSnapshot.builder() + .value(((CounterSnapshot.CounterDataPointSnapshot) d).getValue()) + .exemplar(((CounterSnapshot.CounterDataPointSnapshot) d).getExemplar()) + .labels(outLabels) + .createdTimestampMillis(d.getCreatedTimestampMillis()) + .scrapeTimestampMillis(d.getScrapeTimestampMillis()) + .build(); + } else if (v instanceof GaugeSnapshot) { + outDataPointSnapshot = GaugeSnapshot.GaugeDataPointSnapshot.builder() + .value(((GaugeSnapshot.GaugeDataPointSnapshot) d).getValue()) + .exemplar(((GaugeSnapshot.GaugeDataPointSnapshot) d).getExemplar()) + .labels(outLabels) + .scrapeTimestampMillis(d.getScrapeTimestampMillis()) + .build(); + } else if (v instanceof HistogramSnapshot) { + outDataPointSnapshot = HistogramSnapshot.HistogramDataPointSnapshot.builder() + .classicHistogramBuckets(((HistogramSnapshot.HistogramDataPointSnapshot) d).getClassicBuckets()) + .nativeSchema(((HistogramSnapshot.HistogramDataPointSnapshot) d).getNativeSchema()) + .nativeZeroCount(((HistogramSnapshot.HistogramDataPointSnapshot) d).getNativeZeroCount()) + .nativeZeroThreshold(((HistogramSnapshot.HistogramDataPointSnapshot) d).getNativeZeroThreshold()) + .nativeBucketsForPositiveValues(((HistogramSnapshot.HistogramDataPointSnapshot) d).getNativeBucketsForPositiveValues()) + .nativeBucketsForNegativeValues(((HistogramSnapshot.HistogramDataPointSnapshot) d).getNativeBucketsForNegativeValues()) + .count(((HistogramSnapshot.HistogramDataPointSnapshot) d).getCount()) + .sum(((HistogramSnapshot.HistogramDataPointSnapshot) d).getSum()) + .exemplars(((HistogramSnapshot.HistogramDataPointSnapshot) d).getExemplars()) + .labels(outLabels) + .createdTimestampMillis(d.getCreatedTimestampMillis()) + .scrapeTimestampMillis(d.getScrapeTimestampMillis()) + .build(); + } else if (v instanceof SummarySnapshot) { + outDataPointSnapshot = SummarySnapshot.SummaryDataPointSnapshot.builder() + .quantiles(((SummarySnapshot.SummaryDataPointSnapshot) d).getQuantiles()) + .count(((SummarySnapshot.SummaryDataPointSnapshot) d).getCount()) + .sum(((SummarySnapshot.SummaryDataPointSnapshot) d).getSum()) + .exemplars(((SummarySnapshot.SummaryDataPointSnapshot) d).getExemplars()) + .labels(outLabels) + .createdTimestampMillis(d.getCreatedTimestampMillis()) + .scrapeTimestampMillis(d.getScrapeTimestampMillis()) + .build(); + } else if (v instanceof InfoSnapshot) { + outDataPointSnapshot = InfoSnapshot.InfoDataPointSnapshot.builder() + .labels(outLabels) + .scrapeTimestampMillis(d.getScrapeTimestampMillis()) + .build(); + } else if (v instanceof StateSetSnapshot) { + StateSetSnapshot.StateSetDataPointSnapshot.Builder builder = StateSetSnapshot.StateSetDataPointSnapshot.builder() + .labels(outLabels) + .scrapeTimestampMillis(d.getScrapeTimestampMillis()); + for (StateSetSnapshot.State state : ((StateSetSnapshot.StateSetDataPointSnapshot) d)) { + builder.state(state.getName(), state.isTrue()); + } + outDataPointSnapshot = builder.build(); + } else if (v instanceof UnknownSnapshot) { + outDataPointSnapshot = UnknownSnapshot.UnknownDataPointSnapshot.builder() + .labels(outLabels) + .value(((UnknownSnapshot.UnknownDataPointSnapshot) d).getValue()) + .exemplar(((UnknownSnapshot.UnknownDataPointSnapshot) d).getExemplar()) + .scrapeTimestampMillis(d.getScrapeTimestampMillis()) + .build(); + } + + outDataPoints.add(outDataPointSnapshot); + } + + MetricSnapshot out; + + if (v instanceof CounterSnapshot) { + CounterSnapshot.Builder builder = CounterSnapshot.builder() + .name(outName) + .help(v.getMetadata().getHelp()) + .unit(v.getMetadata().getUnit()); + for (DataPointSnapshot d : outDataPoints) { + builder.dataPoint((CounterSnapshot.CounterDataPointSnapshot) d); + } + out = builder.build(); + } else if (v instanceof GaugeSnapshot) { + GaugeSnapshot.Builder builder = GaugeSnapshot.builder() + .name(outName) + .help(v.getMetadata().getHelp()) + .unit(v.getMetadata().getUnit()); + for (DataPointSnapshot d : outDataPoints) { + builder.dataPoint((GaugeSnapshot.GaugeDataPointSnapshot) d); + } + out = builder.build(); + } else if (v instanceof HistogramSnapshot) { + HistogramSnapshot.Builder builder = HistogramSnapshot.builder() + .name(outName) + .help(v.getMetadata().getHelp()) + .unit(v.getMetadata().getUnit()) + .gaugeHistogram(((HistogramSnapshot) v).isGaugeHistogram()); + for (DataPointSnapshot d : outDataPoints) { + builder.dataPoint((HistogramSnapshot.HistogramDataPointSnapshot) d); + } + out = builder.build(); + } else if (v instanceof SummarySnapshot) { + SummarySnapshot.Builder builder = SummarySnapshot.builder() + .name(outName) + .help(v.getMetadata().getHelp()) + .unit(v.getMetadata().getUnit()); + for (DataPointSnapshot d : outDataPoints) { + builder.dataPoint((SummarySnapshot.SummaryDataPointSnapshot) d); + } + out = builder.build(); + } else if (v instanceof InfoSnapshot) { + InfoSnapshot.Builder builder = InfoSnapshot.builder() + .name(outName) + .help(v.getMetadata().getHelp()); + for (DataPointSnapshot d : outDataPoints) { + builder.dataPoint((InfoSnapshot.InfoDataPointSnapshot) d); + } + out = builder.build(); + } else if (v instanceof StateSetSnapshot) { + StateSetSnapshot.Builder builder = StateSetSnapshot.builder() + .name(outName) + .help(v.getMetadata().getHelp()); + for (DataPointSnapshot d : outDataPoints) { + builder.dataPoint((StateSetSnapshot.StateSetDataPointSnapshot) d); + } + out = builder.build(); + } else if (v instanceof UnknownSnapshot) { + UnknownSnapshot.Builder builder = UnknownSnapshot.builder() + .name(outName) + .help(v.getMetadata().getHelp()) + .unit(v.getMetadata().getUnit()); + for (DataPointSnapshot d : outDataPoints) { + builder.dataPoint((UnknownSnapshot.UnknownDataPointSnapshot) d); + } + out = builder.build(); + } else { + throw new IllegalArgumentException("Unknown MetricSnapshot type: " + v.getClass()); + } + + return out; + } + + static boolean metricNeedsEscaping(DataPointSnapshot d) { + Labels labels = d.getLabels(); + for (Label l : labels) { + if (l.getName().equals(METRIC_NAME_LABEL) && !isValidLegacyMetricName(l.getValue())) { + return true; + } + if (!isValidLegacyMetricName(l.getName())) { + return true; + } + } + return false; + } + + static String escapeName(String name, EscapingScheme scheme) { + if (name.isEmpty()) { + return name; + } + StringBuilder escaped = new StringBuilder(); + switch (scheme) { + case NO_ESCAPING: + return name; + case UNDERSCORE_ESCAPING: + if (isValidLegacyMetricName(name)) { + return name; + } + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (isValidLegacyChar(c, i)) { + escaped.append(c); + } else { + escaped.append('_'); + } + } + return escaped.toString(); + case DOTS_ESCAPING: + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (c == '_') { + escaped.append("__"); + } else if (c == '.') { + escaped.append("_dot_"); + } else if (isValidLegacyChar(c, i)) { + escaped.append(c); + } else { + escaped.append('_'); + } + } + return escaped.toString(); + case VALUE_ENCODING_ESCAPING: + if (isValidLegacyMetricName(name)) { + return name; + } + escaped.append("U__"); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (isValidLegacyChar(c, i)) { + escaped.append(c); + } else if (!isValidUTF8Byte(c)) { + escaped.append("_FFFD_"); + } else if (c < 0x100) { + // TODO Check if this is ok + escaped.append('_'); + escaped.append(encodeByte(c)); + escaped.append('_'); + } else { + escaped.append('_'); + escaped.append(encodeShort((short) c)); + escaped.append('_'); + } + } + return escaped.toString(); + default: + throw new IllegalArgumentException("Invalid escaping scheme " + scheme); + } + } + + static String unescapeName(String name, EscapingScheme scheme) { + if (name.isEmpty()) { + return name; + } + switch (scheme) { + case NO_ESCAPING: + return name; + case UNDERSCORE_ESCAPING: + // It is not possible to unescape from underscore replacement. + return name; + case DOTS_ESCAPING: + name = name.replaceAll("_dot_", "."); + name = name.replaceAll("__", "_"); + return name; + case VALUE_ENCODING_ESCAPING: + // TODO Check if this is ok + Matcher matcher = Pattern.compile("U__").matcher(name); + if (matcher.find()) { + String escapedName = name.substring(matcher.end()); + StringBuilder unescaped = new StringBuilder(); + TOP: + for (int i = 0; i < escapedName.length(); i++) { + // All non-underscores are treated normally. + if (escapedName.charAt(i) != '_') { + unescaped.append(escapedName.charAt(i)); + continue; + } + i++; + if (i >= escapedName.length()) { + return name; + } + // A double underscore is a single underscore. + if (escapedName.charAt(i) == '_') { + unescaped.append('_'); + continue; + } + // We think we are in a UTF-8 code, process it. + long utf8Val = 0; + for (int j = 0; i < escapedName.length(); j++) { + // This is too many characters for a UTF-8 value. + if (j > 4) { + return name; + } + // Found a closing underscore, convert to a char, check validity, and append. + if (escapedName.charAt(i) == '_') { + char utf8Char = (char) utf8Val; + if (!Character.isDefined(utf8Char)) { + return name; + } + unescaped.append(utf8Char); + continue TOP; + } + char r = Character.toLowerCase(escapedName.charAt(i)); + utf8Val *= 16; + if (r >= '0' && r <= '9') { + utf8Val += r - '0'; + } else if (r >= 'a' && r <= 'f') { + utf8Val += r - 'a' + 10; + } else { + return name; + } + i++; + } + // Didn't find closing underscore, invalid. + return name; + } + return unescaped.toString(); + } else { + return name; + } + default: + throw new IllegalArgumentException("Invalid escaping scheme " + scheme); + } + } + + static boolean isValidLegacyChar(char c, int i) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == ':' || (c >= '0' && c <= '9' && i > 0); + } + + private static boolean isValidUTF8Byte(char b) { + byte[] bytes = Character.toString(b).getBytes(StandardCharsets.UTF_8); + return bytes.length == 1; + } + + private static String encodeByte(char b) { + return encodeShort((short) b); + } + + private static String encodeShort(short b) { + StringBuilder encoded = new StringBuilder(); + for (int s = 12; s >= 0; s -= 4) { + encoded.append(LOWERHEX.charAt((b >> s) & 0xF)); + } + return encoded.toString(); + } } diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/ValidationScheme.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/ValidationScheme.java index 4be8933c4..9f03b9549 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/ValidationScheme.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/ValidationScheme.java @@ -1,6 +1,6 @@ package io.prometheus.metrics.model.snapshots; public enum ValidationScheme { - LegacyValidation, - UTF8Validation + LEGACY_VALIDATION, + UTF_8_VALIDATION } 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 5bf137051..be43416f1 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 @@ -9,7 +9,7 @@ public class PrometheusNamingTest { @Test public void testSanitizeMetricName() { - nameValidationScheme = ValidationScheme.LegacyValidation; + nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; Assert.assertEquals("_abc_def", prometheusName(sanitizeMetricName("0abc.def"))); Assert.assertEquals("___ab_:c0", prometheusName(sanitizeMetricName("___ab.:c0"))); Assert.assertEquals("my_prefix_my_metric", sanitizeMetricName("my_prefix/my_metric")); @@ -22,7 +22,7 @@ public void testSanitizeMetricName() { @Test public void testSanitizeLabelName() { - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; Assert.assertEquals("_abc_def", prometheusName(sanitizeLabelName("0abc.def"))); Assert.assertEquals("_abc", prometheusName(sanitizeLabelName("_abc"))); Assert.assertEquals("_abc", prometheusName(sanitizeLabelName("__abc"))); @@ -34,7 +34,7 @@ public void testSanitizeLabelName() { @Test public void testMetricNameIsValid() { - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; Assert.assertNull(validateMetricName("Avalid_23name")); Assert.assertNull(validateMetricName("_Avalid_23name")); Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("1valid_23name")); @@ -45,7 +45,7 @@ public void testMetricNameIsValid() { Assert.assertNull(validateMetricName("colon:in:the:middle")); Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("")); Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("a\ud800z")); - PrometheusNaming.nameValidationScheme = ValidationScheme.UTF8Validation; + PrometheusNaming.nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; Assert.assertNull(validateMetricName("Avalid_23name")); Assert.assertNull(validateMetricName("_Avalid_23name")); Assert.assertNull(validateMetricName("1valid_23name")); @@ -60,7 +60,7 @@ public void testMetricNameIsValid() { @Test public void testLabelNameIsValid() { - PrometheusNaming.nameValidationScheme = ValidationScheme.LegacyValidation; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; Assert.assertTrue(isValidLabelName("Avalid_23name")); Assert.assertTrue(isValidLabelName("_Avalid_23name")); Assert.assertFalse(isValidLabelName("1valid_23name")); @@ -70,7 +70,7 @@ public void testLabelNameIsValid() { Assert.assertFalse(isValidLabelName(":leading_colon")); Assert.assertFalse(isValidLabelName("colon:in:the:middle")); Assert.assertFalse(isValidLabelName("a\ud800z")); - PrometheusNaming.nameValidationScheme = ValidationScheme.UTF8Validation; + PrometheusNaming.nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; Assert.assertTrue(isValidLabelName("Avalid_23name")); Assert.assertTrue(isValidLabelName("_Avalid_23name")); Assert.assertTrue(isValidLabelName("1valid_23name")); From 2f888f45dc6c681e6214d2f13083a96e9380b56e Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Wed, 28 Feb 2024 10:37:42 -0300 Subject: [PATCH 05/11] Add unit tests for UTF-8 content negotiation Signed-off-by: Federico Torres --- .../metrics/core/metrics/HistogramTest.java | 9 +- .../metrics/core/metrics/InfoTest.java | 3 +- .../ExpositionFormatsTest.java | 116 +++++- .../metrics/instrumentation/jvm/TestUtil.java | 3 +- .../model/snapshots/PrometheusNaming.java | 34 +- .../model/snapshots/PrometheusNamingTest.java | 340 +++++++++++++++++- .../bridge/SimpleclientCollectorTest.java | 3 +- 7 files changed, 458 insertions(+), 50 deletions(-) diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java index 2e0df7b01..d9f1aab36 100644 --- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java +++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java @@ -1,17 +1,12 @@ package io.prometheus.metrics.core.metrics; +import io.prometheus.metrics.model.snapshots.*; import io.prometheus.metrics.shaded.com_google_protobuf_3_21_7.TextFormat; import io.prometheus.metrics.core.datapoints.DistributionDataPoint; import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfigTestUtil; import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; import io.prometheus.metrics.expositionformats.PrometheusProtobufWriter; import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_3_21_7.Metrics; -import io.prometheus.metrics.model.snapshots.ClassicHistogramBucket; -import io.prometheus.metrics.model.snapshots.Exemplar; -import io.prometheus.metrics.model.snapshots.Exemplars; -import io.prometheus.metrics.model.snapshots.HistogramSnapshot; -import io.prometheus.metrics.model.snapshots.Labels; -import io.prometheus.metrics.model.snapshots.MetricSnapshots; import io.prometheus.metrics.tracer.common.SpanContext; import io.prometheus.metrics.tracer.initializer.SpanContextSupplier; import org.junit.After; @@ -723,7 +718,7 @@ public void testDefaults() throws IOException { // text ByteArrayOutputStream out = new ByteArrayOutputStream(); OpenMetricsTextFormatWriter writer = new OpenMetricsTextFormatWriter(false, true); - writer.write(out, MetricSnapshots.of(snapshot)); + writer.write(out, MetricSnapshots.of(snapshot), EscapingScheme.NO_ESCAPING); Assert.assertEquals(expectedTextFormat, out.toString()); } diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/InfoTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/InfoTest.java index f88c98d7c..6c7ed3a6f 100644 --- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/InfoTest.java +++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/InfoTest.java @@ -1,6 +1,7 @@ package io.prometheus.metrics.core.metrics; import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.model.snapshots.EscapingScheme; import io.prometheus.metrics.model.snapshots.Labels; import io.prometheus.metrics.model.snapshots.MetricSnapshots; import io.prometheus.metrics.shaded.com_google_protobuf_3_21_7.TextFormat; @@ -98,7 +99,7 @@ public void testConstLabelsDuplicate2() { private void assertTextFormat(String expected, Info info) throws IOException { OpenMetricsTextFormatWriter writer = new OpenMetricsTextFormatWriter(true, true); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - writer.write(outputStream, MetricSnapshots.of(info.collect())); + writer.write(outputStream, MetricSnapshots.of(info.collect()), EscapingScheme.NO_ESCAPING); String result = outputStream.toString(StandardCharsets.UTF_8.name()); if (!result.contains(expected)) { throw new AssertionError(expected + " is not contained in the following output:\n" + result); diff --git a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java index 23af472e6..1e5e1ade9 100644 --- a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java +++ b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java @@ -12,6 +12,9 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.nameEscapingScheme; public class ExpositionFormatsTest { @@ -373,6 +376,8 @@ public void testGaugeUTF8() throws IOException { .build()) .build(); assertPrometheusText(prometheusText, gauge); + + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; } @Test @@ -1837,31 +1842,134 @@ public void testLabelValueEscape() throws IOException { assertPrometheusText(prometheus, counter); } + @Test + public void testFindWriter() { + EscapingScheme oldDefault = nameEscapingScheme; + nameEscapingScheme = EscapingScheme.UNDERSCORE_ESCAPING; + ExpositionFormats expositionFormats = ExpositionFormats.init(); + + // delimited format + String acceptHeaderValue = "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited"; + String expectedFmt = "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=underscores"; + EscapingScheme escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeaderValue); + ExpositionFormatWriter writer = expositionFormats.findWriter(acceptHeaderValue); + Assert.assertEquals(expectedFmt, writer.getContentType() + escapingScheme.toHeaderFormat()); + + // plain text format + acceptHeaderValue = "text/plain;version=0.0.4"; + expectedFmt = "text/plain; version=0.0.4; charset=utf-8; escaping=underscores"; + escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeaderValue); + writer = expositionFormats.findWriter(acceptHeaderValue); + Assert.assertEquals(expectedFmt, writer.getContentType() + escapingScheme.toHeaderFormat()); + + // delimited format UTF-8 + acceptHeaderValue = "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited; escaping=allow-utf-8"; + expectedFmt = "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=allow-utf-8"; + escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeaderValue); + writer = expositionFormats.findWriter(acceptHeaderValue); + Assert.assertEquals(expectedFmt, writer.getContentType() + escapingScheme.toHeaderFormat()); + + // TODO review if this is ok + nameEscapingScheme = EscapingScheme.VALUE_ENCODING_ESCAPING; + + // OM format, no version + acceptHeaderValue = "application/openmetrics-text"; + expectedFmt = "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=values"; + escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeaderValue); + writer = expositionFormats.findWriter(acceptHeaderValue); + Assert.assertEquals(expectedFmt, writer.getContentType() + escapingScheme.toHeaderFormat()); + + // OM format, 0.0.1 version + acceptHeaderValue = "application/openmetrics-text;version=0.0.1; escaping=underscores"; + expectedFmt = "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=underscores"; + escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeaderValue); + writer = expositionFormats.findWriter(acceptHeaderValue); + Assert.assertEquals(expectedFmt, writer.getContentType() + escapingScheme.toHeaderFormat()); + + // plain text format + acceptHeaderValue = "text/plain;version=0.0.4"; + expectedFmt = "text/plain; version=0.0.4; charset=utf-8; escaping=values"; + escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeaderValue); + writer = expositionFormats.findWriter(acceptHeaderValue); + Assert.assertEquals(expectedFmt, writer.getContentType() + escapingScheme.toHeaderFormat()); + + // plain text format UTF-8 + acceptHeaderValue = "text/plain;version=0.0.4; escaping=allow-utf-8"; + expectedFmt = "text/plain; version=0.0.4; charset=utf-8; escaping=allow-utf-8"; + escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeaderValue); + writer = expositionFormats.findWriter(acceptHeaderValue); + Assert.assertEquals(expectedFmt, writer.getContentType() + escapingScheme.toHeaderFormat()); + + // delimited format UTF-8 + acceptHeaderValue = "text/plain;version=0.0.4; escaping=allow-utf-8"; + expectedFmt = "text/plain; version=0.0.4; charset=utf-8; escaping=allow-utf-8"; + escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeaderValue); + writer = expositionFormats.findWriter(acceptHeaderValue); + Assert.assertEquals(expectedFmt, writer.getContentType() + escapingScheme.toHeaderFormat()); + + nameEscapingScheme = oldDefault; + } + + @Test + public void testWrite() throws IOException { + ByteArrayOutputStream buff = new ByteArrayOutputStream(new AtomicInteger(2 << 9).get() + 1024); + ExpositionFormats expositionFormats = ExpositionFormats.init(); + UnknownSnapshot unknown = UnknownSnapshot.builder() + .name("foo_metric") + .dataPoint(UnknownDataPointSnapshot.builder() + .value(1.234) + .build()) + .build(); + + String acceptHeaderValue = "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited"; + EscapingScheme escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeaderValue); + ExpositionFormatWriter protoWriter = expositionFormats.findWriter(acceptHeaderValue); + + protoWriter.write(buff, MetricSnapshots.of(unknown), escapingScheme); + byte[] out = buff.toByteArray(); + Assert.assertNotEquals(0, out.length); + + buff.reset(); + + acceptHeaderValue = "text/plain; version=0.0.4; charset=utf-8"; + escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeaderValue); + ExpositionFormatWriter textWriter = expositionFormats.findWriter(acceptHeaderValue); + + textWriter.write(buff, MetricSnapshots.of(unknown), escapingScheme); + out = buff.toByteArray(); + Assert.assertNotEquals(0, out.length); + + String expected = "# TYPE foo_metric untyped\n" + + "foo_metric 1.234\n"; + + Assert.assertEquals(expected, new String(out)); + } + private void assertOpenMetricsText(String expected, MetricSnapshot snapshot) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); OpenMetricsTextFormatWriter writer = new OpenMetricsTextFormatWriter(true, true); - writer.write(out, MetricSnapshots.of(snapshot)); + writer.write(out, MetricSnapshots.of(snapshot), EscapingScheme.NO_ESCAPING); Assert.assertEquals(expected, out.toString()); } private void assertOpenMetricsTextWithoutCreated(String expected, MetricSnapshot snapshot) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); OpenMetricsTextFormatWriter writer = new OpenMetricsTextFormatWriter(false, true); - writer.write(out, MetricSnapshots.of(snapshot)); + writer.write(out, MetricSnapshots.of(snapshot), EscapingScheme.NO_ESCAPING); Assert.assertEquals(expected, out.toString()); } private void assertPrometheusText(String expected, MetricSnapshot snapshot) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); PrometheusTextFormatWriter writer = new PrometheusTextFormatWriter(true); - writer.write(out, MetricSnapshots.of(snapshot)); + writer.write(out, MetricSnapshots.of(snapshot), EscapingScheme.NO_ESCAPING); Assert.assertEquals(expected, out.toString()); } private void assertPrometheusTextWithoutCreated(String expected, MetricSnapshot snapshot) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); PrometheusTextFormatWriter writer = new PrometheusTextFormatWriter(false); - writer.write(out, MetricSnapshots.of(snapshot)); + writer.write(out, MetricSnapshots.of(snapshot), EscapingScheme.NO_ESCAPING); Assert.assertEquals(expected, out.toString()); } diff --git a/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/TestUtil.java b/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/TestUtil.java index 2b0d7972c..a97e42736 100644 --- a/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/TestUtil.java +++ b/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/TestUtil.java @@ -1,6 +1,7 @@ package io.prometheus.metrics.instrumentation.jvm; import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.model.snapshots.EscapingScheme; import io.prometheus.metrics.model.snapshots.MetricSnapshots; import java.io.ByteArrayOutputStream; @@ -12,7 +13,7 @@ public class TestUtil { static String convertToOpenMetricsFormat(MetricSnapshots snapshots) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); OpenMetricsTextFormatWriter writer = new OpenMetricsTextFormatWriter(true, true); - writer.write(out, snapshots); + writer.write(out, snapshots, EscapingScheme.NO_ESCAPING); return out.toString(StandardCharsets.UTF_8.name()); } } 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 740fcfdc7..fe325bef2 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 @@ -6,6 +6,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static java.lang.Character.MAX_LOW_SURROGATE; +import static java.lang.Character.MIN_HIGH_SURROGATE; + /** * Utility for Prometheus Metric and Label naming. *

@@ -281,6 +284,7 @@ public static MetricSnapshot escapeMetricSnapshot(MetricSnapshot v, EscapingSche } if (l.getName() == null || isValidLegacyMetricName(l.getName())) { outLabelsBuilder.label(l.getName(), l.getValue()); + continue; } outLabelsBuilder.label(escapeName(l.getName(), scheme), l.getValue()); } @@ -481,16 +485,20 @@ static String escapeName(String name, EscapingScheme scheme) { char c = name.charAt(i); if (isValidLegacyChar(c, i)) { escaped.append(c); - } else if (!isValidUTF8Byte(c)) { + } else if (!isValidUTF8Char(c)) { escaped.append("_FFFD_"); } else if (c < 0x100) { // TODO Check if this is ok escaped.append('_'); - escaped.append(encodeByte(c)); + for (int s = 4; s >= 0; s -= 4) { + escaped.append(LOWERHEX.charAt((c >> s) & 0xF)); + } escaped.append('_'); } else { escaped.append('_'); - escaped.append(encodeShort((short) c)); + for (int s = 12; s >= 0; s -= 4) { + escaped.append(LOWERHEX.charAt((c >> s) & 0xF)); + } escaped.append('_'); } } @@ -546,7 +554,7 @@ static String unescapeName(String name, EscapingScheme scheme) { // Found a closing underscore, convert to a char, check validity, and append. if (escapedName.charAt(i) == '_') { char utf8Char = (char) utf8Val; - if (!Character.isDefined(utf8Char)) { + if (!isValidUTF8Char(utf8Char)) { return name; } unescaped.append(utf8Char); @@ -579,20 +587,8 @@ static boolean isValidLegacyChar(char c, int i) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == ':' || (c >= '0' && c <= '9' && i > 0); } - private static boolean isValidUTF8Byte(char b) { - byte[] bytes = Character.toString(b).getBytes(StandardCharsets.UTF_8); - return bytes.length == 1; - } - - private static String encodeByte(char b) { - return encodeShort((short) b); - } - - private static String encodeShort(short b) { - StringBuilder encoded = new StringBuilder(); - for (int s = 12; s >= 0; s -= 4) { - encoded.append(LOWERHEX.charAt((b >> s) & 0xF)); - } - return encoded.toString(); + private static boolean isValidUTF8Char(char b) { + return ((b < MIN_HIGH_SURROGATE || b > MAX_LOW_SURROGATE) && + (b < 0xFFFE)); } } 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 be43416f1..16d66954e 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 @@ -34,24 +34,24 @@ public void testSanitizeLabelName() { @Test public void testMetricNameIsValid() { - PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; + nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; Assert.assertNull(validateMetricName("Avalid_23name")); Assert.assertNull(validateMetricName("_Avalid_23name")); - Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("1valid_23name")); + Assert.assertNull(validateMetricName("1valid_23name")); Assert.assertNull(validateMetricName("avalid_23name")); Assert.assertNull(validateMetricName("Ava:lid_23name")); - Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("a lid_23name")); + Assert.assertNull(validateMetricName("a lid_23name")); Assert.assertNull(validateMetricName(":leading_colon")); Assert.assertNull(validateMetricName("colon:in:the:middle")); Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("")); Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("a\ud800z")); - PrometheusNaming.nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; + nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; Assert.assertNull(validateMetricName("Avalid_23name")); Assert.assertNull(validateMetricName("_Avalid_23name")); - Assert.assertNull(validateMetricName("1valid_23name")); + Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("1valid_23name")); Assert.assertNull(validateMetricName("avalid_23name")); Assert.assertNull(validateMetricName("Ava:lid_23name")); - Assert.assertNull(validateMetricName("a lid_23name")); + Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("a lid_23name")); Assert.assertNull(validateMetricName(":leading_colon")); Assert.assertNull(validateMetricName("colon:in:the:middle")); Assert.assertEquals("The metric name contains unsupported characters", validateMetricName("")); @@ -60,17 +60,7 @@ public void testMetricNameIsValid() { @Test public void testLabelNameIsValid() { - PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; - Assert.assertTrue(isValidLabelName("Avalid_23name")); - Assert.assertTrue(isValidLabelName("_Avalid_23name")); - Assert.assertFalse(isValidLabelName("1valid_23name")); - Assert.assertTrue(isValidLabelName("avalid_23name")); - Assert.assertFalse(isValidLabelName("Ava:lid_23name")); - Assert.assertFalse(isValidLabelName("a lid_23name")); - Assert.assertFalse(isValidLabelName(":leading_colon")); - Assert.assertFalse(isValidLabelName("colon:in:the:middle")); - Assert.assertFalse(isValidLabelName("a\ud800z")); - PrometheusNaming.nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; + nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; Assert.assertTrue(isValidLabelName("Avalid_23name")); Assert.assertTrue(isValidLabelName("_Avalid_23name")); Assert.assertTrue(isValidLabelName("1valid_23name")); @@ -80,5 +70,321 @@ public void testLabelNameIsValid() { Assert.assertTrue(isValidLabelName(":leading_colon")); Assert.assertTrue(isValidLabelName("colon:in:the:middle")); Assert.assertFalse(isValidLabelName("a\ud800z")); + nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; + Assert.assertTrue(isValidLabelName("Avalid_23name")); + Assert.assertTrue(isValidLabelName("_Avalid_23name")); + Assert.assertFalse(isValidLabelName("1valid_23name")); + Assert.assertTrue(isValidLabelName("avalid_23name")); + Assert.assertFalse(isValidLabelName("Ava:lid_23name")); + Assert.assertFalse(isValidLabelName("a lid_23name")); + Assert.assertFalse(isValidLabelName(":leading_colon")); + Assert.assertFalse(isValidLabelName("colon:in:the:middle")); + Assert.assertFalse(isValidLabelName("a\ud800z")); + } + + @Test + public void testEscapeName() { + nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; + + // empty string + String got = escapeName("", EscapingScheme.UNDERSCORE_ESCAPING); + Assert.assertEquals("", got); + got = unescapeName(got, EscapingScheme.UNDERSCORE_ESCAPING); + Assert.assertEquals("", got); + + got = escapeName("", EscapingScheme.DOTS_ESCAPING); + Assert.assertEquals("", got); + got = unescapeName(got, EscapingScheme.DOTS_ESCAPING); + Assert.assertEquals("", got); + + got = escapeName("", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("", got); + got = unescapeName(got, EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("", got); + + // legacy valid name + got = escapeName("no:escaping_required", EscapingScheme.UNDERSCORE_ESCAPING); + Assert.assertEquals("no:escaping_required", got); + got = unescapeName(got, EscapingScheme.UNDERSCORE_ESCAPING); + Assert.assertEquals("no:escaping_required", got); + + got = escapeName("no:escaping_required", EscapingScheme.DOTS_ESCAPING); + // Dots escaping will escape underscores even though it's not strictly + // necessary for compatibility. + Assert.assertEquals("no:escaping__required", got); + got = unescapeName(got, EscapingScheme.DOTS_ESCAPING); + Assert.assertEquals("no:escaping_required", got); + + got = escapeName("no:escaping_required", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("no:escaping_required", got); + got = unescapeName(got, EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("no:escaping_required", got); + + // TODO should add test for legacy validation? + // name with dots + got = escapeName("mysystem.prod.west.cpu.load", EscapingScheme.UNDERSCORE_ESCAPING); + Assert.assertEquals("mysystem_prod_west_cpu_load", got); + got = unescapeName(got, EscapingScheme.UNDERSCORE_ESCAPING); + Assert.assertEquals("mysystem_prod_west_cpu_load", got); + + got = escapeName("mysystem.prod.west.cpu.load", EscapingScheme.DOTS_ESCAPING); + Assert.assertEquals("mysystem_dot_prod_dot_west_dot_cpu_dot_load", got); + got = unescapeName(got, EscapingScheme.DOTS_ESCAPING); + Assert.assertEquals("mysystem.prod.west.cpu.load", got); + + got = escapeName("mysystem.prod.west.cpu.load", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("U__mysystem_2e_prod_2e_west_2e_cpu_2e_load", got); + got = unescapeName(got, EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("mysystem.prod.west.cpu.load", got); + + // name with dots and colon + got = escapeName("http.status:sum", EscapingScheme.UNDERSCORE_ESCAPING); + Assert.assertEquals("http_status:sum", got); + got = unescapeName(got, EscapingScheme.UNDERSCORE_ESCAPING); + Assert.assertEquals("http_status:sum", got); + + got = escapeName("http.status:sum", EscapingScheme.DOTS_ESCAPING); + Assert.assertEquals("http_dot_status:sum", got); + got = unescapeName(got, EscapingScheme.DOTS_ESCAPING); + Assert.assertEquals("http.status:sum", got); + + got = escapeName("http.status:sum", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("U__http_2e_status:sum", got); + got = unescapeName(got, EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("http.status:sum", got); + + // name with unicode characters > 0x100 + got = escapeName("花火", EscapingScheme.UNDERSCORE_ESCAPING); + Assert.assertEquals("__", got); + got = unescapeName(got, EscapingScheme.UNDERSCORE_ESCAPING); + Assert.assertEquals("__", got); + + got = escapeName("花火", EscapingScheme.DOTS_ESCAPING); + Assert.assertEquals("__", got); + got = unescapeName(got, EscapingScheme.DOTS_ESCAPING); + // Dots-replacement does not know the difference between two replaced + // characters and a single underscore. + Assert.assertEquals("_", got); + + got = escapeName("花火", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("U___82b1__706b_", got); + got = unescapeName(got, EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("花火", got); + + nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; + } + + @Test + public void testValueUnescapeErrors() { + nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; + String got; + + // empty string + got = unescapeName("", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("", got); + + // basic case, no error + got = unescapeName("U__no:unescapingrequired", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("no:unescapingrequired", got); + + // capitals ok, no error + got = unescapeName("U__capitals_2E_ok", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("capitals.ok", got); + + // underscores, no error + got = unescapeName("U__underscores__doubled__", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("underscores_doubled_", got); + + // invalid single underscore + got = unescapeName("U__underscores_doubled_", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("U__underscores_doubled_", got); + + // invalid single underscore, 2 + got = unescapeName("U__underscores__doubled_", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("U__underscores__doubled_", got); + + // giant fake UTF-8 code + got = unescapeName("U__my__hack_2e_attempt_872348732fabdabbab_", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("U__my__hack_2e_attempt_872348732fabdabbab_", got); + + // trailing UTF-8 + got = unescapeName("U__my__hack_2e", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("U__my__hack_2e", got); + + // invalid UTF-8 value + got = unescapeName("U__bad__utf_2eg_", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("U__bad__utf_2eg_", got); + + // surrogate UTF-8 value + got = unescapeName("U__bad__utf_D900_", EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("U__bad__utf_D900_", got); + + nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; + } + + @Test + public void testEscapeMetricSnapshotEmpty() { + nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; + MetricSnapshot original = CounterSnapshot.builder().name("empty").build(); + MetricSnapshot got = escapeMetricSnapshot(original, EscapingScheme.VALUE_ENCODING_ESCAPING); + Assert.assertEquals("empty", got.getMetadata().getName()); + Assert.assertEquals("empty", original.getMetadata().getName()); + nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; + } + + @Test + public void testEscapeMetricSnapshotSimpleNoEscapingNeeded() { + nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; + MetricSnapshot original = CounterSnapshot.builder() + .name("my_metric") + .help("some help text") + .dataPoint(CounterSnapshot.CounterDataPointSnapshot.builder() + .value(34.2) + .labels(Labels.builder() + .label("__name__", "my_metric") + .label("some_label", "labelvalue") + .build()) + .build() + ) + .build(); + MetricSnapshot got = escapeMetricSnapshot(original, EscapingScheme.VALUE_ENCODING_ESCAPING); + + Assert.assertEquals("my_metric", got.getMetadata().getName()); + Assert.assertEquals("some help text", got.getMetadata().getHelp()); + Assert.assertEquals(1, got.getDataPoints().size()); + CounterSnapshot.CounterDataPointSnapshot data = (CounterSnapshot.CounterDataPointSnapshot) got.getDataPoints().get(0); + Assert.assertEquals(34.2, data.getValue(), 0.0); + Assert.assertEquals(Labels.builder() + .label("__name__", "my_metric") + .label("some_label", "labelvalue") + .build(), data.getLabels()); + Assert.assertEquals("my_metric", original.getMetadata().getName()); + Assert.assertEquals("some help text", original.getMetadata().getHelp()); + Assert.assertEquals(1, original.getDataPoints().size()); + data = (CounterSnapshot.CounterDataPointSnapshot) original.getDataPoints().get(0); + Assert.assertEquals(34.2, data.getValue(), 0.0); + Assert.assertEquals(Labels.builder() + .label("__name__", "my_metric") + .label("some_label", "labelvalue") + .build(), data.getLabels()); + + nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; + } + + @Test + public void testEscapeMetricSnapshotLabelNameEscapingNeeded() { + nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; + MetricSnapshot original = CounterSnapshot.builder() + .name("my_metric") + .help("some help text") + .dataPoint(CounterSnapshot.CounterDataPointSnapshot.builder() + .value(34.2) + .labels(Labels.builder() + .label("__name__", "my_metric") + .label("some.label", "labelvalue") + .build()) + .build() + ) + .build(); + MetricSnapshot got = escapeMetricSnapshot(original, EscapingScheme.VALUE_ENCODING_ESCAPING); + + Assert.assertEquals("my_metric", got.getMetadata().getName()); + Assert.assertEquals("some help text", got.getMetadata().getHelp()); + Assert.assertEquals(1, got.getDataPoints().size()); + CounterSnapshot.CounterDataPointSnapshot data = (CounterSnapshot.CounterDataPointSnapshot) got.getDataPoints().get(0); + Assert.assertEquals(34.2, data.getValue(), 0.0); + Assert.assertEquals(Labels.builder() + .label("__name__", "my_metric") + .label("U__some_2e_label", "labelvalue") + .build(), data.getLabels()); + Assert.assertEquals("my_metric", original.getMetadata().getName()); + Assert.assertEquals("some help text", original.getMetadata().getHelp()); + Assert.assertEquals(1, original.getDataPoints().size()); + data = (CounterSnapshot.CounterDataPointSnapshot) original.getDataPoints().get(0); + Assert.assertEquals(34.2, data.getValue(), 0.0); + Assert.assertEquals(Labels.builder() + .label("__name__", "my_metric") + .label("some.label", "labelvalue") + .build(), data.getLabels()); + + nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; + } + + @Test + public void testEscapeMetricSnapshotCounterEscapingNeeded() { + nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; + MetricSnapshot original = CounterSnapshot.builder() + .name("my.metric") + .help("some help text") + .dataPoint(CounterSnapshot.CounterDataPointSnapshot.builder() + .value(34.2) + .labels(Labels.builder() + .label("__name__", "my.metric") + .label("some?label", "label??value") + .build()) + .build() + ) + .build(); + MetricSnapshot got = escapeMetricSnapshot(original, EscapingScheme.VALUE_ENCODING_ESCAPING); + + Assert.assertEquals("U__my_2e_metric", got.getMetadata().getName()); + Assert.assertEquals("some help text", got.getMetadata().getHelp()); + Assert.assertEquals(1, got.getDataPoints().size()); + CounterSnapshot.CounterDataPointSnapshot data = (CounterSnapshot.CounterDataPointSnapshot) got.getDataPoints().get(0); + Assert.assertEquals(34.2, data.getValue(), 0.0); + Assert.assertEquals(Labels.builder() + .label("__name__", "U__my_2e_metric") + .label("U__some_3f_label", "label??value") + .build(), data.getLabels()); + Assert.assertEquals("my.metric", original.getMetadata().getName()); + Assert.assertEquals("some help text", original.getMetadata().getHelp()); + Assert.assertEquals(1, original.getDataPoints().size()); + data = (CounterSnapshot.CounterDataPointSnapshot) original.getDataPoints().get(0); + Assert.assertEquals(34.2, data.getValue(), 0.0); + Assert.assertEquals(Labels.builder() + .label("__name__", "my.metric") + .label("some?label", "label??value") + .build(), data.getLabels()); + + nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; + } + + @Test + public void testEscapeMetricSnapshotGaugeEscapingNeeded() { + nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; + MetricSnapshot original = GaugeSnapshot.builder() + .name("unicode.and.dots.花火") + .help("some help text") + .dataPoint(GaugeSnapshot.GaugeDataPointSnapshot.builder() + .value(34.2) + .labels(Labels.builder() + .label("__name__", "unicode.and.dots.花火") + .label("some_label", "label??value") + .build()) + .build() + ) + .build(); + MetricSnapshot got = escapeMetricSnapshot(original, EscapingScheme.DOTS_ESCAPING); + + Assert.assertEquals("unicode_dot_and_dot_dots_dot___", got.getMetadata().getName()); + Assert.assertEquals("some help text", got.getMetadata().getHelp()); + Assert.assertEquals(1, got.getDataPoints().size()); + GaugeSnapshot.GaugeDataPointSnapshot data = (GaugeSnapshot.GaugeDataPointSnapshot) got.getDataPoints().get(0); + Assert.assertEquals(34.2, data.getValue(), 0.0); + Assert.assertEquals(Labels.builder() + .label("__name__", "unicode_dot_and_dot_dots_dot___") + .label("some_label", "label??value") + .build(), data.getLabels()); + Assert.assertEquals("unicode.and.dots.花火", original.getMetadata().getName()); + Assert.assertEquals("some help text", original.getMetadata().getHelp()); + Assert.assertEquals(1, original.getDataPoints().size()); + data = (GaugeSnapshot.GaugeDataPointSnapshot) original.getDataPoints().get(0); + Assert.assertEquals(34.2, data.getValue(), 0.0); + Assert.assertEquals(Labels.builder() + .label("__name__", "unicode.and.dots.花火") + .label("some_label", "label??value") + .build(), data.getLabels()); + + nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; } } diff --git a/prometheus-metrics-simpleclient-bridge/src/test/java/io/prometheus/metrics/simpleclient/bridge/SimpleclientCollectorTest.java b/prometheus-metrics-simpleclient-bridge/src/test/java/io/prometheus/metrics/simpleclient/bridge/SimpleclientCollectorTest.java index 9d1e8f1b9..c3a5fa626 100644 --- a/prometheus-metrics-simpleclient-bridge/src/test/java/io/prometheus/metrics/simpleclient/bridge/SimpleclientCollectorTest.java +++ b/prometheus-metrics-simpleclient-bridge/src/test/java/io/prometheus/metrics/simpleclient/bridge/SimpleclientCollectorTest.java @@ -10,6 +10,7 @@ import io.prometheus.client.exporter.common.TextFormat; import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.model.snapshots.EscapingScheme; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -243,7 +244,7 @@ private String origOpenMetrics() throws IOException { private String newOpenMetrics() throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); OpenMetricsTextFormatWriter writer = new OpenMetricsTextFormatWriter(true, true); - writer.write(out, newRegistry.scrape()); + writer.write(out, newRegistry.scrape(), EscapingScheme.NO_ESCAPING); return out.toString(StandardCharsets.UTF_8.name()); } } From a1b19ef85bf13386d15a8540981c124d864a7cc1 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Wed, 6 Mar 2024 13:27:55 -0300 Subject: [PATCH 06/11] Refactor name validation scheme config Signed-off-by: Federico Torres --- .../metrics/config/NamingProperties.java | 43 +++++++++++++++++++ .../metrics/config/PrometheusProperties.java | 9 +++- .../config/PrometheusPropertiesLoader.java | 3 +- prometheus-metrics-model/pom.xml | 6 +++ .../model/snapshots/PrometheusNaming.java | 16 +++++-- .../src/test/resources/prometheus.properties | 1 + 6 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/NamingProperties.java create mode 100644 prometheus-metrics-model/src/test/resources/prometheus.properties diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/NamingProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/NamingProperties.java new file mode 100644 index 000000000..7562246b7 --- /dev/null +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/NamingProperties.java @@ -0,0 +1,43 @@ +package io.prometheus.metrics.config; + +import java.util.Map; + +public class NamingProperties { + + private static final String VALIDATION_SCHEME = "validationScheme"; + private final String validationScheme; + + private NamingProperties(String validation) { + this.validationScheme = validation; + } + + public String getValidationScheme() { + return validationScheme; + } + + static NamingProperties load(String prefix, Map properties) throws PrometheusPropertiesException { + String validationScheme = Util.loadString(prefix + "." + VALIDATION_SCHEME, properties); + return new NamingProperties(validationScheme); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String validationScheme; + + private Builder() {} + + public Builder validation(String validationScheme) { + this.validationScheme = validationScheme; + return this; + } + + public NamingProperties build() { + return new NamingProperties(validationScheme); + } + } + +} diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java index 4707dd862..6cce9bb99 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java @@ -19,6 +19,7 @@ public class PrometheusProperties { private final ExporterFilterProperties exporterFilterProperties; private final ExporterHttpServerProperties exporterHttpServerProperties; private final ExporterOpenTelemetryProperties exporterOpenTelemetryProperties; + private final NamingProperties namingProperties; /** * Get the properties instance. When called for the first time, {@code get()} loads the properties from the following locations: @@ -39,7 +40,8 @@ public PrometheusProperties( ExporterProperties exporterProperties, ExporterFilterProperties exporterFilterProperties, ExporterHttpServerProperties httpServerConfig, - ExporterOpenTelemetryProperties otelConfig) { + ExporterOpenTelemetryProperties otelConfig, + NamingProperties namingProperties) { this.defaultMetricsProperties = defaultMetricsProperties; this.metricProperties.putAll(metricProperties); this.exemplarProperties = exemplarProperties; @@ -47,6 +49,7 @@ public PrometheusProperties( this.exporterFilterProperties = exporterFilterProperties; this.exporterHttpServerProperties = httpServerConfig; this.exporterOpenTelemetryProperties = otelConfig; + this.namingProperties = namingProperties; } /** @@ -83,4 +86,8 @@ public ExporterHttpServerProperties getExporterHttpServerProperties() { public ExporterOpenTelemetryProperties getExporterOpenTelemetryProperties() { return exporterOpenTelemetryProperties; } + + public NamingProperties getNamingProperties() { + return namingProperties; + } } diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java index c4a09be7b..39fe30e8e 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java @@ -33,8 +33,9 @@ public static PrometheusProperties load() throws PrometheusPropertiesException { ExporterFilterProperties exporterFilterProperties = ExporterFilterProperties.load("io.prometheus.exporter.filter", properties); ExporterHttpServerProperties exporterHttpServerProperties = ExporterHttpServerProperties.load("io.prometheus.exporter.httpServer", properties); ExporterOpenTelemetryProperties exporterOpenTelemetryProperties = ExporterOpenTelemetryProperties.load("io.prometheus.exporter.opentelemetry", properties); + NamingProperties namingProperties = NamingProperties.load("io.prometheus.naming", properties); validateAllPropertiesProcessed(properties); - return new PrometheusProperties(defaultMetricsProperties, metricsConfigs, exemplarConfig, exporterProperties, exporterFilterProperties, exporterHttpServerProperties, exporterOpenTelemetryProperties); + return new PrometheusProperties(defaultMetricsProperties, metricsConfigs, exemplarConfig, exporterProperties, exporterFilterProperties, exporterHttpServerProperties, exporterOpenTelemetryProperties, namingProperties); } // This will remove entries from properties when they are processed. diff --git a/prometheus-metrics-model/pom.xml b/prometheus-metrics-model/pom.xml index f45d5b6fb..0e53a30e1 100644 --- a/prometheus-metrics-model/pom.xml +++ b/prometheus-metrics-model/pom.xml @@ -37,6 +37,12 @@ + + io.prometheus + prometheus-metrics-config + ${project.version} + + junit 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 fe325bef2..0464ba1f7 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 @@ -1,5 +1,7 @@ package io.prometheus.metrics.model.snapshots; +import io.prometheus.metrics.config.PrometheusProperties; + import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -16,7 +18,7 @@ * in Prometheus exposition formats. However, if metrics are exposed in OpenTelemetry format the dots are retained. */ public class PrometheusNaming { - public static ValidationScheme nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; + public static ValidationScheme nameValidationScheme = initValidationScheme(); public static EscapingScheme nameEscapingScheme = EscapingScheme.VALUE_ENCODING_ESCAPING; @@ -81,6 +83,16 @@ public static boolean isValidMetricName(String name) { return validateMetricName(name) == null; } + static ValidationScheme initValidationScheme() { + if (PrometheusProperties.get() != null && PrometheusProperties.get().getNamingProperties() != null) { + String validationScheme = PrometheusProperties.get().getNamingProperties().getValidationScheme(); + if (validationScheme != null && validationScheme.equals("utf-8")) { + return ValidationScheme.UTF_8_VALIDATION; + } + } + return ValidationScheme.LEGACY_VALIDATION; + } + static String validateMetricName(String name) { switch (nameValidationScheme) { case LEGACY_VALIDATION: @@ -488,7 +500,6 @@ static String escapeName(String name, EscapingScheme scheme) { } else if (!isValidUTF8Char(c)) { escaped.append("_FFFD_"); } else if (c < 0x100) { - // TODO Check if this is ok escaped.append('_'); for (int s = 4; s >= 0; s -= 4) { escaped.append(LOWERHEX.charAt((c >> s) & 0xF)); @@ -523,7 +534,6 @@ static String unescapeName(String name, EscapingScheme scheme) { name = name.replaceAll("__", "_"); return name; case VALUE_ENCODING_ESCAPING: - // TODO Check if this is ok Matcher matcher = Pattern.compile("U__").matcher(name); if (matcher.find()) { String escapedName = name.substring(matcher.end()); diff --git a/prometheus-metrics-model/src/test/resources/prometheus.properties b/prometheus-metrics-model/src/test/resources/prometheus.properties new file mode 100644 index 000000000..4ce7f8487 --- /dev/null +++ b/prometheus-metrics-model/src/test/resources/prometheus.properties @@ -0,0 +1 @@ +io.prometheus.naming.validationScheme=legacy From 1956a75fade96ed374ad9e6aaabeffe950cdaab5 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Wed, 6 Mar 2024 17:03:04 -0300 Subject: [PATCH 07/11] Add some comments Signed-off-by: Federico Torres --- .../OpenMetricsTextFormatWriter.java | 2 ++ .../PrometheusTextFormatWriter.java | 2 ++ .../model/snapshots/EscapingScheme.java | 15 ++++++++++++ .../model/snapshots/PrometheusNaming.java | 24 +++++++++++++++++++ .../model/snapshots/ValidationScheme.java | 7 ++++++ 5 files changed, 50 insertions(+) diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java index 020833dbd..0239397be 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java @@ -272,6 +272,8 @@ private void writeNameAndLabels(OutputStreamWriter writer, String name, String s private void writeNameAndLabels(OutputStreamWriter writer, String name, String suffix, Labels labels, String additionalLabelName, double additionalLabelValue) throws IOException { boolean metricInsideBraces = false; + // If the name does not pass the legacy validity check, we must put the + // metric name inside the braces. if (PrometheusNaming.validateLegacyMetricName(name) != null) { metricInsideBraces = true; writer.write('{'); diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java index c624510a4..631c1f0f3 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java @@ -278,6 +278,8 @@ private void writeNameAndLabels(OutputStreamWriter writer, String name, String s private void writeNameAndLabels(OutputStreamWriter writer, String name, String suffix, Labels labels, String additionalLabelName, double additionalLabelValue) throws IOException { boolean metricInsideBraces = false; + // If the name does not pass the legacy validity check, we must put the + // metric name inside the braces. if (PrometheusNaming.validateLegacyMetricName(name) != null) { metricInsideBraces = true; writer.write('{'); diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/EscapingScheme.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/EscapingScheme.java index 91566030e..4e06a59a0 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/EscapingScheme.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/EscapingScheme.java @@ -4,9 +4,19 @@ import static io.prometheus.metrics.model.snapshots.PrometheusNaming.nameEscapingScheme; public enum EscapingScheme { + // NO_ESCAPING indicates that a name will not be escaped. NO_ESCAPING("allow-utf-8"), + + // UNDERSCORE_ESCAPING replaces all legacy-invalid characters with underscores. UNDERSCORE_ESCAPING("underscores"), + + // DOTS_ESCAPING is similar to UNDERSCORE_ESCAPING, except that dots are + // converted to `_dot_` and pre-existing underscores are converted to `__`. DOTS_ESCAPING("dots"), + + // VALUE_ENCODING_ESCAPING prepends the name with `U__` and replaces all invalid + // characters with the Unicode value, surrounded by underscores. Single + // underscores are replaced with double underscores. VALUE_ENCODING_ESCAPING("values"), ; @@ -20,6 +30,10 @@ public final String getValue() { this.value = value; } + // fromAcceptHeader returns an EscapingScheme depending on the Accept header. Iff the + // header contains an escaping=allow-utf-8 term, it will select NO_ESCAPING. If a valid + // "escaping" term exists, that will be used. Otherwise, the global default will + // be returned. public static EscapingScheme fromAcceptHeader(String acceptHeader) { for (String p : acceptHeader.split(";")) { String[] toks = p.split("="); @@ -32,6 +46,7 @@ public static EscapingScheme fromAcceptHeader(String acceptHeader) { try { return EscapingScheme.forString(value); } catch (IllegalArgumentException e) { + // If the escaping parameter is unknown, ignore it. return nameEscapingScheme; } } 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 0464ba1f7..2a3214892 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 @@ -18,10 +18,23 @@ * in Prometheus exposition formats. However, if metrics are exposed in OpenTelemetry format the dots are retained. */ public class PrometheusNaming { + // nameValidationScheme determines the method of name validation to be used by + // all calls to validateMetricName() and isValidMetricName(). Setting UTF-8 mode + // in isolation from other components that don't support UTF-8 may result in + // bugs or other undefined behavior. This value is intended to be set by + // UTF-8-aware binaries as part of their startup via a properties file. public static ValidationScheme nameValidationScheme = initValidationScheme(); + // nameEscapingScheme defines the default way that names will be + // escaped when presented to systems that do not support UTF-8 names. If the + // Accept "escaping" term is specified, that will override this value. public static EscapingScheme nameEscapingScheme = EscapingScheme.VALUE_ENCODING_ESCAPING; + // ESCAPING_KEY is the key in an Accept header that defines how + // metric and label names that do not conform to the legacy character + // requirements should be escaped when being scraped by a legacy Prometheus + // system. If a system does not explicitly pass an escaping parameter in the + // Accept header, the default nameEscapingScheme will be used. public static final String ESCAPING_KEY = "escaping"; private static final String LOWERHEX = "0123456789abcdef"; @@ -258,6 +271,8 @@ private static String replaceIllegalCharsInLabelName(String name) { return new String(sanitized); } + // escapeMetricSnapshot escapes the given metric names and labels with the given + // escaping scheme. public static MetricSnapshot escapeMetricSnapshot(MetricSnapshot v, EscapingScheme scheme) { if (v == null) { return null; @@ -269,6 +284,7 @@ public static MetricSnapshot escapeMetricSnapshot(MetricSnapshot v, EscapingSche String outName; + // If the name is null, copy as-is, don't try to escape. if (v.getMetadata().getPrometheusName() == null || isValidLegacyMetricName(v.getMetadata().getPrometheusName())) { outName = v.getMetadata().getPrometheusName(); } else { @@ -453,6 +469,10 @@ static boolean metricNeedsEscaping(DataPointSnapshot d) { return false; } + // escapeName escapes the incoming name according to the provided escaping + // scheme. Depending on the rules of escaping, this may cause no change in the + // string that is returned (especially NO_ESCAPING, which by definition is a + // noop). This method does not do any validation of the name. static String escapeName(String name, EscapingScheme scheme) { if (name.isEmpty()) { return name; @@ -475,6 +495,7 @@ static String escapeName(String name, EscapingScheme scheme) { } return escaped.toString(); case DOTS_ESCAPING: + // Do not early return for legacy valid names, we still escape underscores. for (int i = 0; i < name.length(); i++) { char c = name.charAt(i); if (c == '_') { @@ -519,6 +540,9 @@ static String escapeName(String name, EscapingScheme scheme) { } } + // unescapeName unescapes the incoming name according to the provided escaping + // scheme if possible. Some schemes are partially or totally non-roundtripable. + // If any error is encountered, returns the original input. static String unescapeName(String name, EscapingScheme scheme) { if (name.isEmpty()) { return name; diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/ValidationScheme.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/ValidationScheme.java index 9f03b9549..63ac70e67 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/ValidationScheme.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/ValidationScheme.java @@ -1,6 +1,13 @@ package io.prometheus.metrics.model.snapshots; +// ValidationScheme is an enum for determining how metric and label names will +// be validated by this library. public enum ValidationScheme { + // LEGACY_VALIDATION is a setting that requires that metric and label names + // conform to the original character requirements. LEGACY_VALIDATION, + + // UTF_8_VALIDATION only requires that metric and label names be valid UTF-8 + // strings. UTF_8_VALIDATION } From d95bcb3392b9a3292f312d0807a4982e39f51a64 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Thu, 7 Mar 2024 12:10:08 -0300 Subject: [PATCH 08/11] Use original prometheusName() implementation Signed-off-by: Federico Torres --- .../io/prometheus/metrics/model/snapshots/Labels.java | 2 +- .../metrics/model/snapshots/MetricMetadata.java | 2 +- .../metrics/model/snapshots/PrometheusNaming.java | 9 +-------- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/Labels.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/Labels.java index 67a0dcf1f..cf80ea37f 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/Labels.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/Labels.java @@ -75,7 +75,7 @@ public static Labels of(String... keyValuePairs) { static String[] makePrometheusNames(String[] names) { String[] prometheusNames = names; for (int i=0; i Date: Thu, 7 Mar 2024 14:05:05 -0300 Subject: [PATCH 09/11] Refactor label name validation Signed-off-by: Federico Torres --- .../expositionformats/TextFormatUtil.java | 3 +- .../model/snapshots/PrometheusNaming.java | 33 ++++++++----------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java index c6994a9c9..cc1b210ab 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java @@ -2,6 +2,7 @@ import io.prometheus.metrics.model.snapshots.Labels; import io.prometheus.metrics.model.snapshots.PrometheusNaming; +import io.prometheus.metrics.model.snapshots.ValidationScheme; import java.io.IOException; import java.io.OutputStreamWriter; @@ -95,7 +96,7 @@ static void writeName(OutputStreamWriter writer, String name, NameType nameType) } break; case Label: - if (PrometheusNaming.isValidLegacyLabelName(name)) { + if (PrometheusNaming.isValidLegacyLabelName(name) && PrometheusNaming.nameValidationScheme == ValidationScheme.LEGACY_VALIDATION) { writer.write(name); return; } 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 6ba6e1142..cd0680ee6 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 @@ -74,6 +74,16 @@ public class PrometheusNaming { ".total", ".created", ".bucket", ".info" }; + static ValidationScheme initValidationScheme() { + if (PrometheusProperties.get() != null && PrometheusProperties.get().getNamingProperties() != null) { + String validationScheme = PrometheusProperties.get().getNamingProperties().getValidationScheme(); + if (validationScheme != null && validationScheme.equals("utf-8")) { + return ValidationScheme.UTF_8_VALIDATION; + } + } + return ValidationScheme.LEGACY_VALIDATION; + } + /** * Test if a metric name is valid. Rules: *

    @@ -96,16 +106,6 @@ public static boolean isValidMetricName(String name) { return validateMetricName(name) == null; } - static ValidationScheme initValidationScheme() { - if (PrometheusProperties.get() != null && PrometheusProperties.get().getNamingProperties() != null) { - String validationScheme = PrometheusProperties.get().getNamingProperties().getValidationScheme(); - if (validationScheme != null && validationScheme.equals("utf-8")) { - return ValidationScheme.UTF_8_VALIDATION; - } - } - return ValidationScheme.LEGACY_VALIDATION; - } - static String validateMetricName(String name) { switch (nameValidationScheme) { case LEGACY_VALIDATION: @@ -151,8 +151,7 @@ public static boolean isValidLegacyMetricName(String name) { public static boolean isValidLabelName(String name) { switch (nameValidationScheme) { case LEGACY_VALIDATION: - return isValidLegacyLabelName(name) && - !(name.startsWith("__") || name.startsWith("._") || name.startsWith("..") || name.startsWith("_.")); + return isValidLegacyLabelName(name); case UTF_8_VALIDATION: return StandardCharsets.UTF_8.newEncoder().canEncode(name); default: @@ -161,14 +160,8 @@ public static boolean isValidLabelName(String name) { } public static boolean isValidLegacyLabelName(String name) { - switch (nameValidationScheme) { - case LEGACY_VALIDATION: - return LEGACY_LABEL_NAME_PATTERN.matcher(name).matches(); - case UTF_8_VALIDATION: - return LABEL_NAME_PATTERN.matcher(name).matches(); - default: - throw new RuntimeException("Invalid name validation scheme requested: " + nameValidationScheme); - } + return LEGACY_LABEL_NAME_PATTERN.matcher(name).matches() && + !(name.startsWith("__") || name.startsWith("._") || name.startsWith("..") || name.startsWith("_.")); } /** From bd971797facb6c879ce31fc93ec571e411e50c27 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Thu, 7 Mar 2024 16:35:52 -0300 Subject: [PATCH 10/11] Remove comments Signed-off-by: Federico Torres --- .../metrics/expositionformats/ExpositionFormatsTest.java | 1 - .../prometheus/metrics/model/snapshots/PrometheusNamingTest.java | 1 - 2 files changed, 2 deletions(-) diff --git a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java index 1e5e1ade9..7b4a6ef0b 100644 --- a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java +++ b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java @@ -1869,7 +1869,6 @@ public void testFindWriter() { writer = expositionFormats.findWriter(acceptHeaderValue); Assert.assertEquals(expectedFmt, writer.getContentType() + escapingScheme.toHeaderFormat()); - // TODO review if this is ok nameEscapingScheme = EscapingScheme.VALUE_ENCODING_ESCAPING; // OM format, no version 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 16d66954e..432481e45 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 @@ -120,7 +120,6 @@ public void testEscapeName() { got = unescapeName(got, EscapingScheme.VALUE_ENCODING_ESCAPING); Assert.assertEquals("no:escaping_required", got); - // TODO should add test for legacy validation? // name with dots got = escapeName("mysystem.prod.west.cpu.load", EscapingScheme.UNDERSCORE_ESCAPING); Assert.assertEquals("mysystem_prod_west_cpu_load", got); From 119947640cef39c123097abca20d10c30968e526 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Thu, 7 Mar 2024 17:21:33 -0300 Subject: [PATCH 11/11] Remove unused constant Signed-off-by: Federico Torres --- .../io/prometheus/metrics/model/snapshots/PrometheusNaming.java | 2 -- 1 file changed, 2 deletions(-) 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 cd0680ee6..1f48e3507 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 @@ -53,8 +53,6 @@ public class PrometheusNaming { */ private static final Pattern LEGACY_LABEL_NAME_PATTERN = Pattern.compile("^[a-zA-Z_.][a-zA-Z0-9_.]*$"); - private static final Pattern LABEL_NAME_PATTERN = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$"); - /** * According to OpenMetrics {@code _count} and {@code _sum} (and {@code _gcount}, {@code _gsum}) should also be * reserved metric name suffixes. However, popular instrumentation libraries have Gauges with names