Skip to content

Commit f56241e

Browse files
committed
updates and cleanups
Signed-off-by: Jay DeLuca <jaydeluca4@gmail.com>
1 parent 27f6115 commit f56241e

File tree

7 files changed

+237
-133
lines changed

7 files changed

+237
-133
lines changed

integration-tests/it-exporter/it-exporter-duplicate-metrics-sample/src/main/java/io/prometheus/metrics/it/exporter/duplicatemetrics/DuplicateMetricsSample.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,12 @@
66
import io.prometheus.metrics.model.snapshots.Unit;
77
import java.io.IOException;
88

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-
*/
9+
/** Integration test sample demonstrating metrics with duplicate names but different label sets. */
1310
public class DuplicateMetricsSample {
1411

1512
public static void main(String[] args) throws IOException, InterruptedException {
1613
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).");
14+
System.err.println("Usage: java -jar duplicate-metrics-sample.jar <port>");
1915
System.exit(1);
2016
}
2117

integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/DuplicateMetricsIT.java

Lines changed: 44 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,27 @@ void testDuplicateMetricsInPrometheusTextFormat() throws IOException {
3434
assertContentType(
3535
"text/plain; version=0.0.4; charset=utf-8", response.getHeader("Content-Type"));
3636

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");
37+
String expected =
38+
"""
39+
# HELP active_connections Active connections
40+
# TYPE active_connections gauge
41+
active_connections{pool="primary",type="read"} 30.0
42+
active_connections{pool="replica",type="write"} 10.0
43+
active_connections{protocol="http",region="us-east"} 42.0
44+
active_connections{protocol="http",region="us-west"} 38.0
45+
active_connections{protocol="https",region="eu-west"} 55.0
46+
# HELP http_requests_total Total HTTP requests by status
47+
# TYPE http_requests_total counter
48+
http_requests_total{endpoint="/api",status="error"} 5.0
49+
http_requests_total{endpoint="/health",status="error"} 2.0
50+
http_requests_total{method="GET",status="success"} 150.0
51+
http_requests_total{method="POST",status="success"} 45.0
52+
# HELP unique_metric_bytes_total A unique metric for reference
53+
# TYPE unique_metric_bytes_total counter
54+
unique_metric_bytes_total 1024.0
55+
""";
56+
57+
assertThat(response.stringBody()).isEqualTo(expected);
6058
}
6159

6260
@Test
@@ -69,31 +67,30 @@ void testDuplicateMetricsInOpenMetricsTextFormat() throws IOException {
6967
"application/openmetrics-text; version=1.0.0; charset=utf-8",
7068
response.getHeader("Content-Type"));
7169

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-
9170
// 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");
71+
String expected =
72+
"""
73+
# TYPE active_connections gauge
74+
# HELP active_connections Active connections
75+
active_connections{pool="primary",type="read"} 30.0
76+
active_connections{pool="replica",type="write"} 10.0
77+
active_connections{protocol="http",region="us-east"} 42.0
78+
active_connections{protocol="http",region="us-west"} 38.0
79+
active_connections{protocol="https",region="eu-west"} 55.0
80+
# TYPE http_requests counter
81+
# HELP http_requests Total HTTP requests by status
82+
http_requests_total{endpoint="/api",status="error"} 5.0
83+
http_requests_total{endpoint="/health",status="error"} 2.0
84+
http_requests_total{method="GET",status="success"} 150.0
85+
http_requests_total{method="POST",status="success"} 45.0
86+
# TYPE unique_metric_bytes counter
87+
# UNIT unique_metric_bytes bytes
88+
# HELP unique_metric_bytes A unique metric for reference
89+
unique_metric_bytes_total 1024.0
90+
# EOF
91+
""";
92+
93+
assertThat(response.stringBody()).isEqualTo(expected);
9794
}
9895

