Skip to content

Commit 1022aa4

Browse files
committed
handle other exposition formats
Signed-off-by: Jay DeLuca <jaydeluca4@gmail.com>
1 parent d342784 commit 1022aa4

File tree

8 files changed

+367
-24
lines changed

8 files changed

+367
-24
lines changed

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@
1010
public class DuplicateMetricsSample {
1111

1212
public static void main(String[] args) throws IOException, InterruptedException {
13-
if (args.length != 1) {
14-
System.err.println("Usage: java -jar duplicate-metrics-sample.jar <port>");
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\".");
1516
System.exit(1);
1617
}
1718

1819
int port = parsePortOrExit(args[0]);
19-
run(port);
20+
String outcome = args[1];
21+
run(port, outcome);
2022
}
2123

22-
private static void run(int port) throws IOException, InterruptedException {
24+
private static void run(int port, String outcome) throws IOException, InterruptedException {
2325
// Register multiple counters with the same Prometheus name "http_requests_total"
2426
// but different label sets
2527
Counter requestsSuccess =

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ public DuplicateMetricsIT() throws IOException, URISyntaxException {
2626
super("exporter-duplicate-metrics-sample");
2727
}
2828

29-
@Override
30-
protected void start(String outcome) {
31-
sampleAppContainer.withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400").start();
32-
}
33-
3429
@Test
3530
void testDuplicateMetricsInPrometheusTextFormat() throws IOException {
3631
start();

prometheus-metrics-exposition-formats/generate-protobuf.sh

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@ set -euo pipefail
66
# I could not figure out how to use a protoc Maven plugin to use the shaded module,
77
# so I ran this command to generate the sources manually.
88

9+
# Use gsed and ggrep on macOS (requires: brew install gnu-sed grep)
10+
if [[ "$OSTYPE" == "darwin"* ]] && command -v gsed >/dev/null 2>&1; then
11+
SED=gsed
12+
else
13+
SED=sed
14+
fi
15+
16+
if [[ "$OSTYPE" == "darwin"* ]] && command -v ggrep >/dev/null 2>&1; then
17+
GREP=ggrep
18+
else
19+
GREP=grep
20+
fi
21+
22+
# Use mise-provided protoc if available
23+
if command -v mise >/dev/null 2>&1; then
24+
PROTOC="mise exec -- protoc"
25+
else
26+
PROTOC=protoc
27+
fi
28+
929
TARGET_DIR=$1
1030
PROTO_DIR=src/main/protobuf
1131
PROTOBUF_VERSION_STRING=$2
@@ -18,22 +38,22 @@ mkdir -p "$TARGET_DIR"
1838
rm -rf $PROTO_DIR || true
1939
mkdir -p $PROTO_DIR
2040

21-
OLD_PACKAGE=$(sed -nE 's/import (io.prometheus.metrics.expositionformats.generated.*).Metrics;/\1/p' src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java)
41+
OLD_PACKAGE=$($SED -nE 's/import (io.prometheus.metrics.expositionformats.generated.*).Metrics;/\1/p' src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java)
2242
PACKAGE="io.prometheus.metrics.expositionformats.generated.com_google_protobuf_${PROTOBUF_VERSION_STRING}"
2343

2444
if [[ $OLD_PACKAGE != "$PACKAGE" ]]; then
2545
echo "Replacing package $OLD_PACKAGE with $PACKAGE in all java files"
26-
find .. -type f -name "*.java" -exec sed -i "s/$OLD_PACKAGE/$PACKAGE/g" {} +
46+
find .. -type f -name "*.java" -exec $SED -i "s/$OLD_PACKAGE/$PACKAGE/g" {} +
2747
fi
2848

2949
curl -sL https://raw.githubusercontent.com/prometheus/client_model/master/io/prometheus/client/metrics.proto -o $PROTO_DIR/metrics.proto
3050

31-
sed -i "s/java_package = \"io.prometheus.client\"/java_package = \"$PACKAGE\"/" $PROTO_DIR/metrics.proto
32-
protoc --java_out "$TARGET_DIR" $PROTO_DIR/metrics.proto
33-
sed -i '1 i\//CHECKSTYLE:OFF: checkstyle' "$(find src/main/generated/io -type f)"
34-
sed -i -e $'$a\\\n//CHECKSTYLE:ON: checkstyle' "$(find src/main/generated/io -type f)"
51+
$SED -i "s/java_package = \"io.prometheus.client\"/java_package = \"$PACKAGE\"/" $PROTO_DIR/metrics.proto
52+
$PROTOC --java_out "$TARGET_DIR" $PROTO_DIR/metrics.proto
53+
$SED -i '1 i\//CHECKSTYLE:OFF: checkstyle' "$(find src/main/generated/io -type f)"
54+
$SED -i -e $'$a\\\n//CHECKSTYLE:ON: checkstyle' "$(find src/main/generated/io -type f)"
3555

36-
GENERATED_WITH=$(grep -oP '\/\/ Protobuf Java Version: \K.*' "$TARGET_DIR/${PACKAGE//\.//}"/Metrics.java)
56+
GENERATED_WITH=$($GREP -oP '\/\/ Protobuf Java Version: \K.*' "$TARGET_DIR/${PACKAGE//\.//}"/Metrics.java)
3757

3858
function help() {
3959
echo "Please use https://mise.jdx.dev/ - this will use the version specified in mise.toml"

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ public String getContentType() {
113113
public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme scheme)
114114
throws IOException {
115115
Writer writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
116-
for (MetricSnapshot s : metricSnapshots) {
116+
MetricSnapshots merged = TextFormatUtil.mergeDuplicates(metricSnapshots);
117+
for (MetricSnapshot s : merged) {
117118
MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, scheme);
118119
if (!snapshot.getDataPoints().isEmpty()) {
119120
if (snapshot instanceof CounterSnapshot) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingSch
115115
// "unknown", "gauge", "counter", "stateset", "info", "histogram", "gaugehistogram", and
116116
// "summary".
117117
Writer writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
118-
for (MetricSnapshot s : metricSnapshots) {
118+
MetricSnapshots merged = TextFormatUtil.mergeDuplicates(metricSnapshots);
119+
for (MetricSnapshot s : merged) {
119120
MetricSnapshot snapshot = escapeMetricSnapshot(s, scheme);
120121
if (!snapshot.getDataPoints().isEmpty()) {
121122
if (snapshot instanceof CounterSnapshot) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package io.prometheus.metrics.expositionformats;
2+
3+
import static java.nio.charset.StandardCharsets.UTF_8;
4+
import static org.assertj.core.api.Assertions.assertThat;
5+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
6+
7+
import io.prometheus.metrics.model.registry.Collector;
8+
import io.prometheus.metrics.model.registry.PrometheusRegistry;
9+
import io.prometheus.metrics.model.snapshots.CounterSnapshot;
10+
import io.prometheus.metrics.model.snapshots.Labels;
11+
import io.prometheus.metrics.model.snapshots.MetricSnapshot;
12+
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
13+
import java.io.ByteArrayOutputStream;
14+
import java.io.IOException;
15+
import org.junit.jupiter.api.Test;
16+
17+
class DuplicateNamesExpositionTest {
18+
19+
private static PrometheusRegistry getPrometheusRegistry() {
20+
PrometheusRegistry registry = new PrometheusRegistry();
21+
22+
registry.register(
23+
new Collector() {
24+
@Override
25+
public MetricSnapshot collect() {
26+
return CounterSnapshot.builder()
27+
.name("api_responses")
28+
.help("API responses")
29+
.dataPoint(
30+
CounterSnapshot.CounterDataPointSnapshot.builder()
31+
.labels(Labels.of("uri", "/hello", "outcome", "SUCCESS"))
32+
.value(100)
33+
.build())
34+
.build();
35+
}
36+
37+
@Override
38+
public String getPrometheusName() {
39+
return "api_responses_total";
40+
}
41+
});
42+
43+
registry.register(
44+
new Collector() {
45+
@Override
46+
public MetricSnapshot collect() {
47+
return CounterSnapshot.builder()
48+
.name("api_responses")
49+
.help("API responses")
50+
.dataPoint(
51+
CounterSnapshot.CounterDataPointSnapshot.builder()
52+
.labels(
53+
Labels.of("uri", "/hello", "outcome", "FAILURE", "error", "TIMEOUT"))
54+
.value(10)
55+
.build())
56+
.build();
57+
}
58+
59+
@Override
60+
public String getPrometheusName() {
61+
return "api_responses_total";
62+
}
63+
});
64+
return registry;
65+
}
66+
67+
@Test
68+
void testDuplicateNames_differentLabels_producesValidOutput() throws IOException {
69+
PrometheusRegistry registry = getPrometheusRegistry();
70+
71+
MetricSnapshots snapshots = registry.scrape();
72+
ByteArrayOutputStream out = new ByteArrayOutputStream();
73+
PrometheusTextFormatWriter writer = PrometheusTextFormatWriter.create();
74+
writer.write(out, snapshots);
75+
String output = out.toString(UTF_8);
76+
77+
String expected =
78+
"""
79+
# HELP api_responses_total API responses
80+
# TYPE api_responses_total counter
81+
api_responses_total{error="TIMEOUT",outcome="FAILURE",uri="/hello"} 10.0
82+
api_responses_total{outcome="SUCCESS",uri="/hello"} 100.0
83+
""";
84+
85+
assertThat(output).isEqualTo(expected);
86+
}
87+
88+
@Test
89+
void testDuplicateNames_multipleDataPoints_producesValidOutput() throws IOException {
90+
PrometheusRegistry registry = new PrometheusRegistry();
91+
92+
registry.register(
93+
new Collector() {
94+
@Override
95+
public MetricSnapshot collect() {
96+
return CounterSnapshot.builder()
97+
.name("api_responses")
98+
.help("API responses")
99+
.dataPoint(
100+
CounterSnapshot.CounterDataPointSnapshot.builder()
101+
.labels(Labels.of("uri", "/hello", "outcome", "SUCCESS"))
102+
.value(100)
103+
.build())
104+
.dataPoint(
105+
CounterSnapshot.CounterDataPointSnapshot.builder()
106+
.labels(Labels.of("uri", "/world", "outcome", "SUCCESS"))
107+
.value(200)
108+
.build())
109+
.build();
110+
}
111+
112+
@Override
113+
public String getPrometheusName() {
114+
return "api_responses_total";
115+
}
116+
});
117+
118+
registry.register(
119+
new Collector() {
120+
@Override
121+
public MetricSnapshot collect() {
122+
return CounterSnapshot.builder()
123+
.name("api_responses")
124+
.help("API responses")
125+
.dataPoint(
126+
CounterSnapshot.CounterDataPointSnapshot.builder()
127+
.labels(
128+
Labels.of("uri", "/hello", "outcome", "FAILURE", "error", "TIMEOUT"))
129+
.value(10)
130+
.build())
131+
.dataPoint(
132+
CounterSnapshot.CounterDataPointSnapshot.builder()
133+
.labels(
134+
Labels.of("uri", "/world", "outcome", "FAILURE", "error", "NOT_FOUND"))
135+
.value(5)
136+
.build())
137+
.build();
138+
}
139+
140+
@Override
141+
public String getPrometheusName() {
142+
return "api_responses_total";
143+
}
144+
});
145+
146+
MetricSnapshots snapshots = registry.scrape();
147+
ByteArrayOutputStream out = new ByteArrayOutputStream();
148+
PrometheusTextFormatWriter writer = PrometheusTextFormatWriter.create();
149+
writer.write(out, snapshots);
150+
String output = out.toString(UTF_8);
151+
152+
String expected =
153+
"""
154+
# HELP api_responses_total API responses
155+
# TYPE api_responses_total counter
156+
api_responses_total{error="NOT_FOUND",outcome="FAILURE",uri="/world"} 5.0
157+
api_responses_total{error="TIMEOUT",outcome="FAILURE",uri="/hello"} 10.0
158+
api_responses_total{outcome="SUCCESS",uri="/hello"} 100.0
159+
api_responses_total{outcome="SUCCESS",uri="/world"} 200.0
160+
""";
161+
assertThat(output).isEqualTo(expected);
162+
}
163+
164+
@Test
165+
void testOpenMetricsFormat_withDuplicateNames() throws IOException {
166+
PrometheusRegistry registry = getPrometheusRegistry();
167+
168+
MetricSnapshots snapshots = registry.scrape();
169+
ByteArrayOutputStream out = new ByteArrayOutputStream();
170+
OpenMetricsTextFormatWriter writer = new OpenMetricsTextFormatWriter(false, false);
171+
writer.write(out, snapshots);
172+
String output = out.toString(UTF_8);
173+
174+
String expected =
175+
"""
176+
# TYPE api_responses counter
177+
# HELP api_responses API responses
178+
api_responses_total{error="TIMEOUT",outcome="FAILURE",uri="/hello"} 10.0
179+
api_responses_total{outcome="SUCCESS",uri="/hello"} 100.0
180+
# EOF
181+
""";
182+
assertThat(output).isEqualTo(expected);
183+
}
184+
185+
@Test
186+
void testDuplicateNames_sameLabels_throwsException() {
187+
PrometheusRegistry registry = new PrometheusRegistry();
188+
189+
registry.register(
190+
new Collector() {
191+
@Override
192+
public MetricSnapshot collect() {
193+
return CounterSnapshot.builder()
194+
.name("api_responses")
195+
.help("API responses")
196+
.dataPoint(
197+
CounterSnapshot.CounterDataPointSnapshot.builder()
198+
.labels(Labels.of("uri", "/hello", "outcome", "SUCCESS"))
199+
.value(100)
200+
.build())
201+
.build();
202+
}
203+
204+
@Override
205+
public String getPrometheusName() {
206+
return "api_responses_total";
207+
}
208+
});
209+
210+
registry.register(
211+
new Collector() {
212+
@Override
213+
public MetricSnapshot collect() {
214+
return CounterSnapshot.builder()
215+
.name("api_responses")
216+
.help("API responses")
217+
.dataPoint(
218+
CounterSnapshot.CounterDataPointSnapshot.builder()
219+
.labels(Labels.of("uri", "/hello", "outcome", "SUCCESS"))
220+
.value(50)
221+
.build())
222+
.build();
223+
}
224+
225+
@Override
226+
public String getPrometheusName() {
227+
return "api_responses_total";
228+
}
229+
});
230+
231+
// Scrape should throw exception due to duplicate time series (same name + same labels)
232+
assertThatThrownBy(registry::scrape)
233+
.isInstanceOf(IllegalStateException.class)
234+
.hasMessageContaining("duplicate metric name with identical label schema [uri, outcome]")
235+
.hasMessageContaining("api_responses");
236+
}
237+
}

0 commit comments

Comments
 (0)