Skip to content

Commit 0c9d5bf

Browse files
authored
Allow metrics with the same name different labels (#1800)
Closes #696 Alternate approach to #1728 Validation occurs at registration time: - Metrics with the same name, type, unit, and help - but different labels, are allowed to be registered - Custom Collectors that do not implement the required fields (prometheus name, metric type) will skip validation and could risk creating duplicate series that will cause issues at scrape time. - I looked into adding a fallback, but it requires calling `collect()` at registration in order to introspect existing metadata and that seemed not great due to potential side effects --------- Signed-off-by: Jay DeLuca <jaydeluca4@gmail.com>
1 parent 39b7be7 commit 0c9d5bf

File tree

36 files changed

+2826
-78
lines changed

36 files changed

+2826
-78
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ docs/public
2424
benchmark-results/
2525
benchmark-results.json
2626
benchmark-output.log
27+
28+
*.DS_Store

docs/content/getting-started/metric-types.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,6 @@ in the `prometheus-metrics-core` API.
364364
However, `prometheus-metrics-model` implements the underlying data model for these types.
365365
To use these types, you need to implement your own `Collector` where the `collect()` method returns
366366
an `UnknownSnapshot` or a `HistogramSnapshot` with `.gaugeHistogram(true)`.
367+
If your custom collector does not implement `getMetricType()` and `getLabelNames()`, ensure it does
368+
not produce the same metric name and label set as another collector, or the exposition may contain
369+
duplicate time series.

docs/content/getting-started/registry.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ weight: 2
66
In order to expose metrics, you need to register them with a `PrometheusRegistry`. We are using a
77
counter as an example here, but the `register()` method is the same for all metric types.
88

9-
## Registering a Metrics with the Default Registry
9+
## Registering a Metric with the Default Registry
1010

1111
```java
1212
Counter eventsTotal = Counter.builder()
@@ -18,7 +18,7 @@ Counter eventsTotal = Counter.builder()
1818
The `register()` call above builds the counter and registers it with the global static
1919
`PrometheusRegistry.defaultRegistry`. Using the default registry is recommended.
2020

21-
## Registering a Metrics with a Custom Registry
21+
## Registering a Metric with a Custom Registry
2222

2323
You can also register your metric with a custom registry:
2424

@@ -78,12 +78,30 @@ Counter eventsTotal2 = Counter.builder()
7878
.register(); // IllegalArgumentException, because a metric with that name is already registered
7979
```
8080

81+
## Validation at registration only
82+
83+
Validation of duplicate metric names and label schemas happens at registration time only.
84+
Built-in metrics (Counter, Gauge, Histogram, etc.) participate in this validation.
85+
86+
Custom collectors that implement the `Collector` or `MultiCollector` interface can optionally
87+
implement `getPrometheusName()` and `getMetricType()` (and the MultiCollector per-name variants) so
88+
the registry can enforce consistency. **Validation is skipped when metric name or type is
89+
unavailable:** if `getPrometheusName()` or `getMetricType()` returns `null`, the registry does not
90+
validate that collector. If two such collectors produce the same metric name and same label set at
91+
scrape time, the exposition output may contain duplicate time series and be invalid for Prometheus.
92+
93+
When validation _is_ performed (name and type are non-null), **null label names are treated as an
94+
empty label schema:** `getLabelNames()` returning `null` is normalized to `Collections.emptySet()`
95+
and full label-schema validation and duplicate detection still apply. A collector that returns a
96+
non-null type but leaves `getLabelNames()` as `null` is still validated, with its labels treated as
97+
empty.
98+
8199
## Unregistering a Metric
82100

83101
There is no automatic expiry of unused metrics (yet), once a metric is registered it will remain
84102
registered forever.
85103

86-
However, you can programmatically unregistered an obsolete metric like this:
104+
However, you can programmatically unregister an obsolete metric like this:
87105

88106
```java
89107
PrometheusRegistry.defaultRegistry.unregister(eventsTotal);

docs/content/internals/model.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ All metric types implement
1919
the [Collector](/client_java/api/io/prometheus/metrics/model/registry/Collector.html) interface,
2020
i.e. they provide
2121
a [collect()](</client_java/api/io/prometheus/metrics/model/registry/Collector.html#collect()>)
22-
method to produce snapshots.
22+
method to produce snapshots. Implementers that do not provide metric type or label names (returning
23+
null from `getMetricType()` and `getLabelNames()`) are not validated at registration; they must
24+
avoid producing the same metric name and label schema as another collector, or exposition may be
25+
invalid.
2326

2427
## prometheus-metrics-model
2528

integration-tests/it-common/src/test/java/io/prometheus/client/it/common/ExporterTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import org.testcontainers.containers.GenericContainer;
2525

2626
public abstract class ExporterTest {
27-
private final GenericContainer<?> sampleAppContainer;
27+
protected final GenericContainer<?> sampleAppContainer;
2828
private final Volume sampleAppVolume;
2929
protected final String sampleApp;
3030

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>io.prometheus</groupId>
8+
<artifactId>it-exporter</artifactId>
9+
<version>1.5.0-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>it-exporter-duplicate-metrics-sample</artifactId>
13+
14+
<name>Integration Tests - Duplicate Metrics Sample</name>
15+
<description>
16+
HTTPServer Sample demonstrating duplicate metric names with different label sets
17+
</description>
18+
19+
<dependencies>
20+
<dependency>
21+
<groupId>io.prometheus</groupId>
22+
<artifactId>prometheus-metrics-exporter-httpserver</artifactId>
23+
<version>${project.version}</version>
24+
</dependency>
25+
<dependency>
26+
<groupId>io.prometheus</groupId>
27+
<artifactId>prometheus-metrics-core</artifactId>
28+
<version>${project.version}</version>
29+
</dependency>
30+
</dependencies>
31+
32+
<build>
33+
<finalName>exporter-duplicate-metrics-sample</finalName>
34+
<plugins>
35+
<plugin>
36+
<groupId>org.apache.maven.plugins</groupId>
37+
<artifactId>maven-shade-plugin</artifactId>
38+
<executions>
39+
<execution>
40+
<phase>package</phase>
41+
<goals>
42+
<goal>shade</goal>
43+
</goals>
44+
<configuration>
45+
<transformers>
46+
<transformer
47+
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
48+
<mainClass>
49+
io.prometheus.metrics.it.exporter.duplicatemetrics.DuplicateMetricsSample
50+
</mainClass>
51+
</transformer>
52+
</transformers>
53+
</configuration>
54+
</execution>
55+
</executions>
56+
</plugin>
57+
</plugins>
58+
</build>
59+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package io.prometheus.metrics.it.exporter.duplicatemetrics;
2+
3+
import io.prometheus.metrics.core.metrics.Counter;
4+
import io.prometheus.metrics.core.metrics.Gauge;
5+
import io.prometheus.metrics.exporter.httpserver.HTTPServer;
6+
import io.prometheus.metrics.model.snapshots.Unit;
7+
import java.io.IOException;
8+
9+
/** Integration test sample demonstrating metrics with duplicate names but different label sets. */
10+
public class DuplicateMetricsSample {
11+
12+
public static void main(String[] args) throws IOException, InterruptedException {
13+
if (args.length != 2) {
14+
System.err.println("Usage: java -jar duplicate-metrics-sample.jar <port> <outcome>");
15+
System.err.println("Where outcome is \"success\" or \"error\".");
16+
System.exit(1);
17+
}
18+
19+
int port = parsePortOrExit(args[0]);
20+
String outcome = args[1];
21+
run(port, outcome);
22+
}
23+
24+
private static void run(int port, String outcome) throws IOException, InterruptedException {
25+
// Register multiple counters with the same Prometheus name "http_requests_total"
26+
// but different label sets
27+
Counter requestsSuccess =
28+
Counter.builder()
29+
.name("http_requests_total")
30+
.help("Total HTTP requests by status")
31+
.labelNames("status", "method")
32+
.register();
33+
requestsSuccess.labelValues("success", "GET").inc(150);
34+
requestsSuccess.labelValues("success", "POST").inc(45);
35+
36+
Counter requestsError =
37+
Counter.builder()
38+
.name("http_requests_total")
39+
.help("Total HTTP requests by status")
40+
.labelNames("status", "endpoint")
41+
.register();
42+
requestsError.labelValues("error", "/api").inc(5);
43+
requestsError.labelValues("error", "/health").inc(2);
44+
45+
// Register multiple gauges with the same Prometheus name "active_connections"
46+
// but different label sets
47+
Gauge connectionsByRegion =
48+
Gauge.builder()
49+
.name("active_connections")
50+
.help("Active connections")
51+
.labelNames("region", "protocol")
52+
.register();
53+
connectionsByRegion.labelValues("us-east", "http").set(42);
54+
connectionsByRegion.labelValues("us-west", "http").set(38);
55+
connectionsByRegion.labelValues("eu-west", "https").set(55);
56+
57+
Gauge connectionsByPool =
58+
Gauge.builder()
59+
.name("active_connections")
60+
.help("Active connections")
61+
.labelNames("pool", "type")
62+
.register();
63+
connectionsByPool.labelValues("primary", "read").set(30);
64+
connectionsByPool.labelValues("replica", "write").set(10);
65+
66+
// Also add a regular metric without duplicates for reference
67+
Counter uniqueMetric =
68+
Counter.builder()
69+
.name("unique_metric_total")
70+
.help("A unique metric for reference")
71+
.unit(Unit.BYTES)
72+
.register();
73+
uniqueMetric.inc(1024);
74+
75+
HTTPServer server = HTTPServer.builder().port(port).buildAndStart();
76+
77+
System.out.println(
78+
"DuplicateMetricsSample listening on http://localhost:" + server.getPort() + "/metrics");
79+
Thread.currentThread().join(); // wait forever
80+
}
81+
82+
private static int parsePortOrExit(String port) {
83+
try {
84+
return Integer.parseInt(port);
85+
} catch (NumberFormatException e) {
86+
System.err.println("\"" + port + "\": Invalid port number.");
87+
System.exit(1);
88+
}
89+
return 0; // this won't happen
90+
}
91+
}

0 commit comments

Comments
 (0)