Skip to content

Commit 25129e5

Browse files
authored
PrometheusHttpServer drops metrics with same name and different type (#5078)
* PrometheusHttpServer drops metrics with same name and different type * Switch to concurrent hash set * Animal sniffer * Fix typo * PR feedback
1 parent cc40dbd commit 25129e5

File tree

5 files changed

+272
-126
lines changed

5 files changed

+272
-126
lines changed

exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServer.java

+22-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
package io.opentelemetry.exporter.prometheus;
1212

13+
import static java.util.stream.Collectors.joining;
14+
1315
import com.sun.net.httpserver.HttpExchange;
1416
import com.sun.net.httpserver.HttpHandler;
1517
import com.sun.net.httpserver.HttpServer;
@@ -34,11 +36,14 @@
3436
import java.util.HashSet;
3537
import java.util.List;
3638
import java.util.Set;
39+
import java.util.concurrent.ConcurrentHashMap;
3740
import java.util.concurrent.ExecutorService;
3841
import java.util.concurrent.Executors;
3942
import java.util.concurrent.TimeUnit;
4043
import java.util.function.Predicate;
4144
import java.util.function.Supplier;
45+
import java.util.logging.Level;
46+
import java.util.logging.Logger;
4247
import java.util.zip.GZIPOutputStream;
4348
import javax.annotation.Nullable;
4449

@@ -52,6 +57,7 @@ public final class PrometheusHttpServer implements Closeable, MetricReader {
5257

5358
private static final DaemonThreadFactory THREAD_FACTORY =
5459
new DaemonThreadFactory("prometheus-http");
60+
private static final Logger LOGGER = Logger.getLogger(PrometheusHttpServer.class.getName());
5561

5662
private final HttpServer server;
5763
private final ExecutorService executor;
@@ -77,9 +83,10 @@ public static PrometheusHttpServerBuilder builder() {
7783
} catch (IOException e) {
7884
throw new UncheckedIOException("Could not create Prometheus HTTP server", e);
7985
}
80-
server.createContext("/", new MetricsHandler(() -> getMetricProducer().collectAllMetrics()));
81-
server.createContext(
82-
"/metrics", new MetricsHandler(() -> getMetricProducer().collectAllMetrics()));
86+
MetricsHandler metricsHandler =
87+
new MetricsHandler(() -> getMetricProducer().collectAllMetrics());
88+
server.createContext("/", metricsHandler);
89+
server.createContext("/metrics", metricsHandler);
8390
server.createContext("/-/healthy", HealthHandler.INSTANCE);
8491

8592
executor = Executors.newFixedThreadPool(5, THREAD_FACTORY);
@@ -159,6 +166,9 @@ InetSocketAddress getAddress() {
159166

160167
private static class MetricsHandler implements HttpHandler {
161168

169+
private final Set<String> allConflictHeaderNames =
170+
Collections.newSetFromMap(new ConcurrentHashMap<>());
171+
162172
private final Supplier<Collection<MetricData>> metricsSupplier;
163173

164174
private MetricsHandler(Supplier<Collection<MetricData>> metricsSupplier) {
@@ -190,7 +200,15 @@ public void handle(HttpExchange exchange) throws IOException {
190200
} else {
191201
out = exchange.getResponseBody();
192202
}
193-
serializer.write(metrics, out);
203+
Set<String> conflictHeaderNames = serializer.write(metrics, out);
204+
conflictHeaderNames.removeAll(allConflictHeaderNames);
205+
if (conflictHeaderNames.size() > 0 && LOGGER.isLoggable(Level.WARNING)) {
206+
LOGGER.log(
207+
Level.WARNING,
208+
"Metric conflict(s) detected. Multiple metrics with same name but different type: "
209+
+ conflictHeaderNames.stream().collect(joining(",", "[", "]")));
210+
allConflictHeaderNames.addAll(conflictHeaderNames);
211+
}
194212
}
195213
exchange.close();
196214
}

exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Serializer.java

+56-54
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,25 @@
4646
import java.io.UncheckedIOException;
4747
import java.io.Writer;
4848
import java.nio.charset.StandardCharsets;
49+
import java.util.ArrayList;
4950
import java.util.Collection;
5051
import java.util.Collections;
52+
import java.util.HashSet;
5153
import java.util.LinkedHashMap;
54+
import java.util.LinkedHashSet;
5255
import java.util.List;
5356
import java.util.Map;
5457
import java.util.Optional;
58+
import java.util.Set;
5559
import java.util.concurrent.TimeUnit;
5660
import java.util.function.BiConsumer;
5761
import java.util.function.Predicate;
58-
import java.util.stream.Collectors;
5962
import javax.annotation.Nullable;
6063

6164
/** Serializes metrics into Prometheus exposition formats. */
6265
// Adapted from
6366
// https://github.com/prometheus/client_java/blob/master/simpleclient_common/src/main/java/io/prometheus/client/exporter/common/TextFormat.java
6467
abstract class Serializer {
65-
6668
static Serializer create(@Nullable String acceptHeader, Predicate<String> filter) {
6769
if (acceptHeader == null) {
6870
return new Prometheus004Serializer(filter);
@@ -100,61 +102,64 @@ abstract void writeExemplar(
100102

101103
abstract void writeEof(Writer writer) throws IOException;
102104

103-
final void write(Collection<MetricData> metrics, OutputStream output) throws IOException {
104-
Map<InstrumentationScopeInfo, List<MetricData>> metricsByScope =
105-
metrics.stream()
106-
// Not supported in specification yet.
107-
.filter(metric -> metric.getType() != MetricDataType.EXPONENTIAL_HISTOGRAM)
108-
// PrometheusHttpServer#getAggregationTemporality specifies cumulative temporality for
109-
// all instruments, but non-SDK MetricProducers may not conform. We drop delta
110-
// temporality metrics to avoid the complexity of stateful transformation to cumulative.
111-
.filter(metric -> !isDeltaTemporality(metric))
112-
.filter(metric -> metricNameFilter.test(metricName(metric)))
113-
.collect(
114-
Collectors.groupingBy(
115-
MetricData::getInstrumentationScopeInfo,
116-
LinkedHashMap::new,
117-
Collectors.toList()));
105+
final Set<String> write(Collection<MetricData> metrics, OutputStream output) throws IOException {
106+
Set<String> conflictMetricNames = new HashSet<>();
107+
Map<String, List<MetricData>> metricsByName = new LinkedHashMap<>();
108+
Set<InstrumentationScopeInfo> scopes = new LinkedHashSet<>();
109+
// Iterate through metrics, filtering and grouping by headerName
110+
for (MetricData metric : metrics) {
111+
// Not supported in specification yet.
112+
if (metric.getType() == MetricDataType.EXPONENTIAL_HISTOGRAM) {
113+
continue;
114+
}
115+
// PrometheusHttpServer#getAggregationTemporality specifies cumulative temporality for
116+
// all instruments, but non-SDK MetricProducers may not conform. We drop delta
117+
// temporality metrics to avoid the complexity of stateful transformation to cumulative.
118+
if (isDeltaTemporality(metric)) {
119+
continue;
120+
}
121+
PrometheusType prometheusType = PrometheusType.forMetric(metric);
122+
String metricName = metricName(metric.getName(), prometheusType);
123+
// Skip metrics which do not pass metricNameFilter
124+
if (!metricNameFilter.test(metricName)) {
125+
continue;
126+
}
127+
List<MetricData> metricsWithHeaderName =
128+
metricsByName.computeIfAbsent(metricName, unused -> new ArrayList<>());
129+
// Skip metrics with the same name but different type
130+
if (metricsWithHeaderName.size() > 0
131+
&& prometheusType != PrometheusType.forMetric(metricsWithHeaderName.get(0))) {
132+
conflictMetricNames.add(metricName);
133+
continue;
134+
}
135+
136+
metricsWithHeaderName.add(metric);
137+
scopes.add(metric.getInstrumentationScopeInfo());
138+
}
139+
118140
Optional<Resource> optResource = metrics.stream().findFirst().map(MetricData::getResource);
119141
try (Writer writer =
120142
new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) {
121143
if (optResource.isPresent()) {
122144
writeResource(optResource.get(), writer);
123145
}
124-
for (Map.Entry<InstrumentationScopeInfo, List<MetricData>> entry :
125-
metricsByScope.entrySet()) {
146+
for (InstrumentationScopeInfo scope : scopes) {
147+
writeScopeInfo(scope, writer);
148+
}
149+
for (Map.Entry<String, List<MetricData>> entry : metricsByName.entrySet()) {
126150
write(entry.getValue(), entry.getKey(), writer);
127151
}
128152
writeEof(writer);
129153
}
154+
return conflictMetricNames;
130155
}
131156

132-
private void write(
133-
List<MetricData> metrics, InstrumentationScopeInfo instrumentationScopeInfo, Writer writer)
134-
throws IOException {
135-
writeScopeInfo(instrumentationScopeInfo, writer);
136-
// Group metrics with the scope, name, but different types. This is a semantic error which the
137-
// SDK warns about but passes through to exporters to handle.
138-
Map<String, List<MetricData>> metricsByName =
139-
metrics.stream()
140-
.collect(
141-
Collectors.groupingBy(
142-
metric ->
143-
headerName(
144-
NameSanitizer.INSTANCE.apply(metric.getName()),
145-
PrometheusType.forMetric(metric)),
146-
LinkedHashMap::new,
147-
Collectors.toList()));
148-
149-
for (Map.Entry<String, List<MetricData>> entry : metricsByName.entrySet()) {
150-
write(entry.getValue(), entry.getKey(), writer);
151-
}
152-
}
153-
154-
private void write(List<MetricData> metrics, String headerName, Writer writer)
157+
private void write(List<MetricData> metrics, String metricName, Writer writer)
155158
throws IOException {
156159
// Write header based on first metric
157-
PrometheusType type = PrometheusType.forMetric(metrics.get(0));
160+
MetricData first = metrics.get(0);
161+
PrometheusType type = PrometheusType.forMetric(first);
162+
String headerName = headerName(NameSanitizer.INSTANCE.apply(first.getName()), type);
158163
String description = metrics.get(0).getDescription();
159164

160165
writer.write("# TYPE ");
@@ -171,21 +176,19 @@ private void write(List<MetricData> metrics, String headerName, Writer writer)
171176

172177
// Then write the metrics.
173178
for (MetricData metric : metrics) {
174-
write(metric, writer);
179+
write(metric, metricName, writer);
175180
}
176181
}
177182

178-
private void write(MetricData metric, Writer writer) throws IOException {
179-
String name = metricName(metric);
180-
183+
private void write(MetricData metric, String metricName, Writer writer) throws IOException {
181184
for (PointData point : getPoints(metric)) {
182185
switch (metric.getType()) {
183186
case DOUBLE_SUM:
184187
case DOUBLE_GAUGE:
185188
writePoint(
186189
writer,
187190
metric.getInstrumentationScopeInfo(),
188-
name,
191+
metricName,
189192
((DoublePointData) point).getValue(),
190193
point.getAttributes(),
191194
point.getEpochNanos());
@@ -195,18 +198,18 @@ private void write(MetricData metric, Writer writer) throws IOException {
195198
writePoint(
196199
writer,
197200
metric.getInstrumentationScopeInfo(),
198-
name,
201+
metricName,
199202
(double) ((LongPointData) point).getValue(),
200203
point.getAttributes(),
201204
point.getEpochNanos());
202205
break;
203206
case HISTOGRAM:
204207
writeHistogram(
205-
writer, metric.getInstrumentationScopeInfo(), name, (HistogramPointData) point);
208+
writer, metric.getInstrumentationScopeInfo(), metricName, (HistogramPointData) point);
206209
break;
207210
case SUMMARY:
208211
writeSummary(
209-
writer, metric.getInstrumentationScopeInfo(), name, (SummaryPointData) point);
212+
writer, metric.getInstrumentationScopeInfo(), metricName, (SummaryPointData) point);
210213
break;
211214
case EXPONENTIAL_HISTOGRAM:
212215
throw new IllegalArgumentException("Can't happen");
@@ -648,9 +651,8 @@ static Collection<? extends PointData> getPoints(MetricData metricData) {
648651
return Collections.emptyList();
649652
}
650653

651-
private static String metricName(MetricData metric) {
652-
PrometheusType type = PrometheusType.forMetric(metric);
653-
String name = NameSanitizer.INSTANCE.apply(metric.getName());
654+
private static String metricName(String rawMetricName, PrometheusType type) {
655+
String name = NameSanitizer.INSTANCE.apply(rawMetricName);
654656
if (type == PrometheusType.COUNTER) {
655657
name = name + "_total";
656658
}

exporters/prometheus/src/module/java/module-info.java

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44

55
requires transitive io.opentelemetry.sdk.metrics;
66
requires jdk.httpserver;
7+
requires java.logging;
78
}

0 commit comments

Comments
 (0)