Skip to content

Commit e2936d4

Browse files
Abhishekkr3003jack-berg
andauthoredAug 8, 2024··
Add config to enable Default Exponential Histogram for Prometheus Exporter (#6541)
Co-authored-by: Jack Berg <jberg@newrelic.com>
1 parent fc283ba commit e2936d4

File tree

9 files changed

+175
-14
lines changed

9 files changed

+175
-14
lines changed
 

‎dependencyManagement/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ val DEPENDENCIES = listOf(
5050
"org.mockito:mockito-junit-jupiter:${mockitoVersion}",
5151
"org.slf4j:slf4j-simple:${slf4jVersion}",
5252
"org.slf4j:jul-to-slf4j:${slf4jVersion}",
53+
"io.prometheus:prometheus-metrics-shaded-protobuf:1.3.1",
5354
"io.prometheus:simpleclient:${prometheusClientVersion}",
5455
"io.prometheus:simpleclient_common:${prometheusClientVersion}",
5556
"io.prometheus:simpleclient_httpserver:${prometheusClientVersion}",

‎exporters/common/src/main/java/io/opentelemetry/exporter/internal/ExporterBuilderUtil.java

+25
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@
55

66
package io.opentelemetry.exporter.internal;
77

8+
import static io.opentelemetry.sdk.metrics.Aggregation.explicitBucketHistogram;
9+
810
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
911
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
1012
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
1113
import io.opentelemetry.sdk.common.export.MemoryMode;
14+
import io.opentelemetry.sdk.metrics.Aggregation;
15+
import io.opentelemetry.sdk.metrics.InstrumentType;
16+
import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector;
17+
import io.opentelemetry.sdk.metrics.internal.aggregator.AggregationUtil;
1218
import java.net.URI;
1319
import java.net.URISyntaxException;
1420
import java.util.Locale;
@@ -71,5 +77,24 @@ public static void configureExporterMemoryMode(
7177
memoryModeConsumer.accept(memoryMode);
7278
}
7379

80+
/**
81+
* Invoke the {@code defaultAggregationSelectorConsumer} with the configured {@link
82+
* DefaultAggregationSelector}.
83+
*/
84+
public static void configureHistogramDefaultAggregation(
85+
String defaultHistogramAggregation,
86+
Consumer<DefaultAggregationSelector> defaultAggregationSelectorConsumer) {
87+
if (AggregationUtil.aggregationName(Aggregation.base2ExponentialBucketHistogram())
88+
.equalsIgnoreCase(defaultHistogramAggregation)) {
89+
defaultAggregationSelectorConsumer.accept(
90+
DefaultAggregationSelector.getDefault()
91+
.with(InstrumentType.HISTOGRAM, Aggregation.base2ExponentialBucketHistogram()));
92+
} else if (!AggregationUtil.aggregationName(explicitBucketHistogram())
93+
.equalsIgnoreCase(defaultHistogramAggregation)) {
94+
throw new ConfigurationException(
95+
"Unrecognized default histogram aggregation: " + defaultHistogramAggregation);
96+
}
97+
}
98+
7499
private ExporterBuilderUtil() {}
75100
}

‎exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/OtlpConfigUtil.java

+3-12
Original file line numberDiff line numberDiff line change
@@ -310,18 +310,9 @@ public static void configureOtlpHistogramDefaultAggregation(
310310
Consumer<DefaultAggregationSelector> defaultAggregationSelectorConsumer) {
311311
String defaultHistogramAggregation =
312312
config.getString("otel.exporter.otlp.metrics.default.histogram.aggregation");
313-
if (defaultHistogramAggregation == null) {
314-
return;
315-
}
316-
if (AggregationUtil.aggregationName(Aggregation.base2ExponentialBucketHistogram())
317-
.equalsIgnoreCase(defaultHistogramAggregation)) {
318-
defaultAggregationSelectorConsumer.accept(
319-
DefaultAggregationSelector.getDefault()
320-
.with(InstrumentType.HISTOGRAM, Aggregation.base2ExponentialBucketHistogram()));
321-
} else if (!AggregationUtil.aggregationName(explicitBucketHistogram())
322-
.equalsIgnoreCase(defaultHistogramAggregation)) {
323-
throw new ConfigurationException(
324-
"Unrecognized default histogram aggregation: " + defaultHistogramAggregation);
313+
if (defaultHistogramAggregation != null) {
314+
ExporterBuilderUtil.configureHistogramDefaultAggregation(
315+
defaultHistogramAggregation, defaultAggregationSelectorConsumer);
325316
}
326317
}
327318

‎exporters/prometheus/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies {
1919

2020
testImplementation(project(":sdk:testing"))
2121
testImplementation("io.opentelemetry.proto:opentelemetry-proto")
22+
testImplementation("io.prometheus:prometheus-metrics-shaded-protobuf")
2223
testImplementation("com.sun.net.httpserver:http")
2324
testImplementation("com.google.guava:guava")
2425
testImplementation("com.linecorp.armeria:armeria")

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
import io.opentelemetry.sdk.common.CompletableResultCode;
1515
import io.opentelemetry.sdk.common.export.MemoryMode;
1616
import io.opentelemetry.sdk.internal.DaemonThreadFactory;
17+
import io.opentelemetry.sdk.metrics.Aggregation;
1718
import io.opentelemetry.sdk.metrics.InstrumentType;
1819
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
1920
import io.opentelemetry.sdk.metrics.export.CollectionRegistration;
21+
import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector;
2022
import io.opentelemetry.sdk.metrics.export.MetricReader;
2123
import io.prometheus.metrics.exporter.httpserver.HTTPServer;
2224
import io.prometheus.metrics.model.registry.PrometheusRegistry;
@@ -41,6 +43,7 @@ public final class PrometheusHttpServer implements MetricReader {
4143
private final PrometheusRegistry prometheusRegistry;
4244
private final String host;
4345
private final MemoryMode memoryMode;
46+
private final DefaultAggregationSelector defaultAggregationSelector;
4447

4548
/**
4649
* Returns a new {@link PrometheusHttpServer} which can be registered to an {@link
@@ -65,7 +68,8 @@ public static PrometheusHttpServerBuilder builder() {
6568
boolean otelScopeEnabled,
6669
@Nullable Predicate<String> allowedResourceAttributesFilter,
6770
MemoryMode memoryMode,
68-
@Nullable HttpHandler defaultHandler) {
71+
@Nullable HttpHandler defaultHandler,
72+
DefaultAggregationSelector defaultAggregationSelector) {
6973
this.builder = builder;
7074
this.prometheusMetricReader =
7175
new PrometheusMetricReader(otelScopeEnabled, allowedResourceAttributesFilter);
@@ -92,13 +96,19 @@ public static PrometheusHttpServerBuilder builder() {
9296
} catch (IOException e) {
9397
throw new UncheckedIOException("Could not create Prometheus HTTP server", e);
9498
}
99+
this.defaultAggregationSelector = defaultAggregationSelector;
95100
}
96101

97102
@Override
98103
public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) {
99104
return prometheusMetricReader.getAggregationTemporality(instrumentType);
100105
}
101106

107+
@Override
108+
public Aggregation getDefaultAggregation(InstrumentType instrumentType) {
109+
return defaultAggregationSelector.getDefaultAggregation(instrumentType);
110+
}
111+
102112
@Override
103113
public MemoryMode getMemoryMode() {
104114
return memoryMode;

‎exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServerBuilder.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
import com.sun.net.httpserver.HttpHandler;
1212
import io.opentelemetry.sdk.common.export.MemoryMode;
13+
import io.opentelemetry.sdk.metrics.InstrumentType;
14+
import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector;
15+
import io.opentelemetry.sdk.metrics.export.MetricExporter;
1316
import io.prometheus.metrics.model.registry.PrometheusRegistry;
1417
import java.util.concurrent.ExecutorService;
1518
import java.util.concurrent.Executors;
@@ -31,6 +34,8 @@ public final class PrometheusHttpServerBuilder {
3134
@Nullable private ExecutorService executor;
3235
private MemoryMode memoryMode = DEFAULT_MEMORY_MODE;
3336
@Nullable private HttpHandler defaultHandler;
37+
private DefaultAggregationSelector defaultAggregationSelector =
38+
DefaultAggregationSelector.getDefault();
3439

3540
PrometheusHttpServerBuilder() {}
3641

@@ -41,6 +46,7 @@ public final class PrometheusHttpServerBuilder {
4146
this.otelScopeEnabled = builder.otelScopeEnabled;
4247
this.allowedResourceAttributesFilter = builder.allowedResourceAttributesFilter;
4348
this.executor = builder.executor;
49+
this.defaultAggregationSelector = builder.defaultAggregationSelector;
4450
}
4551

4652
/** Sets the host to bind to. If unset, defaults to {@value #DEFAULT_HOST}. */
@@ -126,6 +132,19 @@ public PrometheusHttpServerBuilder setDefaultHandler(HttpHandler defaultHandler)
126132
return this;
127133
}
128134

135+
/**
136+
* Set the {@link DefaultAggregationSelector} used for {@link
137+
* MetricExporter#getDefaultAggregation(InstrumentType)}.
138+
*
139+
* <p>If unset, defaults to {@link DefaultAggregationSelector#getDefault()}.
140+
*/
141+
public PrometheusHttpServerBuilder setDefaultAggregationSelector(
142+
DefaultAggregationSelector defaultAggregationSelector) {
143+
requireNonNull(defaultAggregationSelector, "defaultAggregationSelector");
144+
this.defaultAggregationSelector = defaultAggregationSelector;
145+
return this;
146+
}
147+
129148
/**
130149
* Returns a new {@link PrometheusHttpServer} with the configuration of this builder which can be
131150
* registered with a {@link io.opentelemetry.sdk.metrics.SdkMeterProvider}.
@@ -140,6 +159,7 @@ public PrometheusHttpServer build() {
140159
otelScopeEnabled,
141160
allowedResourceAttributesFilter,
142161
memoryMode,
143-
defaultHandler);
162+
defaultHandler,
163+
defaultAggregationSelector);
144164
}
145165
}

‎exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/internal/PrometheusMetricReaderProvider.java

+8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ public MetricReader createMetricReader(ConfigProperties config) {
3535

3636
ExporterBuilderUtil.configureExporterMemoryMode(config, prometheusBuilder::setMemoryMode);
3737

38+
String defaultHistogramAggregation =
39+
config.getString(
40+
"otel.java.experimental.exporter.prometheus.metrics.default.histogram.aggregation");
41+
if (defaultHistogramAggregation != null) {
42+
ExporterBuilderUtil.configureHistogramDefaultAggregation(
43+
defaultHistogramAggregation, prometheusBuilder::setDefaultAggregationSelector);
44+
}
45+
3846
return prometheusBuilder.build();
3947
}
4048

‎exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServerTest.java

+82
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,24 @@
1616
import com.linecorp.armeria.client.retry.RetryRule;
1717
import com.linecorp.armeria.client.retry.RetryingClient;
1818
import com.linecorp.armeria.common.AggregatedHttpResponse;
19+
import com.linecorp.armeria.common.HttpData;
1920
import com.linecorp.armeria.common.HttpHeaderNames;
2021
import com.linecorp.armeria.common.HttpMethod;
2122
import com.linecorp.armeria.common.HttpStatus;
2223
import com.linecorp.armeria.common.RequestHeaders;
2324
import io.github.netmikey.logunit.api.LogCapturer;
2425
import io.opentelemetry.api.common.Attributes;
26+
import io.opentelemetry.api.metrics.DoubleHistogram;
2527
import io.opentelemetry.internal.testing.slf4j.SuppressLogger;
2628
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
2729
import io.opentelemetry.sdk.common.export.MemoryMode;
30+
import io.opentelemetry.sdk.metrics.Aggregation;
31+
import io.opentelemetry.sdk.metrics.InstrumentType;
32+
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
2833
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
2934
import io.opentelemetry.sdk.metrics.data.MetricData;
3035
import io.opentelemetry.sdk.metrics.export.CollectionRegistration;
36+
import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector;
3137
import io.opentelemetry.sdk.metrics.internal.data.ImmutableDoublePointData;
3238
import io.opentelemetry.sdk.metrics.internal.data.ImmutableGaugeData;
3339
import io.opentelemetry.sdk.metrics.internal.data.ImmutableLongPointData;
@@ -36,7 +42,9 @@
3642
import io.opentelemetry.sdk.resources.Resource;
3743
import io.prometheus.metrics.exporter.httpserver.HTTPServer;
3844
import io.prometheus.metrics.exporter.httpserver.MetricsHandler;
45+
import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_3_25_3.Metrics;
3946
import io.prometheus.metrics.model.registry.PrometheusRegistry;
47+
import io.prometheus.metrics.shaded.com_google_protobuf_3_25_3.TextFormat;
4048
import java.io.ByteArrayInputStream;
4149
import java.io.IOException;
4250
import java.net.ServerSocket;
@@ -113,6 +121,9 @@ void invalidConfig() {
113121
assertThatThrownBy(() -> PrometheusHttpServer.builder().setHost(""))
114122
.isInstanceOf(IllegalArgumentException.class)
115123
.hasMessage("host must not be empty");
124+
assertThatThrownBy(() -> PrometheusHttpServer.builder().setDefaultAggregationSelector(null))
125+
.isInstanceOf(NullPointerException.class)
126+
.hasMessage("defaultAggregationSelector");
116127
}
117128

118129
@Test
@@ -526,4 +537,75 @@ void toBuilder() {
526537
.hasFieldOrPropertyWithValue("executor", executor)
527538
.hasFieldOrPropertyWithValue("prometheusRegistry", prometheusRegistry);
528539
}
540+
541+
/**
542+
* Set the default histogram aggregation to be {@link
543+
* Aggregation#base2ExponentialBucketHistogram()}. In order to validate that exponential
544+
* histograms are produced, we request protobuf encoded metrics when scraping since the prometheus
545+
* text format does not support native histograms. We parse the binary content protobuf payload to
546+
* the protobuf java bindings, and assert against the string representation.
547+
*/
548+
@Test
549+
void histogramDefaultBase2ExponentialHistogram() throws IOException {
550+
PrometheusHttpServer prometheusServer =
551+
PrometheusHttpServer.builder()
552+
.setHost("localhost")
553+
.setPort(0)
554+
.setDefaultAggregationSelector(
555+
DefaultAggregationSelector.getDefault()
556+
.with(InstrumentType.HISTOGRAM, Aggregation.base2ExponentialBucketHistogram()))
557+
.build();
558+
try (SdkMeterProvider meterProvider =
559+
SdkMeterProvider.builder().registerMetricReader(prometheusServer).build()) {
560+
DoubleHistogram histogram = meterProvider.get("meter").histogramBuilder("histogram").build();
561+
histogram.record(1.0);
562+
563+
WebClient client =
564+
WebClient.builder("http://localhost:" + prometheusServer.getAddress().getPort())
565+
.decorator(RetryingClient.newDecorator(RetryRule.failsafe()))
566+
// Request protobuf binary encoding, which is required for the prometheus native
567+
// histogram format
568+
.addHeader(
569+
"Accept",
570+
"application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily")
571+
.build();
572+
AggregatedHttpResponse response = client.get("/metrics").aggregate().join();
573+
assertThat(response.status()).isEqualTo(HttpStatus.OK);
574+
assertThat(response.headers().get(HttpHeaderNames.CONTENT_TYPE))
575+
.isEqualTo(
576+
"application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited");
577+
// Parse the data to Metrics.MetricFamily protobuf java binding and assert against the string
578+
// representation
579+
try (HttpData data = response.content()) {
580+
Metrics.MetricFamily metricFamily =
581+
Metrics.MetricFamily.parseDelimitedFrom(data.toInputStream());
582+
String s = TextFormat.printer().printToString(metricFamily);
583+
assertThat(s)
584+
.isEqualTo(
585+
"name: \"histogram\"\n"
586+
+ "help: \"\"\n"
587+
+ "type: HISTOGRAM\n"
588+
+ "metric {\n"
589+
+ " label {\n"
590+
+ " name: \"otel_scope_name\"\n"
591+
+ " value: \"meter\"\n"
592+
+ " }\n"
593+
+ " histogram {\n"
594+
+ " sample_count: 1\n"
595+
+ " sample_sum: 1.0\n"
596+
+ " schema: 8\n"
597+
+ " zero_threshold: 0.0\n"
598+
+ " zero_count: 0\n"
599+
+ " positive_span {\n"
600+
+ " offset: 0\n"
601+
+ " length: 1\n"
602+
+ " }\n"
603+
+ " positive_delta: 1\n"
604+
+ " }\n"
605+
+ "}\n");
606+
}
607+
} finally {
608+
prometheusServer.shutdown();
609+
}
610+
}
529611
}

‎exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/internal/PrometheusMetricReaderProviderTest.java

+23
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@
77

88
import static org.assertj.core.api.Assertions.as;
99
import static org.assertj.core.api.Assertions.assertThat;
10+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1011
import static org.mockito.ArgumentMatchers.any;
1112
import static org.mockito.Mockito.when;
1213

1314
import com.sun.net.httpserver.HttpServer;
1415
import io.opentelemetry.exporter.prometheus.PrometheusHttpServer;
1516
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
17+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
1618
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
1719
import io.opentelemetry.sdk.common.export.MemoryMode;
20+
import io.opentelemetry.sdk.metrics.Aggregation;
21+
import io.opentelemetry.sdk.metrics.InstrumentType;
1822
import io.opentelemetry.sdk.metrics.export.MetricReader;
1923
import io.prometheus.metrics.exporter.httpserver.HTTPServer;
2024
import java.io.IOException;
@@ -59,6 +63,8 @@ void createMetricReader_Default() throws IOException {
5963
assertThat(server.getAddress().getPort()).isEqualTo(9464);
6064
});
6165
assertThat(metricReader.getMemoryMode()).isEqualTo(MemoryMode.IMMUTABLE_DATA);
66+
assertThat(metricReader.getDefaultAggregation(InstrumentType.HISTOGRAM))
67+
.isEqualTo(Aggregation.defaultAggregation());
6268
}
6369
}
6470

