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-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-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 eae5c1b8c..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 @@ -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. @@ -60,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); @@ -214,7 +196,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 +204,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 +271,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 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('{'); } + writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric); + if (!labels.isEmpty() || additionalLabelName != null) { - writeLabels(writer, labels, additionalLabelName, additionalLabelValue); + writeLabels(writer, labels, additionalLabelName, additionalLabelValue, metricInsideBraces); + } else if (metricInsideBraces) { + writer.write('}'); } + writer.write(' '); } @@ -306,7 +296,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 +309,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(), NameType.Metric); writer.write(' '); writer.write(typeName); writer.write('\n'); if (metadata.getUnit() != null) { writer.write("# UNIT "); - writer.write(metadata.getPrometheusName()); + writeName(writer, metadata.getPrometheusName(), NameType.Metric); 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(), NameType.Metric); 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/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 cf9bc3d10..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 @@ -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. @@ -54,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); @@ -78,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); @@ -254,7 +241,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 +249,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 +277,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 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('{'); } + writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric); + if (!labels.isEmpty() || additionalLabelName != null) { - writeLabels(writer, labels, additionalLabelName, additionalLabelValue); + writeLabels(writer, labels, additionalLabelName, additionalLabelValue, metricInsideBraces); + } else 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 : ""), NameType.Metric); 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 : ""), 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 423fa6692..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 @@ -1,12 +1,17 @@ package io.prometheus.metrics.expositionformats; 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; import java.io.Writer; -import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName; +enum NameType { + Metric, + Label +} public class TextFormatUtil { @@ -38,7 +43,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 +62,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), NameType.Label); writer.write("=\""); - writeEscapedLabelValue(writer, labels.getValue(i)); + writeEscapedString(writer, labels.getValue(i)); writer.write("\""); } if (additionalLabelName != null) { @@ -79,4 +86,26 @@ static void writeLabels(OutputStreamWriter writer, Labels labels, String additio } writer.write('}'); } + + 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) && PrometheusNaming.nameValidationScheme == ValidationScheme.LEGACY_VALIDATION) { + writer.write(name); + return; + } + break; + default: + throw new RuntimeException("Invalid name type requested: " + nameType); + } + writer.write('"'); + writeEscapedString(writer, name); + 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..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 @@ -1,32 +1,20 @@ 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; 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 { @@ -140,6 +128,7 @@ public void testCounterComplete() throws IOException { "timestamp_ms: 1672850585820 " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; CounterSnapshot counter = CounterSnapshot.builder() .name("service_time_seconds") @@ -184,6 +173,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.LEGACY_VALIDATION; CounterSnapshot counter = CounterSnapshot.builder() .name("my_counter") .dataPoint(CounterDataPointSnapshot.builder().value(1.1).build()) @@ -215,6 +205,7 @@ public void testCounterWithDots() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; CounterSnapshot counter = CounterSnapshot.builder() .name("my.request.count") @@ -260,6 +251,7 @@ public void testGaugeComplete() throws IOException { "timestamp_ms: 1672850585820 " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; GaugeSnapshot gauge = GaugeSnapshot.builder() .name("disk_usage_ratio") .help("percentage used") @@ -299,6 +291,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.LEGACY_VALIDATION; GaugeSnapshot gauge = GaugeSnapshot.builder() .name("temperature_centigrade") .dataPoint(GaugeDataPointSnapshot.builder() @@ -336,6 +329,7 @@ public void testGaugeWithDots() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; GaugeSnapshot gauge = GaugeSnapshot.builder() .name("my.temperature.celsius") @@ -354,6 +348,38 @@ 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.UTF_8_VALIDATION; + + 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); + + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; + } + @Test public void testSummaryComplete() throws IOException { String openMetricsText = "" + @@ -445,6 +471,7 @@ public void testSummaryComplete() throws IOException { "timestamp_ms: 1672850585820 " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("http_request_duration_seconds") .help("request duration") @@ -513,6 +540,7 @@ public void testSummaryWithoutQuantiles() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .help("latency") @@ -548,6 +576,7 @@ public void testSummaryNoCountAndSum() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .dataPoint(SummaryDataPointSnapshot.builder() @@ -580,6 +609,7 @@ public void testSummaryJustCount() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .dataPoint(SummaryDataPointSnapshot.builder() @@ -612,6 +642,7 @@ public void testSummaryJustSum() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .dataPoint(SummaryDataPointSnapshot.builder() @@ -627,6 +658,7 @@ public void testSummaryJustSum() throws IOException { @Test public void testSummaryEmptyData() throws IOException { + 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() @@ -665,6 +697,7 @@ public void testSummaryEmptyAndNonEmpty() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("latency_seconds") .dataPoint(SummaryDataPointSnapshot.builder() @@ -710,6 +743,7 @@ public void testSummaryWithDots() throws IOException { "summary { sample_count: 1 sample_sum: 0.03 } " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; SummarySnapshot summary = SummarySnapshot.builder() .name("my.request.duration.seconds") @@ -831,6 +865,7 @@ public void testClassicHistogramComplete() throws Exception { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("response_size_bytes") .help("help") @@ -892,6 +927,7 @@ public void testClassicHistogramMinimal() throws Exception { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("request_latency_seconds") .dataPoint(HistogramSnapshot.HistogramDataPointSnapshot.builder() @@ -935,6 +971,7 @@ public void testClassicHistogramCountAndSum() throws Exception { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("request_latency_seconds") .dataPoint(HistogramSnapshot.HistogramDataPointSnapshot.builder() @@ -1054,6 +1091,7 @@ public void testClassicGaugeHistogramComplete() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot gaugeHistogram = HistogramSnapshot.builder() .gaugeHistogram(true) .name("cache_size_bytes") @@ -1117,6 +1155,7 @@ public void testClassicGaugeHistogramMinimal() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot gaugeHistogram = HistogramSnapshot.builder() .gaugeHistogram(true) .name("queue_size_bytes") @@ -1163,6 +1202,7 @@ public void testClassicGaugeHistogramCountAndSum() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot gaugeHistogram = HistogramSnapshot.builder() .gaugeHistogram(true) .name("queue_size_bytes") @@ -1210,6 +1250,7 @@ public void testClassicHistogramWithDots() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("my.request.duration.seconds") @@ -1337,6 +1378,7 @@ public void testNativeHistogramComplete() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot nativeHistogram = HistogramSnapshot.builder() .name("response_size_bytes") .help("help") @@ -1417,6 +1459,7 @@ public void testNativeHistogramMinimal() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot nativeHistogram = HistogramSnapshot.builder() .name("latency_seconds") .dataPoint(HistogramSnapshot.HistogramDataPointSnapshot.builder() @@ -1463,6 +1506,7 @@ public void testNativeHistogramWithDots() throws IOException { "} " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; HistogramSnapshot histogram = HistogramSnapshot.builder() .name("my.request.duration.seconds") @@ -1499,6 +1543,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.LEGACY_VALIDATION; InfoSnapshot info = InfoSnapshot.builder() .name("version") .help("version information") @@ -1533,6 +1578,7 @@ public void testInfoWithDots() throws IOException { "gauge { value: 1.0 } " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; InfoSnapshot info = InfoSnapshot.builder() .name("jvm.status") .help("JVM status info") @@ -1562,6 +1608,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.LEGACY_VALIDATION; StateSetSnapshot stateSet = StateSetSnapshot.builder() .name("state") .help("complete state set example") @@ -1595,6 +1642,7 @@ public void testStateSetMinimal() throws IOException { "# TYPE state gauge\n" + "state{state=\"a\"} 1\n" + "state{state=\"bb\"} 0\n"; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; StateSetSnapshot stateSet = StateSetSnapshot.builder() .name("state") .dataPoint(StateSetSnapshot.StateSetDataPointSnapshot.builder() @@ -1636,6 +1684,7 @@ public void testStateSetWithDots() throws IOException { "gauge { value: 0.0 } " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; StateSetSnapshot stateSet = StateSetSnapshot.builder() .name("my.application.state") .help("My application state") @@ -1664,6 +1713,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.LEGACY_VALIDATION; UnknownSnapshot unknown = UnknownSnapshot.builder() .name("my_special_thing_bytes") .help("help message") @@ -1696,6 +1746,7 @@ public void testUnknownMinimal() throws IOException { String prometheus = "" + "# TYPE other untyped\n" + "other 22.3\n"; + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; UnknownSnapshot unknown = UnknownSnapshot.builder() .name("other") .dataPoint(UnknownDataPointSnapshot.builder() @@ -1730,6 +1781,7 @@ public void testUnknownWithDots() throws IOException { "untyped { value: 0.7 } " + "}"; //@formatter:on + PrometheusNaming.nameValidationScheme = ValidationScheme.LEGACY_VALIDATION; UnknownSnapshot unknown = UnknownSnapshot.builder() .name("some.unknown.metric") .help("help message") @@ -1756,6 +1808,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.LEGACY_VALIDATION; CounterSnapshot counter = CounterSnapshot.builder() .name("test") .help("Some text and \n some \" escaping") // example from https://openMetrics.io @@ -1776,6 +1829,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.LEGACY_VALIDATION; CounterSnapshot counter = CounterSnapshot.builder() .name("test") .dataPoint(CounterDataPointSnapshot.builder() @@ -1788,31 +1842,133 @@ 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()); + + 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/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/EscapingScheme.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/EscapingScheme.java new file mode 100644 index 000000000..4e06a59a0 --- /dev/null +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/EscapingScheme.java @@ -0,0 +1,75 @@ +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 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"), + ; + + public final String getValue() { + return value; + } + + private final String value; + + EscapingScheme(String value) { + 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("="); + 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) { + // If the escaping parameter is unknown, ignore it. + 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/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 @@ -9,16 +18,40 @@ * 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"; + + private static final String METRIC_NAME_LABEL= "__name__"; /** * 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_.]*$"); /** * According to OpenMetrics {@code _count} and {@code _sum} (and {@code _gcount}, {@code _gsum}) should also be @@ -39,10 +72,20 @@ 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: *
    - *
  • The name must match {@link #METRIC_NAME_PATTERN}.
  • + *
  • The name must match {@link #LEGACY_METRIC_NAME_PATTERN}.
  • *
  • The name MUST NOT end with one of the {@link #RESERVED_METRIC_NAME_SUFFIXES}.
  • *
* If a metric has a {@link Unit}, the metric name SHOULD end with the unit as a suffix. @@ -61,25 +104,61 @@ public static boolean isValidMetricName(String name) { return validateMetricName(name) == null; } + static String validateMetricName(String name) { + switch (nameValidationScheme) { + case LEGACY_VALIDATION: + return validateLegacyMetricName(name); + case UTF_8_VALIDATION: + if(name.isEmpty() || !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."; } } - 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) { + switch (nameValidationScheme) { + case LEGACY_VALIDATION: + return LEGACY_METRIC_NAME_PATTERN.matcher(name).matches(); + case UTF_8_VALIDATION: + return METRIC_NAME_PATTERN.matcher(name).matches(); + default: + throw new RuntimeException("Invalid name validation scheme requested: " + nameValidationScheme); + } + } + public static boolean isValidLabelName(String name) { - return LABEL_NAME_PATTERN.matcher(name).matches() && + switch (nameValidationScheme) { + case LEGACY_VALIDATION: + return isValidLegacyLabelName(name); + case UTF_8_VALIDATION: + return StandardCharsets.UTF_8.newEncoder().canEncode(name); + default: + throw new RuntimeException("Invalid name validation scheme requested: " + nameValidationScheme); + } + } + + public static boolean isValidLegacyLabelName(String name) { + return LEGACY_LABEL_NAME_PATTERN.matcher(name).matches() && !(name.startsWith("__") || name.startsWith("._") || name.startsWith("..") || name.startsWith("_.")); } @@ -136,7 +215,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(); @@ -157,7 +236,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(); @@ -175,4 +254,359 @@ 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; + } + + if (scheme == EscapingScheme.NO_ESCAPING) { + return v; + } + + 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 { + 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()); + continue; + } + 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; + } + + // 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; + } + 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: + // 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 == '_') { + 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 (!isValidUTF8Char(c)) { + escaped.append("_FFFD_"); + } else if (c < 0x100) { + escaped.append('_'); + for (int s = 4; s >= 0; s -= 4) { + escaped.append(LOWERHEX.charAt((c >> s) & 0xF)); + } + escaped.append('_'); + } else { + escaped.append('_'); + for (int s = 12; s >= 0; s -= 4) { + escaped.append(LOWERHEX.charAt((c >> s) & 0xF)); + } + escaped.append('_'); + } + } + return escaped.toString(); + default: + throw new IllegalArgumentException("Invalid escaping scheme " + 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; + } + 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: + 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 (!isValidUTF8Char(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 isValidUTF8Char(char b) { + return ((b < MIN_HIGH_SURROGATE || b > MAX_LOW_SURROGATE) && + (b < 0xFFFE)); + } } 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..63ac70e67 --- /dev/null +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/ValidationScheme.java @@ -0,0 +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 +} 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..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 @@ -3,14 +3,13 @@ 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 { @Test public void testSanitizeMetricName() { + 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")); @@ -23,6 +22,7 @@ public void testSanitizeMetricName() { @Test public void testSanitizeLabelName() { + 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"))); @@ -31,4 +31,359 @@ public void testSanitizeLabelName() { Assert.assertEquals("abc.def", sanitizeLabelName("abc.def")); Assert.assertEquals("abc.def2", sanitizeLabelName("abc.def2")); } + + @Test + public void testMetricNameIsValid() { + nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; + 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")); + 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")); + 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")); + } + + @Test + public void testLabelNameIsValid() { + nameValidationScheme = ValidationScheme.UTF_8_VALIDATION; + 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")); + 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); + + // 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-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 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()); } }