diff --git a/examples/example-exemplars-tail-sampling/example-greeting-service/src/main/java/io/prometheus/metrics/examples/otel_exemplars/greeting/GreetingServlet.java b/examples/example-exemplars-tail-sampling/example-greeting-service/src/main/java/io/prometheus/metrics/examples/otel_exemplars/greeting/GreetingServlet.java index 2fd266cd9..a1046e2a2 100644 --- a/examples/example-exemplars-tail-sampling/example-greeting-service/src/main/java/io/prometheus/metrics/examples/otel_exemplars/greeting/GreetingServlet.java +++ b/examples/example-exemplars-tail-sampling/example-greeting-service/src/main/java/io/prometheus/metrics/examples/otel_exemplars/greeting/GreetingServlet.java @@ -27,7 +27,7 @@ public GreetingServlet() { .withUnit(Unit.SECONDS) .withLabelNames("http_status") .register(); - histogram.withLabelValues("200"); + histogram.initLabelValues("200"); } @Override diff --git a/examples/example-exemplars-tail-sampling/example-hello-world-app/src/main/java/io/prometheus/metrics/examples/otel_exemplars/app/HelloWorldServlet.java b/examples/example-exemplars-tail-sampling/example-hello-world-app/src/main/java/io/prometheus/metrics/examples/otel_exemplars/app/HelloWorldServlet.java index d7c5db034..cabedbce5 100644 --- a/examples/example-exemplars-tail-sampling/example-hello-world-app/src/main/java/io/prometheus/metrics/examples/otel_exemplars/app/HelloWorldServlet.java +++ b/examples/example-exemplars-tail-sampling/example-hello-world-app/src/main/java/io/prometheus/metrics/examples/otel_exemplars/app/HelloWorldServlet.java @@ -34,7 +34,7 @@ public HelloWorldServlet() { .withUnit(Unit.SECONDS) .withLabelNames("http_status") .register(); - histogram.withLabelValues("200"); + histogram.initLabelValues("200"); } @Override diff --git a/examples/example-tomcat-servlet/src/main/java/io/prometheus/metrics/examples/tomcat_servlet/HelloWorldServlet.java b/examples/example-tomcat-servlet/src/main/java/io/prometheus/metrics/examples/tomcat_servlet/HelloWorldServlet.java index 1598028db..7a95e36da 100644 --- a/examples/example-tomcat-servlet/src/main/java/io/prometheus/metrics/examples/tomcat_servlet/HelloWorldServlet.java +++ b/examples/example-tomcat-servlet/src/main/java/io/prometheus/metrics/examples/tomcat_servlet/HelloWorldServlet.java @@ -32,6 +32,11 @@ public class HelloWorldServlet extends HttpServlet { .withLabelNames("http_status") .register(); + public HelloWorldServlet() { + counter.initLabelValues("200"); + histogram.initLabelValues("200"); + } + @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { long start = System.nanoTime(); 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 9341c5988..95660a463 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 @@ -58,7 +58,7 @@ public MetricsProperties getDefaultMetricProperties() { * May return {@code null} if no metric-specific properties are configured for a metric name. */ public MetricsProperties getMetricProperties(String metricName) { - return metricProperties.get(metricName); + return metricProperties.get(metricName.replace(".", "_")); } public ExemplarsProperties getExemplarProperties() { 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 ba52372e5..58ddc2669 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 @@ -39,6 +39,8 @@ public static PrometheusProperties load() throws PrometheusPropertiesException { // This will remove entries from properties when they are processed. private static Map loadMetricsConfigs(Map properties) { Map result = new HashMap<>(); + // Note that the metric name in the properties file must be as exposed in the Prometheus exposition formats, + // i.e. all dots replaced with underscores. Pattern pattern = Pattern.compile("io\\.prometheus\\.metrics\\.([^.]+)\\."); // Create a copy of the keySet() for iterating. We cannot iterate directly over keySet() // because entries are removed when MetricsConfig.load(...) is called. @@ -49,7 +51,7 @@ private static Map loadMetricsConfigs(Map @@ -109,8 +112,8 @@ protected CounterSnapshot collect(List labels, List metricDat return new CounterSnapshot(getMetadata(), data); } - static String normalizeName(String name) { - if (name != null && name.endsWith("_total")) { + static String stripTotalSuffix(String name) { + if (name != null && (name.endsWith("_total") || name.endsWith(".total"))) { name = name.substring(0, name.length() - 6); } return name; @@ -231,12 +234,12 @@ private Builder(PrometheusProperties properties) { * In the example above both {@code c1} and {@code c2} would be named {@code "events_total"} in Prometheus. *

* Throws an {@link IllegalArgumentException} if - * {@link io.prometheus.metrics.model.snapshots.MetricMetadata#isValidMetricName(String) MetricMetadata.isValidMetricName(name)} + * {@link io.prometheus.metrics.model.snapshots.PrometheusNaming#isValidMetricName(String) MetricMetadata.isValidMetricName(name)} * is {@code false}. */ @Override public Builder withName(String name) { - return super.withName(normalizeName(name)); + return super.withName(stripTotalSuffix(name)); } @Override diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/CounterWithCallback.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/CounterWithCallback.java index c7446f4c1..b4d7bc761 100644 --- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/CounterWithCallback.java +++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/CounterWithCallback.java @@ -80,12 +80,12 @@ private Builder(PrometheusProperties properties) { * In the example above both {@code c1} and {@code c2} would be named {@code "events_total"} in Prometheus. *

* Throws an {@link IllegalArgumentException} if - * {@link io.prometheus.metrics.model.snapshots.MetricMetadata#isValidMetricName(String) MetricMetadata.isValidMetricName(name)} + * {@link io.prometheus.metrics.model.snapshots.PrometheusNaming#isValidMetricName(String) MetricMetadata.isValidMetricName(name)} * is {@code false}. */ @Override public Builder withName(String name) { - return super.withName(Counter.normalizeName(name)); + return super.withName(Counter.stripTotalSuffix(name)); } @Override diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java index 9d6675dbb..5731e2843 100644 --- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java +++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java @@ -2,16 +2,16 @@ import io.prometheus.metrics.config.PrometheusProperties; import io.prometheus.metrics.model.snapshots.InfoSnapshot; -import io.prometheus.metrics.model.snapshots.Label; import io.prometheus.metrics.model.snapshots.Labels; import io.prometheus.metrics.model.snapshots.Unit; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName; + /** * Info metric. Example: *

{@code
@@ -90,15 +90,12 @@ private Builder(PrometheusProperties config) {
          * In the example above both {@code info1} and {@code info2} will be named {@code "runtime_info"} in Prometheus.
          * 

* Throws an {@link IllegalArgumentException} if - * {@link io.prometheus.metrics.model.snapshots.MetricMetadata#isValidMetricName(String) MetricMetadata.isValidMetricName(name)} + * {@link io.prometheus.metrics.model.snapshots.PrometheusNaming#isValidMetricName(String) MetricMetadata.isValidMetricName(name)} * is {@code false}. */ @Override public Builder withName(String name) { - if (name != null && name.endsWith("_info")) { - name = name.substring(0, name.length() - 5); - } - return super.withName(name); + return super.withName(stripInfoSuffix(name)); } /** @@ -112,8 +109,8 @@ public Builder withUnit(Unit unit) { return this; } - private static String normalizeName(String name) { - if (name != null && name.endsWith("_info")) { + private static String stripInfoSuffix(String name) { + if (name != null && (name.endsWith("_info") || name.endsWith(".info"))) { name = name.substring(0, name.length() - 5); } return name; @@ -121,7 +118,7 @@ private static String normalizeName(String name) { @Override public Info build() { - return new Info(withName(normalizeName(name))); + return new Info(this); } @Override diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java index 22b607654..6f963c299 100644 --- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java +++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java @@ -3,11 +3,14 @@ import io.prometheus.metrics.config.PrometheusProperties; import io.prometheus.metrics.model.snapshots.Labels; import io.prometheus.metrics.model.snapshots.MetricMetadata; +import io.prometheus.metrics.model.snapshots.PrometheusNaming; import io.prometheus.metrics.model.snapshots.Unit; import java.util.Arrays; import java.util.List; +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName; + /** * Almost all metrics have fixed metadata, i.e. the metric name is known when the metric is created. *

@@ -31,9 +34,8 @@ protected MetricMetadata getMetadata() { private String makeName(String name, Unit unit) { if (unit != null) { - String suffix = "_" + unit; - if (!name.endsWith(suffix)) { - name = name + suffix; + if (!name.endsWith(unit.toString())) { + name = name + "_" + unit; } } return name; @@ -51,7 +53,7 @@ protected Builder(List illegalLabelNames, PrometheusProperties propertie } public B withName(String name) { - if (!MetricMetadata.isValidMetricName(name)) { + if (!PrometheusNaming.isValidMetricName(name)) { throw new IllegalArgumentException("'" + name + "': Illegal metric name."); } this.name = name; @@ -70,7 +72,7 @@ public B withHelp(String help) { public B withLabelNames(String... labelNames) { for (String labelName : labelNames) { - if (!Labels.isValidLabelName(labelName)) { + if (!PrometheusNaming.isValidLabelName(labelName)) { throw new IllegalArgumentException(labelName + ": illegal label name"); } if (illegalLabelNames.contains(labelName)) { diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StateSet.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StateSet.java index 308c61be1..7348c0410 100644 --- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StateSet.java +++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StateSet.java @@ -3,6 +3,7 @@ import io.prometheus.metrics.config.MetricsProperties; import io.prometheus.metrics.config.PrometheusProperties; import io.prometheus.metrics.model.snapshots.Labels; +import io.prometheus.metrics.model.snapshots.MetricMetadata; import io.prometheus.metrics.model.snapshots.StateSetSnapshot; import io.prometheus.metrics.core.datapoints.StateSetDataPoint; @@ -11,6 +12,8 @@ import java.util.List; import java.util.stream.Stream; +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName; + /** * StateSet metric. Example: *

{@code
@@ -58,7 +61,7 @@ private StateSet(Builder builder, PrometheusProperties prometheusProperties) {
         exemplarsEnabled = getConfigProperty(properties, MetricsProperties::getExemplarsEnabled);
         this.names = builder.names; // builder.names is already a validated copy
         for (String name : names) {
-            if (this.getMetadata().getName().equals(name)) {
+            if (this.getMetadata().getPrometheusName().equals(prometheusName(name))) {
                 throw new IllegalArgumentException("Label name " + name + " is illegal (can't use the metric name as label name in state set metrics)");
             }
         }
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StatefulMetric.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StatefulMetric.java
index 591c16e6b..17d8da246 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StatefulMetric.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StatefulMetric.java
@@ -62,6 +62,34 @@ public MetricSnapshot collect() {
         return collect(labels, metricData);
     }
 
+    /**
+     * Initialize label values.
+     * 

+ * Example: Imagine you have a counter for payments as follows + *

+     * payment_transactions_total{payment_type="credit card"} 7.0
+     * payment_transactions_total{payment_type="paypal"} 3.0
+     * 
+ * Now, the data points for the {@code payment_type} label values get initialized when they are + * first used, i.e. the first time you call + *
{@code
+     * counter.withLabelValues("paypal").inc();
+     * }
+ * the data point with label {@code payment_type="paypal"} will go from non-existent to having value {@code 1.0}. + *

+ * In some cases this is confusing, and you want to have data points initialized on application start + * with an initial value of {@code 0.0}: + *

+     * payment_transactions_total{payment_type="credit card"} 0.0
+     * payment_transactions_total{payment_type="paypal"} 0.0
+     * 
+ * {@code initLabelValues(...)} can be used to initialize label value, so that the data points + * show up in the exposition format with an initial value of zero. + */ + public void initLabelValues(String... labelValues) { + withLabelValues(labelValues); + } + public D withLabelValues(String... labelValues) { if (labelValues.length != labelNames.length) { if (labelValues.length == 0) { @@ -73,7 +101,6 @@ public D withLabelValues(String... labelValues) { return data.computeIfAbsent(Arrays.asList(labelValues), l -> newDataPoint()); } - // TODO: Remove automatically if label values have not been used in a while? public void remove(String... labelValues) { data.remove(Arrays.asList(labelValues)); } diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java index 0efa6ee4a..a967a8d64 100644 --- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java +++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java @@ -109,18 +109,18 @@ public void testLabels() { @Test public void testTotalStrippedFromName() { - Counter counter = Counter.newBuilder() - .withName("my_counter_total") - .withUnit(Unit.SECONDS) - .build(); - Metrics.MetricFamily protobufData = new PrometheusProtobufWriter().convert(counter.collect()); - assertEquals("name: \"my_counter_seconds_total\" type: COUNTER metric { counter { value: 0.0 } }", TextFormat.printer().shortDebugString(protobufData)); - - counter = Counter.newBuilder() - .withName("my_counter") - .build(); - protobufData = new PrometheusProtobufWriter().convert(counter.collect()); - assertEquals("name: \"my_counter_total\" type: COUNTER metric { counter { value: 0.0 } }", TextFormat.printer().shortDebugString(protobufData)); + for (String name : new String[]{ + "my_counter_total", "my.counter.total", + "my_counter_seconds_total", "my.counter.seconds.total", + "my_counter", "my.counter", + "my_counter_seconds", "my.counter.seconds"}) { + Counter counter = Counter.newBuilder() + .withName(name) + .withUnit(Unit.SECONDS) + .build(); + Metrics.MetricFamily protobufData = new PrometheusProtobufWriter().convert(counter.collect()); + assertEquals("name: \"my_counter_seconds_total\" type: COUNTER metric { counter { value: 0.0 } }", TextFormat.printer().shortDebugString(protobufData)); + } } @Test 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 393e2c876..2af392097 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,19 +1,19 @@ package io.prometheus.metrics.core.metrics; import io.prometheus.metrics.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.core.exemplars.ExemplarSamplerConfigTestUtil; -import io.prometheus.metrics.model.snapshots.MetricSnapshots; -import io.prometheus.metrics.tracer.common.SpanContext; -import io.prometheus.metrics.tracer.initializer.SpanContextSupplier; 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.core.datapoints.DistributionDataPoint; +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; import org.junit.Assert; import org.junit.Before; @@ -1043,19 +1043,9 @@ public void testIllegalLabelNamePrefix() { .withLabelNames("__hello"); } - @Test(expected = IllegalArgumentException.class) - public void testIllegalLabelNameDot() { - // The Prometheus team are investigating to allow dots in future Prometheus versions, but for now it's invalid. - // The reason is that you cannot use illegal label names in the Prometheus query language. - Histogram.newBuilder() - .withName("test") - .withLabelNames("http.status"); - } - @Test(expected = IllegalArgumentException.class) public void testIllegalName() { - Histogram.newBuilder() - .withName("server.durations"); + Histogram.newBuilder().withName("my_namespace/server.durations"); } @Test(expected = IllegalArgumentException.class) 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 4cca37ef6..5b1bbf991 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,23 +1,28 @@ package io.prometheus.metrics.core.metrics; -import io.prometheus.metrics.model.snapshots.InfoSnapshot; -import io.prometheus.metrics.model.snapshots.Labels; -import org.junit.Assert; +import io.prometheus.metrics.com_google_protobuf_3_21_7.TextFormat; +import io.prometheus.metrics.expositionformats.PrometheusProtobufWriter; +import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_3_21_7.Metrics; import org.junit.Test; +import static org.junit.Assert.assertEquals; + public class InfoTest { @Test - public void testIncrement() { - Info info = Info.newBuilder() - .withName("target_info") - .withLabelNames("key") - .build(); - info.infoLabelValues("value"); - InfoSnapshot snapshot = info.collect(); - Assert.assertEquals("target", snapshot.getMetadata().getName()); - Assert.assertEquals(1, snapshot.getData().size()); - InfoSnapshot.InfoDataPointSnapshot data = snapshot.getData().stream().findAny().orElseThrow(RuntimeException::new); - Assert.assertEquals(Labels.of("key", "value"), data.getLabels()); + public void testInfoStrippedFromName() { + for (String name : new String[]{ + "jvm.runtime", "jvm_runtime", + "jvm.runtime.info", "jvm_runtime_info"}) { + for (String labelName : new String[]{"my.key", "my_key"}) { + Info info = Info.newBuilder() + .withName(name) + .withLabelNames(labelName) + .build(); + info.infoLabelValues("value"); + Metrics.MetricFamily protobufData = new PrometheusProtobufWriter().convert(info.collect()); + assertEquals("name: \"jvm_runtime_info\" type: GAUGE metric { label { name: \"my_key\" value: \"value\" } gauge { value: 1.0 } }", TextFormat.printer().shortDebugString(protobufData)); + } + } } } 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 0559ce0be..fa131335e 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 @@ -29,6 +29,7 @@ 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.model.snapshots.PrometheusNaming.prometheusName; /** * Write the OpenMetrics text format as defined on https://openmetrics.io. @@ -89,7 +90,7 @@ private void writeCounter(OutputStreamWriter writer, CounterSnapshot snapshot) t MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "counter", metadata); for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getData()) { - writeNameAndLabels(writer, metadata.getName(), "_total", data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_total", data.getLabels()); writeDouble(writer, data.getValue()); writeScrapeTimestampAndExemplar(writer, data, data.getExemplar()); writeCreated(writer, metadata, data); @@ -100,7 +101,7 @@ private void writeGauge(OutputStreamWriter writer, GaugeSnapshot snapshot) throw MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "gauge", metadata); for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getData()) { - writeNameAndLabels(writer, metadata.getName(), null, data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels()); writeDouble(writer, data.getValue()); if (exemplarsOnAllMetricTypesEnabled) { writeScrapeTimestampAndExemplar(writer, data, data.getExemplar()); @@ -128,7 +129,7 @@ private void writeClassicHistogramBuckets(OutputStreamWriter writer, MetricMetad long cumulativeCount = 0; for (int i = 0; i < buckets.size(); i++) { cumulativeCount += buckets.getCount(i); - writeNameAndLabels(writer, metadata.getName(), "_bucket", data.getLabels(), "le", buckets.getUpperBound(i)); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_bucket", data.getLabels(), "le", buckets.getUpperBound(i)); writeLong(writer, cumulativeCount); Exemplar exemplar; if (i == 0) { @@ -176,7 +177,7 @@ private void writeSummary(OutputStreamWriter writer, SummarySnapshot snapshot) t // quantiles, all indexes modulo exemplars.length. int exemplarIndex = 1; for (Quantile quantile : data.getQuantiles()) { - writeNameAndLabels(writer, metadata.getName(), null, data.getLabels(), "quantile", quantile.getQuantile()); + writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels(), "quantile", quantile.getQuantile()); writeDouble(writer, quantile.getValue()); if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) { exemplarIndex = (exemplarIndex + 1) % exemplars.size(); @@ -195,7 +196,7 @@ private void writeInfo(OutputStreamWriter writer, InfoSnapshot snapshot) throws MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "info", metadata); for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getData()) { - writeNameAndLabels(writer, metadata.getName(), "_info", data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_info", data.getLabels()); writer.write("1"); writeScrapeTimestampAndExemplar(writer, data, null); } @@ -206,13 +207,13 @@ private void writeStateSet(OutputStreamWriter writer, StateSetSnapshot snapshot) writeMetadata(writer, "stateset", metadata); for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getData()) { for (int i = 0; i < data.size(); i++) { - writer.write(metadata.getName()); + writer.write(metadata.getPrometheusName()); writer.write('{'); for (int j = 0; j < data.getLabels().size(); j++) { if (j > 0) { writer.write(","); } - writer.write(data.getLabels().getName(j)); + writer.write(data.getLabels().getPrometheusName(j)); writer.write("=\""); writeEscapedLabelValue(writer, data.getLabels().getValue(j)); writer.write("\""); @@ -220,7 +221,7 @@ private void writeStateSet(OutputStreamWriter writer, StateSetSnapshot snapshot) if (!data.getLabels().isEmpty()) { writer.write(","); } - writer.write(metadata.getName()); + writer.write(metadata.getPrometheusName()); writer.write("=\""); writeEscapedLabelValue(writer, data.getName(i)); writer.write("\"} "); @@ -238,7 +239,7 @@ private void writeUnknown(OutputStreamWriter writer, UnknownSnapshot snapshot) t MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "unknown", metadata); for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getData()) { - writeNameAndLabels(writer, metadata.getName(), null, data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels()); writeDouble(writer, data.getValue()); if (exemplarsOnAllMetricTypesEnabled) { writeScrapeTimestampAndExemplar(writer, data, data.getExemplar()); @@ -251,7 +252,7 @@ private void writeUnknown(OutputStreamWriter writer, UnknownSnapshot snapshot) t private void writeCountAndSum(OutputStreamWriter writer, MetricMetadata metadata, DistributionDataPointSnapshot data, String countSuffix, String sumSuffix, Exemplars exemplars) throws IOException { int exemplarIndex = 0; if (data.hasCount()) { - writeNameAndLabels(writer, metadata.getName(), countSuffix, data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), countSuffix, data.getLabels()); writeLong(writer, data.getCount()); if (exemplars.size() > 0) { writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex)); @@ -261,7 +262,7 @@ private void writeCountAndSum(OutputStreamWriter writer, MetricMetadata metadata } } if (data.hasSum()) { - writeNameAndLabels(writer, metadata.getName(), sumSuffix, data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), sumSuffix, data.getLabels()); writeDouble(writer, data.getSum()); if (exemplars.size() > 0) { writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex)); @@ -273,7 +274,7 @@ private void writeCountAndSum(OutputStreamWriter writer, MetricMetadata metadata private void writeCreated(OutputStreamWriter writer, MetricMetadata metadata, DataPointSnapshot data) throws IOException { if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { - writeNameAndLabels(writer, metadata.getName(), "_created", data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_created", data.getLabels()); writeTimestamp(writer, data.getCreatedTimestampMillis()); if (data.hasScrapeTimestamp()) { writer.write(' '); @@ -319,20 +320,20 @@ private void writeScrapeTimestampAndExemplar(OutputStreamWriter writer, DataPoin private void writeMetadata(OutputStreamWriter writer, String typeName, MetricMetadata metadata) throws IOException { writer.write("# TYPE "); - writer.write(metadata.getName()); + writer.write(metadata.getPrometheusName()); writer.write(' '); writer.write(typeName); writer.write('\n'); if (metadata.getUnit() != null) { writer.write("# UNIT "); - writer.write(metadata.getName()); + writer.write(metadata.getPrometheusName()); writer.write(' '); writeEscapedLabelValue(writer, metadata.getUnit().toString()); writer.write('\n'); } if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { writer.write("# HELP "); - writer.write(metadata.getName()); + writer.write(metadata.getPrometheusName()); writer.write(' '); writeEscapedLabelValue(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 d6de08114..6f4eb6e67 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 @@ -24,6 +24,7 @@ import java.io.OutputStream; import static io.prometheus.metrics.expositionformats.ProtobufUtil.timestampFromMillis; +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName; /** * Write the Prometheus protobuf format as defined in @@ -103,7 +104,7 @@ public Metrics.MetricFamily convert(MetricSnapshot snapshot) { } else if (snapshot instanceof StateSetSnapshot) { for (StateSetSnapshot.StateSetDataPointSnapshot data : ((StateSetSnapshot) snapshot).getData()) { for (int i = 0; i < data.size(); i++) { - builder.addMetric(convert(data, snapshot.getMetadata().getName(), i)); + builder.addMetric(convert(data, snapshot.getMetadata().getPrometheusName(), i)); } } setMetadataUnlessEmpty(builder, snapshot.getMetadata(), null, Metrics.MetricType.GAUGE); @@ -121,9 +122,9 @@ private void setMetadataUnlessEmpty(Metrics.MetricFamily.Builder builder, Metric return; } if (nameSuffix == null) { - builder.setName(metadata.getName()); + builder.setName(metadata.getPrometheusName()); } else { - builder.setName(metadata.getName() + nameSuffix); + builder.setName(metadata.getPrometheusName() + nameSuffix); } if (metadata.getHelp() != null) { builder.setHelp(metadata.getHelp()); @@ -335,7 +336,7 @@ private Metrics.Metric.Builder convert(UnknownSnapshot.UnknownDataPointSnapshot private void addLabels(Metrics.Metric.Builder metricBuilder, Labels labels) { for (int i = 0; i < labels.size(); i++) { metricBuilder.addLabel(Metrics.LabelPair.newBuilder() - .setName(labels.getName(i)) + .setName(labels.getPrometheusName(i)) .setValue(labels.getValue(i)) .build()); } @@ -344,7 +345,7 @@ private void addLabels(Metrics.Metric.Builder metricBuilder, Labels labels) { private void addLabels(Metrics.Exemplar.Builder metricBuilder, Labels labels) { for (int i = 0; i < labels.size(); i++) { metricBuilder.addLabel(Metrics.LabelPair.newBuilder() - .setName(labels.getName(i)) + .setName(labels.getPrometheusName(i)) .setValue(labels.getValue(i)) .build()); } 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 a4499dd9d..bc187a886 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 @@ -26,6 +26,7 @@ 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.model.snapshots.PrometheusNaming.prometheusName; /** * Write the Prometheus text format. This is the default if you view a Prometheus endpoint with your Web browser. @@ -102,7 +103,7 @@ public void writeCreated(OutputStreamWriter writer, MetricSnapshot snapshot) thr writeMetadata(writer, "_created", "gauge", metadata); metadataWritten = true; } - writeNameAndLabels(writer, metadata.getName(), "_created", data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_created", data.getLabels()); writeTimestamp(writer, data.getCreatedTimestampMillis()); writeScrapeTimestampAndNewline(writer, data); } @@ -115,7 +116,7 @@ private void writeCounter(OutputStreamWriter writer, CounterSnapshot snapshot) t MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "_total", "counter", metadata); for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getData()) { - writeNameAndLabels(writer, metadata.getName(), "_total", data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_total", data.getLabels()); writeDouble(writer, data.getValue()); writeScrapeTimestampAndNewline(writer, data); } @@ -126,7 +127,7 @@ private void writeGauge(OutputStreamWriter writer, GaugeSnapshot snapshot) throw MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "", "gauge", metadata); for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getData()) { - writeNameAndLabels(writer, metadata.getName(), null, data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels()); writeDouble(writer, data.getValue()); writeScrapeTimestampAndNewline(writer, data); } @@ -140,18 +141,18 @@ private void writeHistogram(OutputStreamWriter writer, HistogramSnapshot snapsho long cumulativeCount = 0; for (int i = 0; i < buckets.size(); i++) { cumulativeCount += buckets.getCount(i); - writeNameAndLabels(writer, metadata.getName(), "_bucket", data.getLabels(), "le", buckets.getUpperBound(i)); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_bucket", data.getLabels(), "le", buckets.getUpperBound(i)); writeLong(writer, cumulativeCount); writeScrapeTimestampAndNewline(writer, data); } if (!snapshot.isGaugeHistogram()) { if (data.hasCount()) { - writeNameAndLabels(writer, metadata.getName(), "_count", data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_count", data.getLabels()); writeLong(writer, data.getCount()); writeScrapeTimestampAndNewline(writer, data); } if (data.hasSum()) { - writeNameAndLabels(writer, metadata.getName(), "_sum", data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_sum", data.getLabels()); writeDouble(writer, data.getSum()); writeScrapeTimestampAndNewline(writer, data); } @@ -183,7 +184,7 @@ private void writeGaugeCountSum(OutputStreamWriter writer, HistogramSnapshot sna writeMetadata(writer, "_gcount", "gauge", metadata); metadataWritten = true; } - writeNameAndLabels(writer, metadata.getName(), "_gcount", data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_gcount", data.getLabels()); writeLong(writer, data.getCount()); writeScrapeTimestampAndNewline(writer, data); } @@ -195,7 +196,7 @@ private void writeGaugeCountSum(OutputStreamWriter writer, HistogramSnapshot sna writeMetadata(writer, "_gsum", "gauge", metadata); metadataWritten = true; } - writeNameAndLabels(writer, metadata.getName(), "_gsum", data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_gsum", data.getLabels()); writeDouble(writer, data.getSum()); writeScrapeTimestampAndNewline(writer, data); } @@ -214,17 +215,17 @@ private void writeSummary(OutputStreamWriter writer, SummarySnapshot snapshot) t metadataWritten = true; } for (Quantile quantile : data.getQuantiles()) { - writeNameAndLabels(writer, metadata.getName(), null, data.getLabels(), "quantile", quantile.getQuantile()); + writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels(), "quantile", quantile.getQuantile()); writeDouble(writer, quantile.getValue()); writeScrapeTimestampAndNewline(writer, data); } if (data.hasCount()) { - writeNameAndLabels(writer, metadata.getName(), "_count", data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_count", data.getLabels()); writeLong(writer, data.getCount()); writeScrapeTimestampAndNewline(writer, data); } if (data.hasSum()) { - writeNameAndLabels(writer, metadata.getName(), "_sum", data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_sum", data.getLabels()); writeDouble(writer, data.getSum()); writeScrapeTimestampAndNewline(writer, data); } @@ -235,7 +236,7 @@ private void writeInfo(OutputStreamWriter writer, InfoSnapshot snapshot) throws MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "_info", "gauge", metadata); for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getData()) { - writeNameAndLabels(writer, metadata.getName(), "_info", data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), "_info", data.getLabels()); writer.write("1"); writeScrapeTimestampAndNewline(writer, data); } @@ -246,13 +247,13 @@ private void writeStateSet(OutputStreamWriter writer, StateSetSnapshot snapshot) writeMetadata(writer, "", "gauge", metadata); for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getData()) { for (int i = 0; i < data.size(); i++) { - writer.write(metadata.getName()); + writer.write(metadata.getPrometheusName()); writer.write('{'); for (int j = 0; j < data.getLabels().size(); j++) { if (j > 0) { writer.write(","); } - writer.write(data.getLabels().getName(j)); + writer.write(data.getLabels().getPrometheusName(j)); writer.write("=\""); writeEscapedLabelValue(writer, data.getLabels().getValue(j)); writer.write("\""); @@ -260,7 +261,7 @@ private void writeStateSet(OutputStreamWriter writer, StateSetSnapshot snapshot) if (!data.getLabels().isEmpty()) { writer.write(","); } - writer.write(metadata.getName()); + writer.write(metadata.getPrometheusName()); writer.write("=\""); writeEscapedLabelValue(writer, data.getName(i)); writer.write("\"} "); @@ -278,7 +279,7 @@ private void writeUnknown(OutputStreamWriter writer, UnknownSnapshot snapshot) t MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "", "untyped", metadata); for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getData()) { - writeNameAndLabels(writer, metadata.getName(), null, data.getLabels()); + writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels()); writeDouble(writer, data.getValue()); writeScrapeTimestampAndNewline(writer, data); } @@ -303,7 +304,7 @@ private void writeNameAndLabels(OutputStreamWriter writer, String name, String s private void writeMetadata(OutputStreamWriter writer, String suffix, String typeString, MetricMetadata metadata) throws IOException { if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { writer.write("# HELP "); - writer.write(metadata.getName()); + writer.write(metadata.getPrometheusName()); if (suffix != null) { writer.write(suffix); } @@ -312,7 +313,7 @@ private void writeMetadata(OutputStreamWriter writer, String suffix, String type writer.write('\n'); } writer.write("# TYPE "); - writer.write(metadata.getName()); + writer.write(metadata.getPrometheusName()); if (suffix != null) { writer.write(suffix); } 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 5bb15c312..423fa6692 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 @@ -6,6 +6,8 @@ import java.io.OutputStreamWriter; import java.io.Writer; +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName; + public class TextFormatUtil { static void writeLong(OutputStreamWriter writer, long value) throws IOException { @@ -61,7 +63,7 @@ static void writeLabels(OutputStreamWriter writer, Labels labels, String additio if (i > 0) { writer.write(","); } - writer.write(labels.getName(i)); + writer.write(labels.getPrometheusName(i)); writer.write("=\""); writeEscapedLabelValue(writer, labels.getValue(i)); 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 233953e00..7c9e714df 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 @@ -23,6 +23,7 @@ import io.prometheus.metrics.model.snapshots.UnknownSnapshot; import io.prometheus.metrics.model.snapshots.UnknownSnapshot.UnknownDataPointSnapshot; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import java.io.ByteArrayOutputStream; @@ -32,6 +33,7 @@ public class ExpositionFormatsTest { private final String exemplar1String = "{env=\"prod\",span_id=\"12345\",trace_id=\"abcde\"} 1.7 1672850685.829"; private final String exemplar2String = "{env=\"dev\",span_id=\"23456\",trace_id=\"bcdef\"} 2.4 1672850685.830"; + private final String exemplarWithDotsString = "{some_exemplar_key=\"some value\"} 3.0 1690298864.383"; private final String exemplar1protoString = "exemplar { " + "label { name: \"env\" value: \"prod\" } " + @@ -47,6 +49,11 @@ public class ExpositionFormatsTest { "value: 2.4 " + "timestamp { seconds: 1672850685 nanos: 830000000 } }"; + private final String exemplarWithDotsProtoString = "exemplar { " + + "label { name: \"some_exemplar_key\" value: \"some value\" } " + + "value: 3.0 " + + "timestamp { seconds: 1690298864 nanos: 383000000 } }"; + private final String createdTimestamp1s = "1672850385.800"; private final long createdTimestamp1 = (long) (1000 * Double.parseDouble(createdTimestamp1s)); private final String createdTimestamp2s = "1672850285.000"; @@ -72,6 +79,12 @@ public class ExpositionFormatsTest { .withTimestampMillis(1672850685830L) .build(); + private final Exemplar exemplarWithDots = Exemplar.newBuilder() + .withLabels(Labels.of("some.exemplar.key", "some value")) + .withValue(3.0) + .withTimestampMillis(1690298864383L) + .build(); + @Test public void testCounterComplete() throws IOException { String openMetricsText = "" + @@ -183,6 +196,42 @@ public void testCounterMinimal() throws IOException { assertPrometheusProtobuf(prometheusProtobuf, counter); } + @Test + public void testCounterWithDots() throws IOException { + String openMetricsText = "" + + "# TYPE my_request_count counter\n" + + "my_request_count_total{http_path=\"/hello\"} 3.0 # " + exemplarWithDotsString + "\n" + + "# EOF\n"; + String prometheusText = "" + + "# TYPE my_request_count_total counter\n" + + "my_request_count_total{http_path=\"/hello\"} 3.0\n"; + String prometheusProtobuf = "" + + //@formatter:off + "name: \"my_request_count_total\" " + + "type: COUNTER " + + "metric { " + + "label { name: \"http_path\" value: \"/hello\" } " + + "counter { " + + "value: 3.0 " + exemplarWithDotsProtoString + " " + + "} " + + "}"; + //@formatter:on + + CounterSnapshot counter = CounterSnapshot.newBuilder() + .withName("my.request.count") + .addDataPoint(CounterDataPointSnapshot.newBuilder() + .withValue(3.0) + .withLabels(Labels.newBuilder() + .addLabel("http.path", "/hello") + .build()) + .withExemplar(exemplarWithDots) + .build()) + .build(); + assertOpenMetricsText(openMetricsText, counter); + assertPrometheusText(prometheusText, counter); + assertPrometheusProtobuf(prometheusProtobuf, counter); + } + @Test public void testGaugeComplete() throws IOException { String openMetricsText = "" + @@ -264,6 +313,48 @@ public void testGaugeMinimal() throws IOException { assertPrometheusProtobuf(prometheusProtobuf, gauge); } + @Test + public void testGaugeWithDots() throws IOException { + String openMetricsText = "" + + "# TYPE my_temperature_celsius gauge\n" + + "# UNIT my_temperature_celsius celsius\n" + + "# HELP my_temperature_celsius Temperature\n" + + "my_temperature_celsius{location_id=\"data-center-1\"} 23.0 # " + exemplarWithDotsString + "\n" + + "# EOF\n"; + String prometheusText = "" + + "# HELP my_temperature_celsius Temperature\n" + + "# TYPE my_temperature_celsius gauge\n" + + "my_temperature_celsius{location_id=\"data-center-1\"} 23.0\n"; + String prometheusProtobuf = "" + + //@formatter:off + "name: \"my_temperature_celsius\" " + + "help: \"Temperature\" " + + "type: GAUGE " + + "metric { " + + "label { name: \"location_id\" value: \"data-center-1\" } " + + "gauge { " + + "value: 23.0 " + + "} " + + "}"; + //@formatter:on + + GaugeSnapshot gauge = GaugeSnapshot.newBuilder() + .withName("my.temperature.celsius") + .withHelp("Temperature") + .withUnit(Unit.CELSIUS) + .addDataPoint(GaugeDataPointSnapshot.newBuilder() + .withValue(23.0) + .withLabels(Labels.newBuilder() + .addLabel("location.id", "data-center-1") + .build()) + .withExemplar(exemplarWithDots) + .build()) + .build(); + assertOpenMetricsText(openMetricsText, gauge); + assertPrometheusText(prometheusText, gauge); + assertPrometheusProtobuf(prometheusProtobuf, gauge); + } + @Test public void testSummaryComplete() throws IOException { String openMetricsText = "" + @@ -596,6 +687,49 @@ public void testSummaryEmptyAndNonEmpty() throws IOException { assertPrometheusProtobuf(prometheusProtobuf, summary); } + @Test + public void testSummaryWithDots() throws IOException { + String openMetricsText = "" + + "# TYPE my_request_duration_seconds summary\n" + + "# UNIT my_request_duration_seconds seconds\n" + + "# HELP my_request_duration_seconds Request duration in seconds\n" + + "my_request_duration_seconds_count{http_path=\"/hello\"} 1 # " + exemplarWithDotsString + "\n" + + "my_request_duration_seconds_sum{http_path=\"/hello\"} 0.03 # " + exemplarWithDotsString + "\n" + + "# EOF\n"; + String prometheusText = "" + + "# HELP my_request_duration_seconds Request duration in seconds\n" + + "# TYPE my_request_duration_seconds summary\n" + + "my_request_duration_seconds_count{http_path=\"/hello\"} 1\n" + + "my_request_duration_seconds_sum{http_path=\"/hello\"} 0.03\n"; + String prometheusProtobuf = "" + + //@formatter:off + "name: \"my_request_duration_seconds\" " + + "help: \"Request duration in seconds\" " + + "type: SUMMARY " + + "metric { " + + "label { name: \"http_path\" value: \"/hello\" } " + + "summary { sample_count: 1 sample_sum: 0.03 } " + + "}"; + //@formatter:on + + SummarySnapshot summary = SummarySnapshot.newBuilder() + .withName("my.request.duration.seconds") + .withHelp("Request duration in seconds") + .withUnit(Unit.SECONDS) + .addDataPoint(SummaryDataPointSnapshot.newBuilder() + .withCount(1) + .withSum(0.03) + .withLabels(Labels.newBuilder() + .addLabel("http.path", "/hello") + .build()) + .withExemplars(Exemplars.of(exemplarWithDots)) + .build()) + .build(); + assertOpenMetricsText(openMetricsText, summary); + assertPrometheusText(prometheusText, summary); + assertPrometheusProtobuf(prometheusProtobuf, summary); + } + @Test public void testClassicHistogramComplete() throws Exception { String openMetricsText = "" + @@ -1047,6 +1181,57 @@ public void testClassicGaugeHistogramCountAndSum() throws IOException { assertPrometheusProtobuf(prometheusProtobuf, gaugeHistogram); } + @Test + public void testClassicHistogramWithDots() throws IOException { + String openMetricsText = "" + + "# TYPE my_request_duration_seconds histogram\n" + + "# UNIT my_request_duration_seconds seconds\n" + + "# HELP my_request_duration_seconds Request duration in seconds\n" + + "my_request_duration_seconds_bucket{http_path=\"/hello\",le=\"+Inf\"} 130 # " + exemplarWithDotsString + "\n" + + "my_request_duration_seconds_count{http_path=\"/hello\"} 130\n" + + "my_request_duration_seconds_sum{http_path=\"/hello\"} 0.01\n" + + "# EOF\n"; + String prometheusText = "" + + "# HELP my_request_duration_seconds Request duration in seconds\n" + + "# TYPE my_request_duration_seconds histogram\n" + + "my_request_duration_seconds_bucket{http_path=\"/hello\",le=\"+Inf\"} 130\n" + + "my_request_duration_seconds_count{http_path=\"/hello\"} 130\n" + + "my_request_duration_seconds_sum{http_path=\"/hello\"} 0.01\n"; + String prometheusProtobuf = "" + + //@formatter:off + "name: \"my_request_duration_seconds\" " + + "help: \"Request duration in seconds\" " + + "type: HISTOGRAM " + + "metric { " + + "label { name: \"http_path\" value: \"/hello\" } " + + "histogram { " + + "sample_count: 130 " + + "sample_sum: 0.01 " + + "bucket { cumulative_count: 130 upper_bound: Infinity " + exemplarWithDotsProtoString + " } " + + "} " + + "}"; + //@formatter:on + + HistogramSnapshot histogram = HistogramSnapshot.newBuilder() + .withName("my.request.duration.seconds") + .withHelp("Request duration in seconds") + .withUnit(Unit.SECONDS) + .addDataPoint(HistogramSnapshot.HistogramDataPointSnapshot.newBuilder() + .withSum(0.01) + .withLabels(Labels.newBuilder() + .addLabel("http.path", "/hello") + .build()) + .withClassicHistogramBuckets(ClassicHistogramBuckets.newBuilder() + .addBucket(Double.POSITIVE_INFINITY, 130) + .build()) + .withExemplars(Exemplars.of(exemplarWithDots)) + .build()) + .build(); + assertOpenMetricsText(openMetricsText, histogram); + assertPrometheusText(prometheusText, histogram); + assertPrometheusProtobuf(prometheusProtobuf, histogram); + } + @Test public void testNativeHistogramComplete() throws IOException { String openMetricsText = "" + @@ -1244,6 +1429,64 @@ public void testNativeHistogramMinimal() throws IOException { assertPrometheusProtobuf(prometheusProtobuf, nativeHistogram); } + @Test + public void testNativeHistogramWithDots() throws IOException { + String openMetricsText = "" + + "# TYPE my_request_duration_seconds histogram\n" + + "# UNIT my_request_duration_seconds seconds\n" + + "# HELP my_request_duration_seconds Request duration in seconds\n" + + "my_request_duration_seconds_bucket{http_path=\"/hello\",le=\"+Inf\"} 4 # " + exemplarWithDotsString + "\n" + + "my_request_duration_seconds_count{http_path=\"/hello\"} 4\n" + + "my_request_duration_seconds_sum{http_path=\"/hello\"} 3.2\n" + + "# EOF\n"; + String prometheusText = "" + + "# HELP my_request_duration_seconds Request duration in seconds\n" + + "# TYPE my_request_duration_seconds histogram\n" + + "my_request_duration_seconds_bucket{http_path=\"/hello\",le=\"+Inf\"} 4\n" + + "my_request_duration_seconds_count{http_path=\"/hello\"} 4\n" + + "my_request_duration_seconds_sum{http_path=\"/hello\"} 3.2\n"; + String prometheusProtobuf = "" + + //@formatter:off + "name: \"my_request_duration_seconds\" " + + "help: \"Request duration in seconds\" " + + "type: HISTOGRAM " + + "metric { " + + "label { name: \"http_path\" value: \"/hello\" } " + + "histogram { " + + "sample_count: 4 " + + "sample_sum: 3.2 " + + "bucket { cumulative_count: 4 upper_bound: Infinity " + exemplarWithDotsProtoString + " } " + + "schema: 5 " + + "zero_threshold: 0.0 " + + "zero_count: 1 " + + "positive_span { offset: 2 length: 1 } " + + "positive_delta: 3 " + + "} " + + "}"; + //@formatter:on + + HistogramSnapshot histogram = HistogramSnapshot.newBuilder() + .withName("my.request.duration.seconds") + .withHelp("Request duration in seconds") + .withUnit(Unit.SECONDS) + .addDataPoint(HistogramSnapshot.HistogramDataPointSnapshot.newBuilder() + .withLabels(Labels.newBuilder() + .addLabel("http.path", "/hello") + .build()) + .withSum(3.2) + .withNativeSchema(5) + .withNativeZeroCount(1) + .withNativeBucketsForPositiveValues(NativeHistogramBuckets.newBuilder() + .addBucket(2, 3) + .build() + ) + .withExemplars(Exemplars.of(exemplarWithDots)) + .build()) + .build(); + assertOpenMetricsText(openMetricsText, histogram); + assertPrometheusText(prometheusText, histogram); + assertPrometheusProtobuf(prometheusProtobuf, histogram); + } // TODO: Gauge Native Histogram @Test @@ -1270,6 +1513,39 @@ public void testInfo() throws IOException { assertPrometheusTextWithoutCreated(prometheus, info); } + @Test + public void testInfoWithDots() throws IOException { + String openMetricsText = "" + + "# TYPE jvm_status info\n" + + "# HELP jvm_status JVM status info\n" + + "jvm_status_info{jvm_version=\"1.2.3\"} 1\n" + + "# EOF\n"; + String prometheusText = "" + + "# HELP jvm_status_info JVM status info\n" + + "# TYPE jvm_status_info gauge\n" + + "jvm_status_info{jvm_version=\"1.2.3\"} 1\n"; + String prometheusProtobuf = "" + + //@formatter:off + "name: \"jvm_status_info\" " + + "help: \"JVM status info\" " + + "type: GAUGE " + + "metric { " + "" + + "label { name: \"jvm_version\" value: \"1.2.3\" } " + + "gauge { value: 1.0 } " + + "}"; + //@formatter:on + InfoSnapshot info = InfoSnapshot.newBuilder() + .withName("jvm.status") + .withHelp("JVM status info") + .addDataPoint(InfoSnapshot.InfoDataPointSnapshot.newBuilder() + .withLabels(Labels.of("jvm.version", "1.2.3")) + .build()) + .build(); + assertOpenMetricsText(openMetricsText, info); + assertPrometheusText(prometheusText, info); + assertPrometheusProtobuf(prometheusProtobuf, info); + } + @Test public void testStateSetComplete() throws IOException { String openMetrics = "" + @@ -1333,6 +1609,48 @@ public void testStateSetMinimal() throws IOException { assertPrometheusTextWithoutCreated(prometheus, stateSet); } + @Test + public void testStateSetWithDots() throws IOException { + String openMetricsText = "" + + "# TYPE my_application_state stateset\n" + + "# HELP my_application_state My application state\n" + + "my_application_state{data_center=\"us east\",my_application_state=\"feature.enabled\"} 1\n" + + "my_application_state{data_center=\"us east\",my_application_state=\"is.alpha.version\"} 0\n" + + "# EOF\n"; + String prometheusText = "" + + "# HELP my_application_state My application state\n" + + "# TYPE my_application_state gauge\n" + + "my_application_state{data_center=\"us east\",my_application_state=\"feature.enabled\"} 1\n" + + "my_application_state{data_center=\"us east\",my_application_state=\"is.alpha.version\"} 0\n"; + String prometheusProtobuf = "" + + //@formatter:off + "name: \"my_application_state\" " + + "help: \"My application state\" " + + "type: GAUGE " + + "metric { " + + "label { name: \"data_center\" value: \"us east\" } " + + "label { name: \"my_application_state\" value: \"feature.enabled\" } " + + "gauge { value: 1.0 } " + + "} metric { " + + "label { name: \"data_center\" value: \"us east\" } " + + "label { name: \"my_application_state\" value: \"is.alpha.version\" } " + + "gauge { value: 0.0 } " + + "}"; + //@formatter:on + StateSetSnapshot stateSet = StateSetSnapshot.newBuilder() + .withName("my.application.state") + .withHelp("My application state") + .addDataPoint(StateSetSnapshot.StateSetDataPointSnapshot.newBuilder() + .withLabels(Labels.of("data.center", "us east")) + .addState("feature.enabled", true) + .addState("is.alpha.version", false) + .build()) + .build(); + assertOpenMetricsText(openMetricsText, stateSet); + assertPrometheusText(prometheusText, stateSet); + assertPrometheusProtobuf(prometheusProtobuf, stateSet); + } + @Test public void testUnknownComplete() throws IOException { String openMetrics = "" + @@ -1391,6 +1709,43 @@ public void testUnknownMinimal() throws IOException { assertPrometheusTextWithoutCreated(prometheus, unknown); } + @Test + public void testUnknownWithDots() throws IOException { + String openMetrics = "" + + "# TYPE some_unknown_metric unknown\n" + + "# UNIT some_unknown_metric bytes\n" + + "# HELP some_unknown_metric help message\n" + + "some_unknown_metric{test_env=\"7\"} 0.7 # " + exemplarWithDotsString + "\n" + + "# EOF\n"; + String prometheus = "" + + "# HELP some_unknown_metric help message\n" + + "# TYPE some_unknown_metric untyped\n" + + "some_unknown_metric{test_env=\"7\"} 0.7\n"; + String prometheusProtobuf = "" + + //@formatter:off + "name: \"some_unknown_metric\" " + + "help: \"help message\" " + + "type: UNTYPED " + + "metric { " + + "label { name: \"test_env\" value: \"7\" } " + + "untyped { value: 0.7 } " + + "}"; + //@formatter:on + UnknownSnapshot unknown = UnknownSnapshot.newBuilder() + .withName("some.unknown.metric") + .withHelp("help message") + .withUnit(Unit.BYTES) + .addDataPoint(UnknownDataPointSnapshot.newBuilder() + .withValue(0.7) + .withLabels(Labels.of("test.env", "7")) + .withExemplar(exemplarWithDots) + .build()) + .build(); + assertOpenMetricsText(openMetrics, unknown); + assertPrometheusText(prometheus, unknown); + assertPrometheusProtobuf(prometheusProtobuf, unknown); + } + @Test public void testHelpEscape() throws IOException { String openMetrics = "" + diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java index b0fba2db1..349ebda60 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java @@ -1,11 +1,11 @@ package io.prometheus.metrics.model.registry; import io.prometheus.metrics.model.snapshots.MetricSnapshot; -import io.prometheus.metrics.model.snapshots.MetricSnapshots; -import java.util.List; import java.util.function.Predicate; +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName; + /** * To be registered with the Prometheus collector registry. * See Overall Structure on @@ -26,7 +26,7 @@ public interface Collector { */ default MetricSnapshot collect(Predicate includedNames) { MetricSnapshot result = collect(); - if (includedNames.test(result.getMetadata().getName())) { + if (includedNames.test(result.getMetadata().getPrometheusName())) { return result; } else { return null; @@ -36,7 +36,7 @@ default MetricSnapshot collect(Predicate includedNames) { /** * Override this and return {@code null} if a collector does not have a constant name (name may change between scrapes). */ - default String getName() { - return collect().getMetadata().getName(); + default String getPrometheusName() { + return collect().getMetadata().getPrometheusName(); } } diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MultiCollector.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MultiCollector.java index 3fd56e493..8e5aff15f 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MultiCollector.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MultiCollector.java @@ -7,6 +7,8 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName; + /** * Like {@link Collector}, but collecting multiple Snapshots at once. */ @@ -27,7 +29,7 @@ default MetricSnapshots collect(Predicate includedNames) { MetricSnapshots allSnapshots = collect(); MetricSnapshots.Builder result = MetricSnapshots.newBuilder(); for (MetricSnapshot snapshot : allSnapshots) { - if (includedNames.test(snapshot.getMetadata().getName())) { + if (includedNames.test(snapshot.getMetadata().getPrometheusName())) { result.addMetricSnapshot(snapshot); } } @@ -38,7 +40,7 @@ default MetricSnapshots collect(Predicate includedNames) { * Override this and return an empty list if the MultiCollector does not return a constant list of names * (names may be added / removed between scrapes). */ - default List getNames() { - return collect().stream().map(snapshot -> snapshot.getMetadata().getName()).collect(Collectors.toList()); + default List getPrometheusNames() { + return collect().stream().map(snapshot -> snapshot.getMetadata().getPrometheusName()).collect(Collectors.toList()); } } diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java index 858a1b37d..373d32a89 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java @@ -9,26 +9,30 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Predicate; +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName; + public class PrometheusRegistry { public static final PrometheusRegistry defaultRegistry = new PrometheusRegistry(); - private final Set names = ConcurrentHashMap.newKeySet(); + private final Set prometheusNames = ConcurrentHashMap.newKeySet(); private final List collectors = new CopyOnWriteArrayList<>(); private final List multiCollectors = new CopyOnWriteArrayList<>(); public void register(Collector collector) { - String name = collector.getName(); - if (!names.add(name)) { - throw new IllegalStateException("Can't register " + name + " because that name is already registered."); + String prometheusName = collector.getPrometheusName(); + if (prometheusName != null) { + if (!prometheusNames.add(prometheusName)) { + throw new IllegalStateException("Can't register " + prometheusName + " because a metric with that name is already registered."); + } } collectors.add(collector); } public void register(MultiCollector collector) { - for (String name : collector.getNames()) { - if (!names.add(name)) { - throw new IllegalStateException("Can't register " + name + " because that name is already registered."); + for (String prometheusName : collector.getPrometheusNames()) { + if (!prometheusNames.add(prometheusName)) { + throw new IllegalStateException("Can't register " + prometheusName + " because that name is already registered."); } } multiCollectors.add(collector); @@ -36,23 +40,32 @@ public void register(MultiCollector collector) { public void unregister(Collector collector) { collectors.remove(collector); - names.remove(collector.getName()); + prometheusNames.remove(collector.getPrometheusName()); } public void unregister(MultiCollector collector) { multiCollectors.remove(collector); - for (String name : collector.getNames()) { - names.remove(name); + for (String prometheusName : collector.getPrometheusNames()) { + prometheusNames.remove(prometheusName(prometheusName)); } } public MetricSnapshots scrape() { MetricSnapshots.Builder result = MetricSnapshots.newBuilder(); for (Collector collector : collectors) { - result.addMetricSnapshot(collector.collect()); + MetricSnapshot snapshot = collector.collect(); + if (snapshot != null) { + if (result.containsMetricName(snapshot.getMetadata().getName())) { + throw new IllegalStateException(snapshot.getMetadata().getPrometheusName() + ": duplicate metric name."); + } + result.addMetricSnapshot(snapshot); + } } for (MultiCollector collector : multiCollectors) { for (MetricSnapshot snapshot : collector.collect()) { + if (result.containsMetricName(snapshot.getMetadata().getName())) { + throw new IllegalStateException(snapshot.getMetadata().getPrometheusName() + ": duplicate metric name."); + } result.addMetricSnapshot(snapshot); } } @@ -65,8 +78,8 @@ public MetricSnapshots scrape(Predicate includedNames) { } MetricSnapshots.Builder result = MetricSnapshots.newBuilder(); for (Collector collector : collectors) { - String name = collector.getName(); - if (name == null || includedNames.test(name)) { + String prometheusName = collector.getPrometheusName(); + if (prometheusName == null || includedNames.test(prometheusName)) { MetricSnapshot snapshot = collector.collect(includedNames); if (snapshot != null) { result.addMetricSnapshot(snapshot); @@ -74,10 +87,10 @@ public MetricSnapshots scrape(Predicate includedNames) { } } for (MultiCollector collector : multiCollectors) { - List names = collector.getNames(); + List prometheusNames = collector.getPrometheusNames(); boolean excluded = true; // the multi-collector is excluded unless at least one name matches - for (String name : names) { - if (includedNames.test(name)) { + for (String prometheusName : prometheusNames) { + if (includedNames.test(prometheusName)) { excluded = false; break; } diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/InfoSnapshot.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/InfoSnapshot.java index b49e0d121..dac311004 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/InfoSnapshot.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/InfoSnapshot.java @@ -20,9 +20,6 @@ public final class InfoSnapshot extends MetricSnapshot { */ public InfoSnapshot(MetricMetadata metadata, Collection data) { super(metadata, data); - if (metadata.getName().endsWith("_info")) { - throw new IllegalArgumentException("The name of an info snapshot must not include the _info suffix"); - } if (metadata.hasUnit()) { throw new IllegalArgumentException("An Info metric cannot have a unit."); } 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 8f9d36ea4..60b1b58be 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 @@ -5,23 +5,37 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.regex.Pattern; import java.util.stream.Stream; +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.isValidLabelName; +import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName; + /** * Immutable set of name/value pairs, sorted by name. */ public class Labels implements Comparable, Iterable