Skip to content

Commit a57de22

Browse files
committed
tests
1 parent 8dd8207 commit a57de22

File tree

6 files changed

+102
-287
lines changed

6 files changed

+102
-287
lines changed

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

Lines changed: 3 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
package io.prometheus.metrics.expositionformats;
22

3-
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeDouble;
4-
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeEscapedString;
5-
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLabels;
6-
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong;
7-
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeName;
8-
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeOpenMetricsTimestamp;
3+
import static io.prometheus.metrics.expositionformats.TextFormatUtil.*;
94
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getMetadataName;
105
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName;
116

@@ -35,7 +30,9 @@
3530
import java.io.OutputStreamWriter;
3631
import java.io.Writer;
3732
import java.nio.charset.StandardCharsets;
33+
import java.util.LinkedHashMap;
3834
import java.util.List;
35+
import java.util.Map;
3936
import javax.annotation.Nullable;
4037

4138
/**
@@ -109,91 +106,6 @@ public String getContentType() {
109106
return CONTENT_TYPE;
110107
}
111108

112-
/**
113-
* Merges snapshots with duplicate Prometheus names by combining their data points. This ensures
114-
* only one HELP/TYPE declaration per metric family.
115-
*/
116-
private MetricSnapshots mergeDuplicates(MetricSnapshots metricSnapshots) {
117-
java.util.Map<String, java.util.List<MetricSnapshot>> grouped = new java.util.LinkedHashMap<>();
118-
119-
// Group snapshots by Prometheus name
120-
for (MetricSnapshot snapshot : metricSnapshots) {
121-
String prometheusName = snapshot.getMetadata().getPrometheusName();
122-
grouped.computeIfAbsent(prometheusName, k -> new java.util.ArrayList<>()).add(snapshot);
123-
}
124-
125-
// Merge groups with multiple snapshots
126-
MetricSnapshots.Builder builder = MetricSnapshots.builder();
127-
for (java.util.List<MetricSnapshot> group : grouped.values()) {
128-
if (group.size() == 1) {
129-
builder.metricSnapshot(group.get(0));
130-
} else {
131-
// Merge multiple snapshots with same name
132-
MetricSnapshot merged = mergeSnapshots(group);
133-
builder.metricSnapshot(merged);
134-
}
135-
}
136-
137-
return builder.build();
138-
}
139-
140-
/**
141-
* Merges multiple snapshots of the same type into a single snapshot with combined data points.
142-
* Note: Duplicate label validation is performed at the registry level during scrape().
143-
*/
144-
@SuppressWarnings("unchecked")
145-
private MetricSnapshot mergeSnapshots(java.util.List<MetricSnapshot> snapshots) {
146-
if (snapshots.isEmpty()) {
147-
throw new IllegalArgumentException("Cannot merge empty list of snapshots");
148-
}
149-
150-
MetricSnapshot first = snapshots.get(0);
151-
if (snapshots.size() == 1) {
152-
return first;
153-
}
154-
155-
// Combine all data points from all snapshots
156-
java.util.List<DataPointSnapshot> allDataPoints = new java.util.ArrayList<>();
157-
for (MetricSnapshot snapshot : snapshots) {
158-
allDataPoints.addAll((java.util.Collection<DataPointSnapshot>) snapshot.getDataPoints());
159-
}
160-
161-
// Create merged snapshot based on type
162-
if (first instanceof CounterSnapshot) {
163-
return new CounterSnapshot(
164-
first.getMetadata(),
165-
(java.util.Collection<CounterSnapshot.CounterDataPointSnapshot>) (Object) allDataPoints);
166-
} else if (first instanceof GaugeSnapshot) {
167-
return new GaugeSnapshot(
168-
first.getMetadata(),
169-
(java.util.Collection<GaugeSnapshot.GaugeDataPointSnapshot>) (Object) allDataPoints);
170-
} else if (first instanceof HistogramSnapshot) {
171-
return new HistogramSnapshot(
172-
first.getMetadata(),
173-
(java.util.Collection<HistogramSnapshot.HistogramDataPointSnapshot>)
174-
(Object) allDataPoints);
175-
} else if (first instanceof SummarySnapshot) {
176-
return new SummarySnapshot(
177-
first.getMetadata(),
178-
(java.util.Collection<SummarySnapshot.SummaryDataPointSnapshot>) (Object) allDataPoints);
179-
} else if (first instanceof InfoSnapshot) {
180-
return new InfoSnapshot(
181-
first.getMetadata(),
182-
(java.util.Collection<InfoSnapshot.InfoDataPointSnapshot>) (Object) allDataPoints);
183-
} else if (first instanceof StateSetSnapshot) {
184-
return new StateSetSnapshot(
185-
first.getMetadata(),
186-
(java.util.Collection<StateSetSnapshot.StateSetDataPointSnapshot>)
187-
(Object) allDataPoints);
188-
} else if (first instanceof UnknownSnapshot) {
189-
return new UnknownSnapshot(
190-
first.getMetadata(),
191-
(java.util.Collection<UnknownSnapshot.UnknownDataPointSnapshot>) (Object) allDataPoints);
192-
} else {
193-
throw new IllegalArgumentException("Unknown snapshot type: " + first.getClass().getName());
194-
}
195-
}
196-
197109
@Override
198110
public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme scheme)
199111
throws IOException {

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

Lines changed: 1 addition & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
package io.prometheus.metrics.expositionformats;
22

3-
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeDouble;
4-
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeEscapedString;
5-
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLabels;
6-
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong;
7-
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeName;
8-
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writePrometheusTimestamp;
3+
import static io.prometheus.metrics.expositionformats.TextFormatUtil.*;
94
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.escapeMetricSnapshot;
105
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getMetadataName;
116
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName;
@@ -108,91 +103,6 @@ public String getContentType() {
108103
return CONTENT_TYPE;
109104
}
110105

111-
/**
112-
* Merges snapshots with duplicate Prometheus names by combining their data points. This ensures
113-
* only one HELP/TYPE declaration per metric family.
114-
*/
115-
private MetricSnapshots mergeDuplicates(MetricSnapshots metricSnapshots) {
116-
java.util.Map<String, java.util.List<MetricSnapshot>> grouped = new java.util.LinkedHashMap<>();
117-
118-
// Group snapshots by Prometheus name
119-
for (MetricSnapshot snapshot : metricSnapshots) {
120-
String prometheusName = snapshot.getMetadata().getPrometheusName();
121-
grouped.computeIfAbsent(prometheusName, k -> new java.util.ArrayList<>()).add(snapshot);
122-
}
123-
124-
// Merge groups with multiple snapshots
125-
MetricSnapshots.Builder builder = MetricSnapshots.builder();
126-
for (java.util.List<MetricSnapshot> group : grouped.values()) {
127-
if (group.size() == 1) {
128-
builder.metricSnapshot(group.get(0));
129-
} else {
130-
// Merge multiple snapshots with same name
131-
MetricSnapshot merged = mergeSnapshots(group);
132-
builder.metricSnapshot(merged);
133-
}
134-
}
135-
136-
return builder.build();
137-
}
138-
139-
/**
140-
* Merges multiple snapshots of the same type into a single snapshot with combined data points.
141-
* Note: Duplicate label validation is performed at the registry level during scrape().
142-
*/
143-
@SuppressWarnings("unchecked")
144-
private MetricSnapshot mergeSnapshots(java.util.List<MetricSnapshot> snapshots) {
145-
if (snapshots.isEmpty()) {
146-
throw new IllegalArgumentException("Cannot merge empty list of snapshots");
147-
}
148-
149-
MetricSnapshot first = snapshots.get(0);
150-
if (snapshots.size() == 1) {
151-
return first;
152-
}
153-
154-
// Combine all data points from all snapshots
155-
java.util.List<DataPointSnapshot> allDataPoints = new java.util.ArrayList<>();
156-
for (MetricSnapshot snapshot : snapshots) {
157-
allDataPoints.addAll((java.util.Collection<DataPointSnapshot>) snapshot.getDataPoints());
158-
}
159-
160-
// Create merged snapshot based on type
161-
if (first instanceof CounterSnapshot) {
162-
return new CounterSnapshot(
163-
first.getMetadata(),
164-
(java.util.Collection<CounterSnapshot.CounterDataPointSnapshot>) (Object) allDataPoints);
165-
} else if (first instanceof GaugeSnapshot) {
166-
return new GaugeSnapshot(
167-
first.getMetadata(),
168-
(java.util.Collection<GaugeSnapshot.GaugeDataPointSnapshot>) (Object) allDataPoints);
169-
} else if (first instanceof HistogramSnapshot) {
170-
return new HistogramSnapshot(
171-
first.getMetadata(),
172-
(java.util.Collection<HistogramSnapshot.HistogramDataPointSnapshot>)
173-
(Object) allDataPoints);
174-
} else if (first instanceof SummarySnapshot) {
175-
return new SummarySnapshot(
176-
first.getMetadata(),
177-
(java.util.Collection<SummarySnapshot.SummaryDataPointSnapshot>) (Object) allDataPoints);
178-
} else if (first instanceof InfoSnapshot) {
179-
return new InfoSnapshot(
180-
first.getMetadata(),
181-
(java.util.Collection<InfoSnapshot.InfoDataPointSnapshot>) (Object) allDataPoints);
182-
} else if (first instanceof StateSetSnapshot) {
183-
return new StateSetSnapshot(
184-
first.getMetadata(),
185-
(java.util.Collection<StateSetSnapshot.StateSetDataPointSnapshot>)
186-
(Object) allDataPoints);
187-
} else if (first instanceof UnknownSnapshot) {
188-
return new UnknownSnapshot(
189-
first.getMetadata(),
190-
(java.util.Collection<UnknownSnapshot.UnknownDataPointSnapshot>) (Object) allDataPoints);
191-
} else {
192-
throw new IllegalArgumentException("Unknown snapshot type: " + first.getClass().getName());
193-
}
194-
}
195-
196106
@Override
197107
public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme scheme)
198108
throws IOException {

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

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package io.prometheus.metrics.expositionformats;
22

33
import io.prometheus.metrics.config.EscapingScheme;
4-
import io.prometheus.metrics.model.snapshots.Labels;
5-
import io.prometheus.metrics.model.snapshots.PrometheusNaming;
6-
import io.prometheus.metrics.model.snapshots.SnapshotEscaper;
4+
import io.prometheus.metrics.model.snapshots.*;
5+
76
import java.io.IOException;
87
import java.io.Writer;
8+
import java.util.*;
99
import javax.annotation.Nullable;
1010

1111
public class TextFormatUtil {
@@ -155,4 +155,87 @@ static void writeName(Writer writer, String name, NameType nameType) throws IOEx
155155
writeEscapedString(writer, name);
156156
writer.write('"');
157157
}
158+
159+
/**
160+
* Merges snapshots with duplicate Prometheus names by combining their data points. This ensures
161+
* only one HELP/TYPE declaration per metric family.
162+
*/
163+
static MetricSnapshots mergeDuplicates(MetricSnapshots metricSnapshots) {
164+
Map<String, List<MetricSnapshot>> grouped = new LinkedHashMap<>();
165+
166+
// Group snapshots by Prometheus name
167+
for (MetricSnapshot snapshot : metricSnapshots) {
168+
String prometheusName = snapshot.getMetadata().getPrometheusName();
169+
grouped.computeIfAbsent(prometheusName, k -> new ArrayList<>()).add(snapshot);
170+
}
171+
172+
// Merge groups with multiple snapshots
173+
MetricSnapshots.Builder builder = MetricSnapshots.builder();
174+
for (List<MetricSnapshot> group : grouped.values()) {
175+
if (group.size() == 1) {
176+
builder.metricSnapshot(group.get(0));
177+
} else {
178+
// Merge multiple snapshots with same name
179+
MetricSnapshot merged = mergeSnapshots(group);
180+
builder.metricSnapshot(merged);
181+
}
182+
}
183+
184+
return builder.build();
185+
}
186+
187+
/**
188+
* Merges multiple snapshots of the same type into a single snapshot with combined data points.
189+
*/
190+
@SuppressWarnings("unchecked")
191+
private static MetricSnapshot mergeSnapshots(List<MetricSnapshot> snapshots) {
192+
if (snapshots.isEmpty()) {
193+
throw new IllegalArgumentException("Cannot merge empty list of snapshots");
194+
}
195+
196+
MetricSnapshot first = snapshots.get(0);
197+
if (snapshots.size() == 1) {
198+
return first;
199+
}
200+
201+
List<DataPointSnapshot> allDataPoints = new ArrayList<>();
202+
for (MetricSnapshot snapshot : snapshots) {
203+
allDataPoints.addAll(snapshot.getDataPoints());
204+
}
205+
206+
// Create merged snapshot based on type
207+
if (first instanceof CounterSnapshot) {
208+
return new CounterSnapshot(
209+
first.getMetadata(),
210+
(Collection<CounterSnapshot.CounterDataPointSnapshot>) (Object) allDataPoints);
211+
} else if (first instanceof GaugeSnapshot) {
212+
return new GaugeSnapshot(
213+
first.getMetadata(),
214+
(Collection<GaugeSnapshot.GaugeDataPointSnapshot>) (Object) allDataPoints);
215+
} else if (first instanceof HistogramSnapshot) {
216+
return new HistogramSnapshot(
217+
first.getMetadata(),
218+
(Collection<HistogramSnapshot.HistogramDataPointSnapshot>)
219+
(Object) allDataPoints);
220+
} else if (first instanceof SummarySnapshot) {
221+
return new SummarySnapshot(
222+
first.getMetadata(),
223+
(Collection<SummarySnapshot.SummaryDataPointSnapshot>) (Object) allDataPoints);
224+
} else if (first instanceof InfoSnapshot) {
225+
return new InfoSnapshot(
226+
first.getMetadata(),
227+
(Collection<InfoSnapshot.InfoDataPointSnapshot>) (Object) allDataPoints);
228+
} else if (first instanceof StateSetSnapshot) {
229+
return new StateSetSnapshot(
230+
first.getMetadata(),
231+
(Collection<StateSetSnapshot.StateSetDataPointSnapshot>)
232+
(Object) allDataPoints);
233+
} else if (first instanceof UnknownSnapshot) {
234+
return new UnknownSnapshot(
235+
first.getMetadata(),
236+
(Collection<UnknownSnapshot.UnknownDataPointSnapshot>) (Object) allDataPoints);
237+
} else {
238+
throw new IllegalArgumentException("Unknown snapshot type: " + first.getClass().getName());
239+
}
240+
}
158241
}

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

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static java.nio.charset.StandardCharsets.UTF_8;
44
import static org.assertj.core.api.Assertions.assertThat;
5+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
56

