Skip to content

Commit 3080bb9

Browse files
committed
fix formatting
Signed-off-by: Jay DeLuca <[email protected]>
1 parent 947e9c3 commit 3080bb9

File tree

23 files changed

+2065
-72
lines changed

23 files changed

+2065
-72
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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>io.prometheus.metrics.it.exporter.duplicatemetrics.DuplicateMetricsSample</mainClass>
49+
</transformer>
50+
</transformers>
51+
</configuration>
52+
</execution>
53+
</executions>
54+
</plugin>
55+
</plugins>
56+
</build>
57+
</project>
58+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
/**
10+
* Integration test sample demonstrating metrics with duplicate names but different label sets. This
11+
* validates that the duplicate metrics feature works end-to-end.
12+
*/
13+
public class DuplicateMetricsSample {
14+
15+
public static void main(String[] args) throws IOException, InterruptedException {
16+
if (args.length < 1 || args.length > 2) {
17+
System.err.println("Usage: java -jar duplicate-metrics-sample.jar <port> [mode]");
18+
System.err.println("Where mode is optional (ignored for this sample).");
19+
System.exit(1);
20+
}
21+
22+
int port = parsePortOrExit(args[0]);
23+
run(port);
24+
}
25+
26+
private static void run(int port) throws IOException, InterruptedException {
27+
// Register multiple counters with the same Prometheus name "http_requests_total"
28+
// but different label sets
29+
Counter requestsSuccess =
30+
Counter.builder()
31+
.name("http_requests_total")
32+
.help("Total HTTP requests by status")
33+
.labelNames("status", "method")
34+
.register();
35+
requestsSuccess.labelValues("success", "GET").inc(150);
36+
requestsSuccess.labelValues("success", "POST").inc(45);
37+
38+
Counter requestsError =
39+
Counter.builder()
40+
.name("http_requests_total")
41+
.help("Total HTTP requests by status")
42+
.labelNames("status", "endpoint")
43+
.register();
44+
requestsError.labelValues("error", "/api").inc(5);
45+
requestsError.labelValues("error", "/health").inc(2);
46+
47+
// Register multiple gauges with the same Prometheus name "active_connections"
48+
// but different label sets
49+
Gauge connectionsByRegion =
50+
Gauge.builder()
51+
.name("active_connections")
52+
.help("Active connections")
53+
.labelNames("region", "protocol")
54+
.register();
55+
connectionsByRegion.labelValues("us-east", "http").set(42);
56+
connectionsByRegion.labelValues("us-west", "http").set(38);
57+
connectionsByRegion.labelValues("eu-west", "https").set(55);
58+
59+
Gauge connectionsByPool =
60+
Gauge.builder()
61+
.name("active_connections")
62+
.help("Active connections")
63+
.labelNames("pool", "type")
64+
.register();
65+
connectionsByPool.labelValues("primary", "read").set(30);
66+
connectionsByPool.labelValues("replica", "write").set(10);
67+
68+
// Also add a regular metric without duplicates for reference
69+
Counter uniqueMetric =
70+
Counter.builder()
71+
.name("unique_metric_total")
72+
.help("A unique metric for reference")
73+
.unit(Unit.BYTES)
74+
.register();
75+
uniqueMetric.inc(1024);
76+
77+
HTTPServer server = HTTPServer.builder().port(port).buildAndStart();
78+
79+
System.out.println(
80+
"DuplicateMetricsSample listening on http://localhost:" + server.getPort() + "/metrics");
81+
Thread.currentThread().join(); // wait forever
82+
}
83+
84+
private static int parsePortOrExit(String port) {
85+
try {
86+
return Integer.parseInt(port);
87+
} catch (NumberFormatException e) {
88+
System.err.println("\"" + port + "\": Invalid port number.");
89+
System.exit(1);
90+
}
91+
return 0; // this won't happen
92+
}
93+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package io.prometheus.metrics.it.exporter.test;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import io.prometheus.client.it.common.ExporterTest;
6+
import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_33_2.Metrics;
7+
import java.io.IOException;
8+
import java.net.URISyntaxException;
9+
import java.util.List;
10+
import org.junit.jupiter.api.Test;
11+
12+
/**
13+
* Integration test for duplicate metric names with different label sets.
14+
*
15+
* <p>This test validates that:
16+
*
17+
* <ul>
18+
* <li>Multiple metrics with the same Prometheus name but different labels can be registered
19+
* <li>All exposition formats (text, OpenMetrics, protobuf) correctly merge and expose them
20+
* <li>The merged output is valid and scrapeable by Prometheus
21+
* </ul>
22+
*/
23+
class DuplicateMetricsIT extends ExporterTest {
24+
25+
public DuplicateMetricsIT() throws IOException, URISyntaxException {
26+
super("exporter-duplicate-metrics-sample");
27+
}
28+
29+
@Test
30+
void testDuplicateMetricsInPrometheusTextFormat() throws IOException {
31+
start();
32+
Response response = scrape("GET", "");
33+
assertThat(response.status).isEqualTo(200);
34+
assertContentType(
35+
"text/plain; version=0.0.4; charset=utf-8", response.getHeader("Content-Type"));
36+
37+
String body = response.stringBody();
38+
39+
assertThat(body).contains("# TYPE http_requests_total counter");
40+
assertThat(body).contains("# HELP http_requests_total Total HTTP requests by status");
41+
42+
// Verify all data points from both collectors are present
43+
assertThat(body)
44+
.contains("http_requests_total{method=\"GET\",status=\"success\"} 150.0")
45+
.contains("http_requests_total{method=\"POST\",status=\"success\"} 45.0")
46+
.contains("http_requests_total{endpoint=\"/api\",status=\"error\"} 5.0")
47+
.contains("http_requests_total{endpoint=\"/health\",status=\"error\"} 2.0");
48+
49+
assertThat(body).contains("# TYPE active_connections gauge");
50+
assertThat(body).contains("# HELP active_connections Active connections");
51+
52+
assertThat(body)
53+
.contains("active_connections{protocol=\"http\",region=\"us-east\"} 42.0")
54+
.contains("active_connections{protocol=\"http\",region=\"us-west\"} 38.0")
55+
.contains("active_connections{protocol=\"https\",region=\"eu-west\"} 55.0")
56+
.contains("active_connections{pool=\"primary\",type=\"read\"} 30.0")
57+
.contains("active_connections{pool=\"replica\",type=\"write\"} 10.0");
58+
59+
assertThat(body).contains("unique_metric_bytes_total 1024.0");
60+
}
61+
62+
@Test
63+
void testDuplicateMetricsInOpenMetricsTextFormat() throws IOException {
64+
start();
65+
Response response =
66+
scrape("GET", "", "Accept", "application/openmetrics-text; version=1.0.0; charset=utf-8");
67+
assertThat(response.status).isEqualTo(200);
68+
assertContentType(
69+
"application/openmetrics-text; version=1.0.0; charset=utf-8",
70+
response.getHeader("Content-Type"));
71+
72+
String body = response.stringBody();
73+
74+
// Verify http_requests_total is properly merged
75+
assertThat(body).contains("# TYPE http_requests counter");
76+
assertThat(body)
77+
.contains("http_requests_total{method=\"GET\",status=\"success\"} 150.0")
78+
.contains("http_requests_total{method=\"POST\",status=\"success\"} 45.0")
79+
.contains("http_requests_total{endpoint=\"/api\",status=\"error\"} 5.0")
80+
.contains("http_requests_total{endpoint=\"/health\",status=\"error\"} 2.0");
81+
82+
// Verify active_connections is properly merged
83+
assertThat(body).contains("# TYPE active_connections gauge");
84+
assertThat(body)
85+
.contains("active_connections{protocol=\"http\",region=\"us-east\"} 42.0")
86+
.contains("active_connections{protocol=\"http\",region=\"us-west\"} 38.0")
87+
.contains("active_connections{protocol=\"https\",region=\"eu-west\"} 55.0")
88+
.contains("active_connections{pool=\"primary\",type=\"read\"} 30.0")
89+
.contains("active_connections{pool=\"replica\",type=\"write\"} 10.0");
90+
91+
// OpenMetrics format should have UNIT for unique_metric_bytes (base name without _total)
92+
assertThat(body)
93+
.contains("unique_metric_bytes_total 1024.0")
94+
.contains("# UNIT unique_metric_bytes bytes");
95+
96+
assertThat(body).endsWith("# EOF\n");
97+
}
98+
99+
@Test
100+
void testDuplicateMetricsInPrometheusProtobufFormat() throws IOException {
101+
start();
102+
Response response =
103+
scrape(
104+
"GET",
105+
"",
106+
"Accept",
107+
"application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily;"
108+
+ " encoding=delimited");
109+
assertThat(response.status).isEqualTo(200);
110+
assertContentType(
111+
"application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily;"
112+
+ " encoding=delimited",
113+
response.getHeader("Content-Type"));
114+
115+
List<Metrics.MetricFamily> metrics = response.protoBody();
116+
117+
// Should have exactly 3 metric families (active_connections, http_requests_total,
118+
// unique_metric_bytes_total)
119+
assertThat(metrics).hasSize(3);
120+
121+
// Metrics are sorted by name
122+
assertThat(metrics.get(0).getName()).isEqualTo("active_connections");
123+
assertThat(metrics.get(1).getName()).isEqualTo("http_requests_total");
124+
assertThat(metrics.get(2).getName()).isEqualTo("unique_metric_bytes_total");
125+
126+
// Verify active_connections has all 5 data points merged
127+
Metrics.MetricFamily activeConnections = metrics.get(0);
128+
assertThat(activeConnections.getType()).isEqualTo(Metrics.MetricType.GAUGE);
129+
assertThat(activeConnections.getHelp()).isEqualTo("Active connections");
130+
assertThat(activeConnections.getMetricList()).hasSize(5);
131+
132+
// Verify http_requests_total has all 4 data points merged
133+
Metrics.MetricFamily httpRequests = metrics.get(1);
134+
assertThat(httpRequests.getType()).isEqualTo(Metrics.MetricType.COUNTER);
135+
assertThat(httpRequests.getHelp()).isEqualTo("Total HTTP requests by status");
136+
assertThat(httpRequests.getMetricList()).hasSize(4);
137+
138+
// Verify each data point has the expected labels
139+
boolean foundSuccessGet = false;
140+
boolean foundSuccessPost = false;
141+
boolean foundErrorApi = false;
142+
boolean foundErrorHealth = false;
143+
144+
for (Metrics.Metric metric : httpRequests.getMetricList()) {
145+
List<Metrics.LabelPair> labels = metric.getLabelList();
146+
if (hasLabel(labels, "status", "success") && hasLabel(labels, "method", "GET")) {
147+
assertThat(metric.getCounter().getValue()).isEqualTo(150.0);
148+
foundSuccessGet = true;
149+
} else if (hasLabel(labels, "status", "success") && hasLabel(labels, "method", "POST")) {
150+
assertThat(metric.getCounter().getValue()).isEqualTo(45.0);
151+
foundSuccessPost = true;
152+
} else if (hasLabel(labels, "status", "error") && hasLabel(labels, "endpoint", "/api")) {
153+
assertThat(metric.getCounter().getValue()).isEqualTo(5.0);
154+
foundErrorApi = true;
155+
} else if (hasLabel(labels, "status", "error") && hasLabel(labels, "endpoint", "/health")) {
156+
assertThat(metric.getCounter().getValue()).isEqualTo(2.0);
157+
foundErrorHealth = true;
158+
}
159+
}
160+
161+
assertThat(foundSuccessGet).isTrue();
162+
assertThat(foundSuccessPost).isTrue();
163+
assertThat(foundErrorApi).isTrue();
164+
assertThat(foundErrorHealth).isTrue();
165+
166+
// Verify unique metric
167+
Metrics.MetricFamily uniqueMetric = metrics.get(2);
168+
assertThat(uniqueMetric.getType()).isEqualTo(Metrics.MetricType.COUNTER);
169+
assertThat(uniqueMetric.getMetricList()).hasSize(1);
170+
assertThat(uniqueMetric.getMetric(0).getCounter().getValue()).isEqualTo(1024.0);
171+
}
172+
173+
@Test
174+
void testDuplicateMetricsWithNameFilter() throws IOException {
175+
start();
176+
// Only scrape http_requests_total
177+
Response response = scrape("GET", nameParam());
178+
assertThat(response.status).isEqualTo(200);
179+
180+
String body = response.stringBody();
181+
182+
assertThat(body)
183+
.contains("http_requests_total{method=\"GET\",status=\"success\"} 150.0")
184+
.contains("http_requests_total{endpoint=\"/api\",status=\"error\"} 5.0");
185+
186+
// Should NOT contain active_connections or unique_metric_total
187+
assertThat(body).doesNotContain("active_connections").doesNotContain("unique_metric_total");
188+
}
189+
190+
private boolean hasLabel(List<Metrics.LabelPair> labels, String name, String value) {
191+
return labels.stream()
192+
.anyMatch(label -> label.getName().equals(name) && label.getValue().equals(value));
193+
}
194+
195+
private String nameParam() {
196+
return "name[]=" + "http_requests_total";
197+
}
198+
}

integration-tests/it-exporter/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<module>it-exporter-servlet-tomcat-sample</module>
2222
<module>it-exporter-servlet-jetty-sample</module>
2323
<module>it-exporter-httpserver-sample</module>
24+
<module>it-exporter-duplicate-metrics-sample</module>
2425
<module>it-exporter-no-protobuf</module>
2526
<module>it-exporter-test</module>
2627
<module>it-no-protobuf-test</module>

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.prometheus.metrics.core.datapoints.CounterDataPoint;
66
import io.prometheus.metrics.core.exemplars.ExemplarSampler;
77
import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfig;
8+
import io.prometheus.metrics.model.registry.MetricType;
89
import io.prometheus.metrics.model.snapshots.CounterSnapshot;
910
import io.prometheus.metrics.model.snapshots.Exemplar;
1011
import io.prometheus.metrics.model.snapshots.Labels;
@@ -92,6 +93,11 @@ protected CounterSnapshot collect(List<Labels> labels, List<DataPoint> metricDat
9293
return new CounterSnapshot(getMetadata(), data);
9394
}
9495

96+
@Override
97+
public MetricType getMetricType() {
98+
return MetricType.COUNTER;
99+
}
100+
95101
@Override
96102
protected DataPoint newDataPoint() {
97103
if (exemplarSamplerConfig != null) {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.prometheus.metrics.core.datapoints.GaugeDataPoint;
66
import io.prometheus.metrics.core.exemplars.ExemplarSampler;
77
import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfig;
8+
import io.prometheus.metrics.model.registry.MetricType;
89
import io.prometheus.metrics.model.snapshots.Exemplar;
910
import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
1011
import io.prometheus.metrics.model.snapshots.Labels;
@@ -94,6 +95,11 @@ protected GaugeSnapshot collect(List<Labels> labels, List<DataPoint> metricData)
9495
return new GaugeSnapshot(getMetadata(), dataPointSnapshots);
9596
}
9697

98+
@Override
99+
public MetricType getMetricType() {
100+
return MetricType.GAUGE;
101+
}
102+
97103
@Override
98104
protected DataPoint newDataPoint() {
99105
if (exemplarSamplerConfig != null) {

0 commit comments

Comments
 (0)