9996
@Test
@@ -163,7 +160,6 @@ void testDuplicateMetricsInPrometheusProtobufFormat() throws IOException {
163160
assertThat(foundErrorApi).isTrue();
164161
assertThat(foundErrorHealth).isTrue();
165162

166-
// Verify unique metric
167163
Metrics.MetricFamily uniqueMetric = metrics.get(2);
168164
assertThat(uniqueMetric.getType()).isEqualTo(Metrics.MetricType.COUNTER);
169165
assertThat(uniqueMetric.getMetricList()).hasSize(1);

integration-tests/it-spring-boot-smoke-test/src/main/java/io/prometheus/metrics/it/springboot/Application.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,9 @@
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55

66
@SpringBootApplication
7-
public final class Application {
7+
public class Application {
88

9-
private Application() {
10-
// Utility class
11-
}
12-
13-
public static void main(final String[] args) {
9+
public static void main(String[] args) {
1410
SpringApplication.run(Application.class, args);
1511
}
1612
}

prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/DuplicateNamesProtobufTest.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ public MetricSnapshot collect() {
271271
.help("Active sessions gauge")
272272
.dataPoint(
273273
GaugeSnapshot.GaugeDataPointSnapshot.builder()
274-
.labels(Labels.of("method", "POST"))
274+
.labels(Labels.of("region", "us-east-1"))
275275
.value(50)
276276
.build())
277277
.build();
@@ -286,12 +286,14 @@ public String getPrometheusName() {
286286
return registry.scrape();
287287
}
288288

289-
private List<Metrics.MetricFamily> parseProtobufOutput(ByteArrayOutputStream out)
289+
private static List<Metrics.MetricFamily> parseProtobufOutput(ByteArrayOutputStream out)
290290
throws IOException {
291291
List<Metrics.MetricFamily> metricFamilies = new ArrayList<>();
292-
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
293-
while (in.available() > 0) {
294-
metricFamilies.add(Metrics.MetricFamily.parseDelimitedFrom(in));
292+
try (ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray())) {
293+
Metrics.MetricFamily family;
294+
while ((family = Metrics.MetricFamily.parseDelimitedFrom(in)) != null) {
295+
metricFamilies.add(family);
296+
}
295297
}
296298
return metricFamilies;
297299
}

prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,41 @@
2323
import java.util.Map;
2424
import javax.annotation.Nullable;
2525

26+
/**
27+
* Utility methods for writing Prometheus text exposition formats.
28+
*
29+
* <p>This class provides low-level formatting utilities used by both Prometheus text format and
30+
* OpenMetrics format writers. It handles escaping, label formatting, timestamp conversion, and
31+
* merging of duplicate metric names.
32+
*/
2633
public class TextFormatUtil {
34+
/**
35+
* Merges snapshots with duplicate Prometheus names by combining their data points. This ensures
36+
* only one HELP/TYPE declaration per metric family.
37+
*/
38+
public static MetricSnapshots mergeDuplicates(MetricSnapshots metricSnapshots) {
39+
Map<String, List<MetricSnapshot>> grouped = new LinkedHashMap<>();
40+
41+
// Group snapshots by Prometheus name
42+
for (MetricSnapshot snapshot : metricSnapshots) {
43+
String prometheusName = snapshot.getMetadata().getPrometheusName();
44+
grouped.computeIfAbsent(prometheusName, k -> new ArrayList<>()).add(snapshot);
45+
}
46+
47+
// Merge groups with multiple snapshots
48+
MetricSnapshots.Builder builder = MetricSnapshots.builder();
49+
for (List<MetricSnapshot> group : grouped.values()) {
50+
if (group.size() == 1) {
51+
builder.metricSnapshot(group.get(0));
52+
} else {
53+
// Merge multiple snapshots with same name
54+
MetricSnapshot merged = mergeSnapshots(group);
55+
builder.metricSnapshot(merged);
56+
}
57+
}
58+
59+
return builder.build();
60+
}
2761

2862
static void writeLong(Writer writer, long value) throws IOException {
2963
writer.append(Long.toString(value));
@@ -171,46 +205,22 @@ static void writeName(Writer writer, String name, NameType nameType) throws IOEx
171205
writer.write('"');
172206
}
173207

174-
/**
175-
* Merges snapshots with duplicate Prometheus names by combining their data points. This ensures
176-
* only one HELP/TYPE declaration per metric family.
177-
*/
178-
public static MetricSnapshots mergeDuplicates(MetricSnapshots metricSnapshots) {
179-
Map<String, List<MetricSnapshot>> grouped = new LinkedHashMap<>();
180-
181-
// Group snapshots by Prometheus name
182-
for (MetricSnapshot snapshot : metricSnapshots) {
183-
String prometheusName = snapshot.getMetadata().getPrometheusName();
184-
grouped.computeIfAbsent(prometheusName, k -> new ArrayList<>()).add(snapshot);
185-
}
186-
187-
// Merge groups with multiple snapshots
188-
MetricSnapshots.Builder builder = MetricSnapshots.builder();
189-
for (List<MetricSnapshot> group : grouped.values()) {
190-
if (group.size() == 1) {
191-
builder.metricSnapshot(group.get(0));
192-
} else {
193-
// Merge multiple snapshots with same name
194-
MetricSnapshot merged = mergeSnapshots(group);
195-
builder.metricSnapshot(merged);
196-
}
197-
}
198-
199-
return builder.build();
200-
}
201-
202208
/**
203209
* Merges multiple snapshots of the same type into a single snapshot with combined data points.
204210
*/
205211
@SuppressWarnings("unchecked")
206212
private static MetricSnapshot mergeSnapshots(List<MetricSnapshot> snapshots) {
207-
if (snapshots.isEmpty()) {
208-
throw new IllegalArgumentException("Cannot merge empty list of snapshots");
209-
}
210-
211213
MetricSnapshot first = snapshots.get(0);
212-
if (snapshots.size() == 1) {
213-
return first;
214+
215+
// Validate all snapshots are the same type
216+
for (MetricSnapshot snapshot : snapshots) {
217+
if (snapshot.getClass() != first.getClass()) {
218+
throw new IllegalArgumentException(
219+
"Cannot merge snapshots of different types: "
220+
+ first.getClass().getName()
221+
+ " and "
222+
+ snapshot.getClass().getName());
223+
}
214224
}
215225

216226
List<DataPointSnapshot> allDataPoints = new ArrayList<>();

prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/TextFormatUtilTest.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.junit.jupiter.api.Assertions.assertEquals;
55

6+
import io.prometheus.metrics.model.snapshots.CounterSnapshot;
7+
import io.prometheus.metrics.model.snapshots.Labels;
8+
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
69
import java.io.IOException;
710
import java.io.StringWriter;
811
import org.junit.jupiter.api.Test;
@@ -34,4 +37,93 @@ private static String writePrometheusTimestamp(boolean timestampsInMs) throws IO
3437
TextFormatUtil.writePrometheusTimestamp(writer, 1000, timestampsInMs);
3538
return writer.toString();
3639
}
40+
41+
@Test
42+
public void testMergeDuplicates_sameName_mergesDataPoints() {
43+
CounterSnapshot counter1 =
44+
CounterSnapshot.builder()
45+
.name("api_responses")
46+
.dataPoint(
47+
CounterSnapshot.CounterDataPointSnapshot.builder()
48+
.labels(Labels.of("uri", "/hello", "outcome", "SUCCESS"))
49+
.value(100)
50+
.build())
51+
.build();
52+
53+
CounterSnapshot counter2 =
54+
CounterSnapshot.builder()
55+
.name("api_responses")
56+
.dataPoint(
57+
CounterSnapshot.CounterDataPointSnapshot.builder()
58+
.labels(Labels.of("uri", "/hello", "outcome", "FAILURE"))
59+
.value(10)
60+
.build())
61+
.build();
62+
63+
MetricSnapshots snapshots = new MetricSnapshots(counter1, counter2);
64+
MetricSnapshots result = TextFormatUtil.mergeDuplicates(snapshots);
65+
66+
assertThat(result).hasSize(1);
67+
assertThat(result.get(0).getMetadata().getName()).isEqualTo("api_responses");
68+
assertThat(result.get(0).getDataPoints()).hasSize(2);
69+
70+
CounterSnapshot merged = (CounterSnapshot) result.get(0);
71+
assertThat(merged.getDataPoints())
72+
.anyMatch(
73+
dp ->
74+
dp.getLabels().equals(Labels.of("uri", "/hello", "outcome", "SUCCESS"))
75+
&& dp.getValue() == 100);
76+
assertThat(merged.getDataPoints())
77+
.anyMatch(
78+
dp ->
79+
dp.getLabels().equals(Labels.of("uri", "/hello", "outcome", "FAILURE"))
80+
&& dp.getValue() == 10);
81+
}
82+
83+
@Test
84+
public void testMergeDuplicates_multipleDataPoints_allMerged() {
85+
CounterSnapshot counter1 =
86+
CounterSnapshot.builder()
87+
.name("api_responses")
88+
.dataPoint(
89+
CounterSnapshot.CounterDataPointSnapshot.builder()
90+
.labels(Labels.of("uri", "/hello", "outcome", "SUCCESS"))
91+
.value(100)
92+
.build())
93+
.dataPoint(
94+
CounterSnapshot.CounterDataPointSnapshot.builder()
95+
.labels(Labels.of("uri", "/world", "outcome", "SUCCESS"))
96+
.value(200)
97+
.build())
98+
.build();
99+
100+
CounterSnapshot counter2 =
101+
CounterSnapshot.builder()
102+
.name("api_responses")
103+
.dataPoint(
104+
CounterSnapshot.CounterDataPointSnapshot.builder()
105+
.labels(Labels.of("uri", "/hello", "outcome", "FAILURE"))
106+
.value(10)
107+
.build())
108+
.dataPoint(
109+
CounterSnapshot.CounterDataPointSnapshot.builder()
110+
.labels(Labels.of("uri", "/world", "outcome", "FAILURE"))
111+
.value(5)
112+
.build())
113+
.build();
114+
115+
MetricSnapshots snapshots = new MetricSnapshots(counter1, counter2);
116+
MetricSnapshots result = TextFormatUtil.mergeDuplicates(snapshots);
117+
118+
assertThat(result).hasSize(1);
119+
assertThat(result.get(0).getDataPoints()).hasSize(4);
120+
}
121+
122+
@Test
123+
public void testMergeDuplicates_emptySnapshots_returnsEmpty() {
124+
MetricSnapshots snapshots = MetricSnapshots.builder().build();
125+
MetricSnapshots result = TextFormatUtil.mergeDuplicates(snapshots);
126+
127+
assertThat(result).isEmpty();
128+
}
37129
}

0 commit comments

Comments
 (0)