67
import io.prometheus.metrics.model.registry.Collector;
78
import io.prometheus.metrics.model.registry.PrometheusRegistry;
@@ -96,7 +97,7 @@ void testDuplicateNames_differentLabels_producesValidOutput() throws IOException
9697
}
9798

9899
@Test
99-
void testDuplicateNames_sameLabels_producesOutput() throws IOException {
100+
void testDuplicateNames_sameLabels_throwsException() {
100101
PrometheusRegistry registry = new PrometheusRegistry();
101102

102103
// Counter 1
@@ -121,7 +122,7 @@ public String getPrometheusName() {
121122
}
122123
});
123124

124-
// Counter 2: SAME labels, different value
125+
// Counter 2: SAME labels, different value - this should cause an exception during scrape
125126
registry.register(
126127
new Collector() {
127128
@Override
@@ -143,35 +144,11 @@ public String getPrometheusName() {
143144
}
144145
});
145146

146-
// Scrape and write to text format
147-
MetricSnapshots snapshots = registry.scrape();
148-
ByteArrayOutputStream out = new ByteArrayOutputStream();
149-
PrometheusTextFormatWriter writer = PrometheusTextFormatWriter.create();
150-
writer.write(out, snapshots);
151-
String output = out.toString(UTF_8);
152-
153-
System.out.println("=== Duplicate Names (Same Labels) Output ===");
154-
System.out.println(output);
155-
System.out.println("=== End Output ===\n");
156-
System.out.println("⚠️ WARNING: This produces duplicate time series with identical labels!");
157-
System.out.println(" Prometheus may only keep one value, which could be confusing.\n");
158-
159-
// Verify both values appear in output
160-
assertThat(output).contains("api_responses_total{");
161-
assertThat(output).contains(" 100");
162-
assertThat(output).contains(" 50");
163-
164-
// Both have identical label sets
165-
long matchingLines =
166-
output
167-
.lines()
168-
.filter(
169-
line ->
170-
line.contains("api_responses_total{")
171-
&& line.contains("outcome=\"SUCCESS\"")
172-
&& line.contains("uri=\"/hello\""))
173-
.count();
174-
assertThat(matchingLines).isEqualTo(2);
147+
// Scraping should throw exception due to duplicate time series (same name + same labels)
148+
assertThatThrownBy(registry::scrape)
149+
.isInstanceOf(IllegalArgumentException.class)
150+
.hasMessageContaining("Duplicate labels detected")
151+
.hasMessageContaining("api_responses");
175152
}
176153

177154
@Test

0 commit comments

Comments
 (0)