Skip to content

Commit 1a955b1

Browse files
committed
Allow dots in metric names and label names
Signed-off-by: Fabian Stäber <[email protected]>
1 parent 0d232b4 commit 1a955b1

File tree

35 files changed

+1096
-317
lines changed

35 files changed

+1096
-317
lines changed

examples/example-exemplars-tail-sampling/example-greeting-service/src/main/java/io/prometheus/metrics/examples/otel_exemplars/greeting/GreetingServlet.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public GreetingServlet() {
2727
.withUnit(Unit.SECONDS)
2828
.withLabelNames("http_status")
2929
.register();
30-
histogram.withLabelValues("200");
30+
histogram.initLabelValues("200");
3131
}
3232

3333
@Override

examples/example-exemplars-tail-sampling/example-hello-world-app/src/main/java/io/prometheus/metrics/examples/otel_exemplars/app/HelloWorldServlet.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public HelloWorldServlet() {
3434
.withUnit(Unit.SECONDS)
3535
.withLabelNames("http_status")
3636
.register();
37-
histogram.withLabelValues("200");
37+
histogram.initLabelValues("200");
3838
}
3939

4040
@Override

examples/example-tomcat-servlet/src/main/java/io/prometheus/metrics/examples/tomcat_servlet/HelloWorldServlet.java

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ public class HelloWorldServlet extends HttpServlet {
3232
.withLabelNames("http_status")
3333
.register();
3434

35+
public HelloWorldServlet() {
36+
counter.initLabelValues("200");
37+
histogram.initLabelValues("200");
38+
}
39+
3540
@Override
3641
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
3742
long start = System.nanoTime();

prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public MetricsProperties getDefaultMetricProperties() {
5858
* May return {@code null} if no metric-specific properties are configured for a metric name.
5959
*/
6060
public MetricsProperties getMetricProperties(String metricName) {
61-
return metricProperties.get(metricName);
61+
return metricProperties.get(metricName.replace(".", "_"));
6262
}
6363

6464
public ExemplarsProperties getExemplarProperties() {

prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public static PrometheusProperties load() throws PrometheusPropertiesException {
3939
// This will remove entries from properties when they are processed.
4040
private static Map<String, MetricsProperties> loadMetricsConfigs(Map<Object, Object> properties) {
4141
Map<String, MetricsProperties> result = new HashMap<>();
42+
// Note that the metric name in the properties file must be as exposed in the Prometheus exposition formats,
43+
// i.e. all dots replaced with underscores.
4244
Pattern pattern = Pattern.compile("io\\.prometheus\\.metrics\\.([^.]+)\\.");
4345
// Create a copy of the keySet() for iterating. We cannot iterate directly over keySet()
4446
// because entries are removed when MetricsConfig.load(...) is called.
@@ -49,7 +51,7 @@ private static Map<String, MetricsProperties> loadMetricsConfigs(Map<Object, Obj
4951
for (String propertyName : propertyNames) {
5052
Matcher matcher = pattern.matcher(propertyName);
5153
if (matcher.find()) {
52-
String metricName = matcher.group(1);
54+
String metricName = matcher.group(1).replace(".", "_");
5355
if (!result.containsKey(metricName)) {
5456
result.put(metricName, MetricsProperties.load("io.prometheus.metrics." + metricName, properties));
5557
}

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
import io.prometheus.metrics.model.snapshots.CounterSnapshot;
1010
import io.prometheus.metrics.model.snapshots.Exemplar;
1111
import io.prometheus.metrics.model.snapshots.Labels;
12+
import io.prometheus.metrics.model.snapshots.PrometheusNaming;
1213

1314
import java.util.ArrayList;
1415
import java.util.Collections;
1516
import java.util.List;
1617
import java.util.concurrent.atomic.DoubleAdder;
1718
import java.util.concurrent.atomic.LongAdder;
1819

20+
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
21+
1922
/**
2023
* Counter metric.
2124
* <p>
@@ -109,8 +112,8 @@ protected CounterSnapshot collect(List<Labels> labels, List<DataPoint> metricDat
109112
return new CounterSnapshot(getMetadata(), data);
110113
}
111114

112-
static String normalizeName(String name) {
113-
if (name != null && name.endsWith("_total")) {
115+
static String stripTotalSuffix(String name) {
116+
if (name != null && (name.endsWith("_total") || name.endsWith(".total"))) {
114117
name = name.substring(0, name.length() - 6);
115118
}
116119
return name;
@@ -231,12 +234,12 @@ private Builder(PrometheusProperties properties) {
231234
* In the example above both {@code c1} and {@code c2} would be named {@code "events_total"} in Prometheus.
232235
* <p>
233236
* Throws an {@link IllegalArgumentException} if
234-
* {@link io.prometheus.metrics.model.snapshots.MetricMetadata#isValidMetricName(String) MetricMetadata.isValidMetricName(name)}
237+
* {@link io.prometheus.metrics.model.snapshots.PrometheusNaming#isValidMetricName(String) MetricMetadata.isValidMetricName(name)}
235238
* is {@code false}.
236239
*/
237240
@Override
238241
public Builder withName(String name) {
239-
return super.withName(normalizeName(name));
242+
return super.withName(stripTotalSuffix(name));
240243
}
241244

242245
@Override

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/CounterWithCallback.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,12 @@ private Builder(PrometheusProperties properties) {
8080
* In the example above both {@code c1} and {@code c2} would be named {@code "events_total"} in Prometheus.
8181
* <p>
8282
* Throws an {@link IllegalArgumentException} if
83-
* {@link io.prometheus.metrics.model.snapshots.MetricMetadata#isValidMetricName(String) MetricMetadata.isValidMetricName(name)}
83+
* {@link io.prometheus.metrics.model.snapshots.PrometheusNaming#isValidMetricName(String) MetricMetadata.isValidMetricName(name)}
8484
* is {@code false}.
8585
*/
8686
@Override
8787
public Builder withName(String name) {
88-
return super.withName(Counter.normalizeName(name));
88+
return super.withName(Counter.stripTotalSuffix(name));
8989
}
9090

9191
@Override

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java

+7-10
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33
import io.prometheus.metrics.config.PrometheusProperties;
44
import io.prometheus.metrics.model.snapshots.InfoSnapshot;
5-
import io.prometheus.metrics.model.snapshots.Label;
65
import io.prometheus.metrics.model.snapshots.Labels;
76
import io.prometheus.metrics.model.snapshots.Unit;
87

98
import java.util.ArrayList;
10-
import java.util.Arrays;
119
import java.util.Collections;
1210
import java.util.List;
1311
import java.util.concurrent.CopyOnWriteArrayList;
1412

13+
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
14+
1515
/**
1616
* Info metric. Example:
1717
* <pre>{@code
@@ -90,15 +90,12 @@ private Builder(PrometheusProperties config) {
9090
* In the example above both {@code info1} and {@code info2} will be named {@code "runtime_info"} in Prometheus.
9191
* <p>
9292
* Throws an {@link IllegalArgumentException} if
93-
* {@link io.prometheus.metrics.model.snapshots.MetricMetadata#isValidMetricName(String) MetricMetadata.isValidMetricName(name)}
93+
* {@link io.prometheus.metrics.model.snapshots.PrometheusNaming#isValidMetricName(String) MetricMetadata.isValidMetricName(name)}
9494
* is {@code false}.
9595
*/
9696
@Override
9797
public Builder withName(String name) {
98-
if (name != null && name.endsWith("_info")) {
99-
name = name.substring(0, name.length() - 5);
100-
}
101-
return super.withName(name);
98+
return super.withName(stripInfoSuffix(name));
10299
}
103100

104101
/**
@@ -112,16 +109,16 @@ public Builder withUnit(Unit unit) {
112109
return this;
113110
}
114111

115-
private static String normalizeName(String name) {
116-
if (name != null && name.endsWith("_info")) {
112+
private static String stripInfoSuffix(String name) {
113+
if (name != null && (name.endsWith("_info") || name.endsWith(".info"))) {
117114
name = name.substring(0, name.length() - 5);
118115
}
119116
return name;
120117
}
121118

122119
@Override
123120
public Info build() {
124-
return new Info(withName(normalizeName(name)));
121+
return new Info(this);
125122
}
126123

127124
@Override

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java

+7-5
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
import io.prometheus.metrics.config.PrometheusProperties;
44
import io.prometheus.metrics.model.snapshots.Labels;
55
import io.prometheus.metrics.model.snapshots.MetricMetadata;
6+
import io.prometheus.metrics.model.snapshots.PrometheusNaming;
67
import io.prometheus.metrics.model.snapshots.Unit;
78

89
import java.util.Arrays;
910
import java.util.List;
1011

12+
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
13+
1114
/**
1215
* Almost all metrics have fixed metadata, i.e. the metric name is known when the metric is created.
1316
* <p>
@@ -31,9 +34,8 @@ protected MetricMetadata getMetadata() {
3134

3235
private String makeName(String name, Unit unit) {
3336
if (unit != null) {
34-
String suffix = "_" + unit;
35-
if (!name.endsWith(suffix)) {
36-
name = name + suffix;
37+
if (!name.endsWith(unit.toString())) {
38+
name = name + "_" + unit;
3739
}
3840
}
3941
return name;
@@ -51,7 +53,7 @@ protected Builder(List<String> illegalLabelNames, PrometheusProperties propertie
5153
}
5254

5355
public B withName(String name) {
54-
if (!MetricMetadata.isValidMetricName(name)) {
56+
if (!PrometheusNaming.isValidMetricName(name)) {
5557
throw new IllegalArgumentException("'" + name + "': Illegal metric name.");
5658
}
5759
this.name = name;
@@ -70,7 +72,7 @@ public B withHelp(String help) {
7072

7173
public B withLabelNames(String... labelNames) {
7274
for (String labelName : labelNames) {
73-
if (!Labels.isValidLabelName(labelName)) {
75+
if (!PrometheusNaming.isValidLabelName(labelName)) {
7476
throw new IllegalArgumentException(labelName + ": illegal label name");
7577
}
7678
if (illegalLabelNames.contains(labelName)) {

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StateSet.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.prometheus.metrics.config.MetricsProperties;
44
import io.prometheus.metrics.config.PrometheusProperties;
55
import io.prometheus.metrics.model.snapshots.Labels;
6+
import io.prometheus.metrics.model.snapshots.MetricMetadata;
67
import io.prometheus.metrics.model.snapshots.StateSetSnapshot;
78
import io.prometheus.metrics.core.datapoints.StateSetDataPoint;
89

@@ -11,6 +12,8 @@
1112
import java.util.List;
1213
import java.util.stream.Stream;
1314

15+
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
16+
1417
/**
1518
* StateSet metric. Example:
1619
* <pre>{@code
@@ -58,7 +61,7 @@ private StateSet(Builder builder, PrometheusProperties prometheusProperties) {
5861
exemplarsEnabled = getConfigProperty(properties, MetricsProperties::getExemplarsEnabled);
5962
this.names = builder.names; // builder.names is already a validated copy
6063
for (String name : names) {
61-
if (this.getMetadata().getName().equals(name)) {
64+
if (this.getMetadata().getPrometheusName().equals(prometheusName(name))) {
6265
throw new IllegalArgumentException("Label name " + name + " is illegal (can't use the metric name as label name in state set metrics)");
6366
}
6467
}

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StatefulMetric.java

+28-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,34 @@ public MetricSnapshot collect() {
6262
return collect(labels, metricData);
6363
}
6464

65+
/**
66+
* Initialize label values.
67+
* <p>
68+
* Example: Imagine you have a counter for payments as follows
69+
* <pre>
70+
* payment_transactions_total{payment_type="credit card"} 7.0
71+
* payment_transactions_total{payment_type="paypal"} 3.0
72+
* </pre>
73+
* Now, the data points for the {@code payment_type} label values get initialized when they are
74+
* first used, i.e. the first time you call
75+
* <pre>{@code
76+
* counter.withLabelValues("paypal").inc();
77+
* }</pre>
78+
* the data point with label {@code payment_type="paypal"} will go from non-existent to having value {@code 1.0}.
79+
* <p>
80+
* In some cases this is confusing, and you want to have data points initialized on application start
81+
* with an initial value of {@code 0.0}:
82+
* <pre>
83+
* payment_transactions_total{payment_type="credit card"} 0.0
84+
* payment_transactions_total{payment_type="paypal"} 0.0
85+
* </pre>
86+
* {@code initLabelValues(...)} can be used to initialize label value, so that the data points
87+
* show up in the exposition format with an initial value of zero.
88+
*/
89+
public void initLabelValues(String... labelValues) {
90+
withLabelValues(labelValues);
91+
}
92+
6593
public D withLabelValues(String... labelValues) {
6694
if (labelValues.length != labelNames.length) {
6795
if (labelValues.length == 0) {
@@ -73,7 +101,6 @@ public D withLabelValues(String... labelValues) {
73101
return data.computeIfAbsent(Arrays.asList(labelValues), l -> newDataPoint());
74102
}
75103

76-
// TODO: Remove automatically if label values have not been used in a while?
77104
public void remove(String... labelValues) {
78105
data.remove(Arrays.asList(labelValues));
79106
}

prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java

+12-12
Original file line numberDiff line numberDiff line change
@@ -109,18 +109,18 @@ public void testLabels() {
109109

110110
@Test
111111
public void testTotalStrippedFromName() {
112-
Counter counter = Counter.newBuilder()
113-
.withName("my_counter_total")
114-
.withUnit(Unit.SECONDS)
115-
.build();
116-
Metrics.MetricFamily protobufData = new PrometheusProtobufWriter().convert(counter.collect());
117-
assertEquals("name: \"my_counter_seconds_total\" type: COUNTER metric { counter { value: 0.0 } }", TextFormat.printer().shortDebugString(protobufData));
118-
119-
counter = Counter.newBuilder()
120-
.withName("my_counter")
121-
.build();
122-
protobufData = new PrometheusProtobufWriter().convert(counter.collect());
123-
assertEquals("name: \"my_counter_total\" type: COUNTER metric { counter { value: 0.0 } }", TextFormat.printer().shortDebugString(protobufData));
112+
for (String name : new String[]{
113+
"my_counter_total", "my.counter.total",
114+
"my_counter_seconds_total", "my.counter.seconds.total",
115+
"my_counter", "my.counter",
116+
"my_counter_seconds", "my.counter.seconds"}) {
117+
Counter counter = Counter.newBuilder()
118+
.withName(name)
119+
.withUnit(Unit.SECONDS)
120+
.build();
121+
Metrics.MetricFamily protobufData = new PrometheusProtobufWriter().convert(counter.collect());
122+
assertEquals("name: \"my_counter_seconds_total\" type: COUNTER metric { counter { value: 0.0 } }", TextFormat.printer().shortDebugString(protobufData));
123+
}
124124
}
125125

126126
@Test

prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java

+6-16
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
package io.prometheus.metrics.core.metrics;
22

33
import io.prometheus.metrics.com_google_protobuf_3_21_7.TextFormat;
4+
import io.prometheus.metrics.core.datapoints.DistributionDataPoint;
5+
import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfigTestUtil;
46
import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter;
57
import io.prometheus.metrics.expositionformats.PrometheusProtobufWriter;
68
import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_3_21_7.Metrics;
7-
import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfigTestUtil;
8-
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
9-
import io.prometheus.metrics.tracer.common.SpanContext;
10-
import io.prometheus.metrics.tracer.initializer.SpanContextSupplier;
119
import io.prometheus.metrics.model.snapshots.ClassicHistogramBucket;
1210
import io.prometheus.metrics.model.snapshots.Exemplar;
1311
import io.prometheus.metrics.model.snapshots.Exemplars;
1412
import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
1513
import io.prometheus.metrics.model.snapshots.Labels;
16-
import io.prometheus.metrics.core.datapoints.DistributionDataPoint;
14+
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
15+
import io.prometheus.metrics.tracer.common.SpanContext;
16+
import io.prometheus.metrics.tracer.initializer.SpanContextSupplier;
1717
import org.junit.After;
1818
import org.junit.Assert;
1919
import org.junit.Before;
@@ -1043,19 +1043,9 @@ public void testIllegalLabelNamePrefix() {
10431043
.withLabelNames("__hello");
10441044
}
10451045

1046-
@Test(expected = IllegalArgumentException.class)
1047-
public void testIllegalLabelNameDot() {
1048-
// The Prometheus team are investigating to allow dots in future Prometheus versions, but for now it's invalid.
1049-
// The reason is that you cannot use illegal label names in the Prometheus query language.
1050-
Histogram.newBuilder()
1051-
.withName("test")
1052-
.withLabelNames("http.status");
1053-
}
1054-
10551046
@Test(expected = IllegalArgumentException.class)
10561047
public void testIllegalName() {
1057-
Histogram.newBuilder()
1058-
.withName("server.durations");
1048+
Histogram.newBuilder().withName("my_namespace/server.durations");
10591049
}
10601050

10611051
@Test(expected = IllegalArgumentException.class)

0 commit comments

Comments
 (0)