Skip to content

Commit

Permalink
Implement prometheus exporter provider (open-telemetry#5053)
Browse files Browse the repository at this point in the history
* Implement prometheus exporter provider

* Remove stray comment

* Adjust method visibility

* Delete ClasspathUtil
  • Loading branch information
jack-berg authored and dmarkwat committed Dec 30, 2022
1 parent 04d208b commit f939724
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 151 deletions.
2 changes: 2 additions & 0 deletions exporters/prometheus/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ otelJava.moduleName.set("io.opentelemetry.exporter.prometheus")
dependencies {
api(project(":sdk:metrics"))

implementation(project(":sdk-extensions:autoconfigure-spi"))

compileOnly("com.sun.net.httpserver:http")

testImplementation("com.google.guava:guava")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.exporter.prometheus.internal;

import io.opentelemetry.exporter.prometheus.PrometheusHttpServer;
import io.opentelemetry.exporter.prometheus.PrometheusHttpServerBuilder;
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer;
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;

/**
* SPI implementation for {@link PrometheusHttpServer}.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*/
public class PrometheusCustomizerProvider implements AutoConfigurationCustomizerProvider {

@Override
public void customize(AutoConfigurationCustomizer autoConfiguration) {
autoConfiguration.addMeterProviderCustomizer(
(builder, config) -> {
boolean prometheusEnabled =
config.getList("otel.metrics.exporter").contains("prometheus");
if (prometheusEnabled) {
builder.registerMetricReader(configurePrometheusHttpServer(config));
}
return builder;
});
}

// Visible for test
static PrometheusHttpServer configurePrometheusHttpServer(ConfigProperties config) {
PrometheusHttpServerBuilder prometheusBuilder = PrometheusHttpServer.builder();

Integer port = config.getInt("otel.exporter.prometheus.port");
if (port != null) {
prometheusBuilder.setPort(port);
}
String host = config.getString("otel.exporter.prometheus.host");
if (host != null) {
prometheusBuilder.setHost(host);
}
return prometheusBuilder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.opentelemetry.exporter.prometheus.internal.PrometheusCustomizerProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.exporter.prometheus.internal;

import static org.assertj.core.api.Assertions.as;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.when;

import com.sun.net.httpserver.HttpServer;
import io.opentelemetry.exporter.prometheus.PrometheusHttpServer;
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;

@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class PrometheusCustomizerProviderTest {

private static final PrometheusCustomizerProvider provider = new PrometheusCustomizerProvider();

private SdkMeterProviderBuilder meterProviderBuilder;

@Mock private ConfigProperties configProperties;

@Mock private AutoConfigurationCustomizer customizer;

@BeforeEach
void setup() {
meterProviderBuilder = SdkMeterProvider.builder();
doAnswer(
invocation -> {
BiFunction<SdkMeterProviderBuilder, ConfigProperties, SdkMeterProviderBuilder>
meterProviderCustomizer = invocation.getArgument(0);
meterProviderBuilder =
meterProviderCustomizer.apply(meterProviderBuilder, configProperties);
return null;
})
.when(customizer)
.addMeterProviderCustomizer(any());
}

@Test
void customize_PrometheusEnabled() {
when(configProperties.getList("otel.metrics.exporter"))
.thenReturn(Collections.singletonList("prometheus"));
provider.customize(customizer);

try (SdkMeterProvider meterProvider = meterProviderBuilder.build()) {
assertThat(meterProvider)
.extracting("registeredReaders", as(InstanceOfAssertFactories.list(Object.class)))
.satisfiesExactly(
registeredReader ->
assertThat(registeredReader)
.extracting("metricReader")
.isInstanceOf(PrometheusHttpServer.class));
}
}

@Test
void customize_PrometheusDisabled() {
when(configProperties.getList("otel.metrics.exporter"))
.thenReturn(Collections.singletonList("foo"));
provider.customize(customizer);

try (SdkMeterProvider meterProvider = meterProviderBuilder.build()) {
assertThat(meterProvider)
.extracting("registeredReaders", as(InstanceOfAssertFactories.list(Object.class)))
.isEmpty();
}
}

@Test
void configurePrometheusHttpServer_Default() {
try (PrometheusHttpServer prometheusHttpServer =
PrometheusCustomizerProvider.configurePrometheusHttpServer(
DefaultConfigProperties.createForTest(Collections.emptyMap()))) {
assertThat(prometheusHttpServer)
.extracting("server", as(InstanceOfAssertFactories.type(HttpServer.class)))
.satisfies(
server -> {
assertThat(server.getAddress().getHostName()).isEqualTo("0:0:0:0:0:0:0:0");
assertThat(server.getAddress().getPort()).isEqualTo(9464);
});
}
}

@Test
void configurePrometheusHttpServer_WithConfiguration() throws IOException {
// Find a random unused port. There's a small race if another process takes it before we
// initialize. Consider adding retries to this test if it flakes, presumably it never will on
// CI since there's no prometheus there blocking the well-known port.
int port;
try (ServerSocket socket2 = new ServerSocket(0)) {
port = socket2.getLocalPort();
}

Map<String, String> config = new HashMap<>();
config.put("otel.exporter.prometheus.host", "localhost");
config.put("otel.exporter.prometheus.port", String.valueOf(port));

try (PrometheusHttpServer prometheusHttpServer =
PrometheusCustomizerProvider.configurePrometheusHttpServer(
DefaultConfigProperties.createForTest(config))) {
assertThat(prometheusHttpServer)
.extracting("server", as(InstanceOfAssertFactories.type(HttpServer.class)))
.satisfies(
server -> {
assertThat(server.getAddress().getHostName()).isEqualTo("localhost");
assertThat(server.getAddress().getPort()).isEqualTo(port);
});
}
}
}
20 changes: 0 additions & 20 deletions sdk-extensions/autoconfigure/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ dependencies {

implementation(project(":semconv"))

compileOnly(project(":exporters:prometheus"))

annotationProcessor("com.google.auto.value:auto-value")

testImplementation(project(":sdk:trace-shaded-deps"))
Expand Down Expand Up @@ -60,7 +58,6 @@ testing {
implementation(project(":exporters:logging"))
implementation(project(":exporters:otlp:all"))
implementation(project(":exporters:otlp:logs"))
implementation(project(":exporters:prometheus"))
implementation(project(":exporters:zipkin"))
}
}
Expand Down Expand Up @@ -129,23 +126,6 @@ testing {
runtimeOnly("io.grpc:grpc-netty-shaded")
}
}
val testPrometheus by registering(JvmTestSuite::class) {
dependencies {
implementation(project(":exporters:prometheus"))

implementation("com.linecorp.armeria:armeria-junit5")
}

targets {
all {
testTask {
environment("OTEL_TRACES_EXPORTER", "none")
environment("OTEL_METRICS_EXPORTER", "prometheus")
environment("OTEL_METRIC_EXPORT_INTERVAL", "10")
}
}
}
}
}
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -70,6 +71,7 @@ static List<MetricReader> configureMetricReaders(
exporterName ->
MetricExporterConfiguration.configureReader(
exporterName, config, serviceClassLoader, metricExporterCustomizer))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

package io.opentelemetry.sdk.autoconfigure;

import io.opentelemetry.exporter.prometheus.PrometheusHttpServer;
import io.opentelemetry.exporter.prometheus.PrometheusHttpServerBuilder;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider;
Expand All @@ -17,6 +15,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
import javax.annotation.Nullable;

final class MetricExporterConfiguration {

Expand All @@ -30,22 +29,35 @@ final class MetricExporterConfiguration {
EXPORTER_ARTIFACT_ID_BY_NAME.put("otlp", "opentelemetry-exporter-otlp");
}

@Nullable
static MetricReader configureReader(
String name,
ConfigProperties config,
ClassLoader serviceClassLoader,
BiFunction<? super MetricExporter, ConfigProperties, ? extends MetricExporter>
metricExporterCustomizer) {
if (name.equals("prometheus")) {
return configurePrometheusMetricReader(config);
// PrometheusHttpServer is implemented as MetricReader (not MetricExporter) and uses
// the AutoConfigurationCustomizer#addMeterProviderCustomizer SPI hook instead of
// ConfigurableMetricExporterProvider. While the prometheus SPI hook is not handled here,
// the classpath check here provides uniform exception messages.
try {
Class.forName("io.opentelemetry.exporter.prometheus.PrometheusHttpServer");
return null;
} catch (ClassNotFoundException unused) {
throw missingExporterException("prometheus", "opentelemetry-exporter-prometheus");
}
}

NamedSpiManager<MetricExporter> spiExportersManager =
metricExporterSpiManager(config, serviceClassLoader);

MetricExporter metricExporter = configureExporter(name, spiExportersManager);
metricExporter = metricExporterCustomizer.apply(metricExporter, config);
return configurePeriodicMetricReader(config, metricExporter);

return PeriodicMetricReader.builder(metricExporter)
.setInterval(config.getDuration("otel.metric.export.interval", DEFAULT_EXPORT_INTERVAL))
.build();
}

// Visible for testing
Expand All @@ -66,42 +78,21 @@ static MetricExporter configureExporter(
if (metricExporter == null) {
String artifactId = EXPORTER_ARTIFACT_ID_BY_NAME.get(name);
if (artifactId != null) {
throw new ConfigurationException(
"otel.metrics.exporter set to \""
+ name
+ "\" but "
+ artifactId
+ " not found on classpath. Make sure to add it as a dependency.");
throw missingExporterException(name, artifactId);
}
throw new ConfigurationException("Unrecognized value for otel.metrics.exporter: " + name);
}
return metricExporter;
}

private static PeriodicMetricReader configurePeriodicMetricReader(
ConfigProperties config, MetricExporter exporter) {

return PeriodicMetricReader.builder(exporter)
.setInterval(config.getDuration("otel.metric.export.interval", DEFAULT_EXPORT_INTERVAL))
.build();
}

private static PrometheusHttpServer configurePrometheusMetricReader(ConfigProperties config) {
ClasspathUtil.checkClassExists(
"io.opentelemetry.exporter.prometheus.PrometheusHttpServer",
"Prometheus Metrics Server",
"opentelemetry-exporter-prometheus");
PrometheusHttpServerBuilder prom = PrometheusHttpServer.builder();

Integer port = config.getInt("otel.exporter.prometheus.port");
if (port != null) {
prom.setPort(port);
}
String host = config.getString("otel.exporter.prometheus.host");
if (host != null) {
prom.setHost(host);
}
return prom.build();
private static ConfigurationException missingExporterException(
String exporterName, String artifactId) {
return new ConfigurationException(
"otel.metrics.exporter set to \""
+ exporterName
+ "\" but "
+ artifactId
+ " not found on classpath. Make sure to add it as a dependency.");
}

private MetricExporterConfiguration() {}
Expand Down
Loading

0 comments on commit f939724

Please sign in to comment.