@@ -76,6 +82,9 @@ void createMetricReader_WithConfiguration() throws IOException {
7682
config.put("otel.exporter.prometheus.host", "localhost");
7783
config.put("otel.exporter.prometheus.port", String.valueOf(port));
7884
config.put("otel.java.experimental.exporter.memory_mode", "reusable_data");
85+
config.put(
86+
"otel.java.experimental.exporter.prometheus.metrics.default.histogram.aggregation",
87+
"BASE2_EXPONENTIAL_BUCKET_HISTOGRAM");
7988

8089
when(configProperties.getInt(any())).thenReturn(null);
8190
when(configProperties.getString(any())).thenReturn(null);
@@ -91,6 +100,20 @@ void createMetricReader_WithConfiguration() throws IOException {
91100
assertThat(server.getAddress().getPort()).isEqualTo(port);
92101
});
93102
assertThat(metricReader.getMemoryMode()).isEqualTo(MemoryMode.REUSABLE_DATA);
103+
assertThat(metricReader.getDefaultAggregation(InstrumentType.HISTOGRAM))
104+
.isEqualTo(Aggregation.base2ExponentialBucketHistogram());
94105
}
95106
}
107+
108+
@Test
109+
void createMetricReader_WithWrongConfiguration() {
110+
Map<String, String> config = new HashMap<>();
111+
config.put(
112+
"otel.java.experimental.exporter.prometheus.metrics.default.histogram.aggregation", "foo");
113+
114+
assertThatThrownBy(
115+
() -> provider.createMetricReader(DefaultConfigProperties.createFromMap(config)))
116+
.isInstanceOf(ConfigurationException.class)
117+
.hasMessageContaining("Unrecognized default histogram aggregation:");
118+
}
96119
}

0 commit comments

Comments
 (0)
Please sign in to comment.