Support comma separated list for exporters for otel.traces.exporter. (#3529)

* Support comma separated list for exporters for otel.traces.exporter.

* Respond to PR feedback.
This commit is contained in:
jack-berg 2021-08-23 16:34:24 -05:00 committed by GitHub
parent bb46b0c0a4
commit a0b0ab4e2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 213 additions and 51 deletions

View File

@ -39,7 +39,7 @@ The following configuration properties are common to all exporters:
| System property | Environment variable | Purpose |
|-----------------|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
| otel.traces.exporter | OTEL_TRACES_EXPORTER | The exporter to be used for tracing. Default is `otlp`. `none` means no autoconfigured exporter. |
| otel.traces.exporter | OTEL_TRACES_EXPORTER | List of exporters to be used for tracing, separated by commas. Default is `otlp`. `none` means no autoconfigured exporter. |
| otel.metrics.exporter | OTEL_METRICS_EXPORTER | The exporter to be used for metrics. Default is `otlp`. `none` means no autoconfigured exporter. |
### OTLP exporter (both span and metric exporters)

View File

@ -6,6 +6,10 @@
package io.opentelemetry.sdk.autoconfigure;
import static io.opentelemetry.sdk.autoconfigure.OtlpConfigUtil.DATA_TYPE_TRACES;
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;
import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter;
import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporterBuilder;
@ -19,25 +23,65 @@ import io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurableSpanExporterProvider;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import java.time.Duration;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
final class SpanExporterConfiguration {
@Nullable
static SpanExporter configureExporter(String name, ConfigProperties config) {
private static final String EXPORTER_NONE = "none";
// Visible for testing
static Map<String, SpanExporter> configureSpanExporters(ConfigProperties config) {
List<String> exporterNamesList = config.getCommaSeparatedValues("otel.traces.exporter");
Set<String> exporterNames = new HashSet<>(exporterNamesList);
if (exporterNamesList.size() != exporterNames.size()) {
String duplicates =
exporterNamesList.stream()
.collect(groupingBy(Function.identity(), counting()))
.entrySet()
.stream()
.filter(entry -> entry.getValue() > 1)
.map(Map.Entry::getKey)
.collect(joining(",", "[", "]"));
throw new ConfigurationException("otel.traces.exporter contains duplicates: " + duplicates);
}
if (exporterNames.contains(EXPORTER_NONE)) {
if (exporterNames.size() > 1) {
throw new ConfigurationException(
"otel.traces.exporter contains " + EXPORTER_NONE + " along with other exporters");
}
return Collections.emptyMap();
}
if (exporterNames.isEmpty()) {
exporterNames = Collections.singleton("otlp");
}
Map<String, SpanExporter> spiExporters =
StreamSupport.stream(
ServiceLoader.load(ConfigurableSpanExporterProvider.class).spliterator(), false)
.collect(
Collectors.toMap(
toMap(
ConfigurableSpanExporterProvider::getName,
configurableSpanExporterProvider ->
configurableSpanExporterProvider.createExporter(config)));
return exporterNames.stream()
.collect(
toMap(
Function.identity(),
exporterName -> configureExporter(exporterName, config, spiExporters)));
}
// Visible for testing
static SpanExporter configureExporter(
String name, ConfigProperties config, Map<String, SpanExporter> spiExporters) {
switch (name) {
case "otlp":
return configureOtlp(config);
@ -51,8 +95,6 @@ final class SpanExporterConfiguration {
"Logging Trace Exporter",
"opentelemetry-exporter-logging");
return new LoggingSpanExporter();
case "none":
return null;
default:
SpanExporter spiExporter = spiExporters.get(name);
if (spiExporter == null) {

View File

@ -19,6 +19,9 @@ import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
@ -44,32 +47,38 @@ final class TracerProviderConfiguration {
configurer.configure(tracerProviderBuilder);
}
String exporterName = config.getString("otel.traces.exporter");
if (exporterName == null) {
exporterName = "otlp";
}
SpanExporter exporter = SpanExporterConfiguration.configureExporter(exporterName, config);
if (exporter != null) {
tracerProviderBuilder.addSpanProcessor(
configureSpanProcessor(config, exporter, exporterName));
}
Map<String, SpanExporter> exportersByName =
SpanExporterConfiguration.configureSpanExporters(config);
configureSpanProcessors(config, exportersByName)
.forEach(tracerProviderBuilder::addSpanProcessor);
SdkTracerProvider tracerProvider = tracerProviderBuilder.build();
Runtime.getRuntime().addShutdownHook(new Thread(tracerProvider::close));
return tracerProvider;
}
// VisibleForTesting
static SpanProcessor configureSpanProcessor(
ConfigProperties config, SpanExporter exporter, String exporterName) {
if (exporterName.equals("logging")) {
return SimpleSpanProcessor.create(exporter);
static List<SpanProcessor> configureSpanProcessors(
ConfigProperties config, Map<String, SpanExporter> exportersByName) {
Map<String, SpanExporter> exportersByNameCopy = new HashMap<>(exportersByName);
List<SpanProcessor> spanProcessors = new ArrayList<>();
if (exportersByNameCopy.containsKey("logging")) {
spanProcessors.add(SimpleSpanProcessor.create(exportersByName.get("logging")));
exportersByNameCopy.remove("logging");
}
return configureSpanProcessor(config, exporter);
if (!exportersByNameCopy.isEmpty()) {
SpanExporter compositeSpanExporter = SpanExporter.composite(exportersByNameCopy.values());
spanProcessors.add(configureBatchSpanProcessor(config, compositeSpanExporter));
}
return spanProcessors;
}
// VisibleForTesting
static BatchSpanProcessor configureSpanProcessor(ConfigProperties config, SpanExporter exporter) {
static BatchSpanProcessor configureBatchSpanProcessor(
ConfigProperties config, SpanExporter exporter) {
BatchSpanProcessorBuilder builder = BatchSpanProcessor.builder(exporter);
Duration scheduleDelay = config.getDuration("otel.bsp.schedule.delay");

View File

@ -19,7 +19,9 @@ class NotOnClasspathTest {
@Test
void otlpGrpcSpans() {
assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("otlp", EMPTY))
assertThatThrownBy(
() ->
SpanExporterConfiguration.configureExporter("otlp", EMPTY, Collections.emptyMap()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(
"OTLP gRPC Trace Exporter enabled but opentelemetry-exporter-otlp not found on "
@ -31,7 +33,9 @@ class NotOnClasspathTest {
ConfigProperties config =
DefaultConfigProperties.createForTest(
Collections.singletonMap("otel.experimental.exporter.otlp.protocol", "http/protobuf"));
assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("otlp", config))
assertThatThrownBy(
() ->
SpanExporterConfiguration.configureExporter("otlp", config, Collections.emptyMap()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(
"OTLP HTTP Trace Exporter enabled but opentelemetry-exporter-otlp-http-trace not found on "
@ -40,7 +44,10 @@ class NotOnClasspathTest {
@Test
void jaeger() {
assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("jaeger", EMPTY))
assertThatThrownBy(
() ->
SpanExporterConfiguration.configureExporter(
"jaeger", EMPTY, Collections.emptyMap()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(
"Jaeger gRPC Exporter enabled but opentelemetry-exporter-jaeger not found on "
@ -49,7 +56,10 @@ class NotOnClasspathTest {
@Test
void zipkin() {
assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("zipkin", EMPTY))
assertThatThrownBy(
() ->
SpanExporterConfiguration.configureExporter(
"zipkin", EMPTY, Collections.emptyMap()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(
"Zipkin Exporter enabled but opentelemetry-exporter-zipkin not found on classpath");
@ -57,7 +67,10 @@ class NotOnClasspathTest {
@Test
void logging() {
assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("logging", EMPTY))
assertThatThrownBy(
() ->
SpanExporterConfiguration.configureExporter(
"logging", EMPTY, Collections.emptyMap()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(
"Logging Trace Exporter enabled but opentelemetry-exporter-logging not found on "

View File

@ -79,9 +79,9 @@ class TracerProviderConfigurationTest {
}
@Test
void configureSpanProcessor_empty() {
void configureBatchSpanProcessor_empty() {
BatchSpanProcessor processor =
TracerProviderConfiguration.configureSpanProcessor(EMPTY, mockSpanExporter);
TracerProviderConfiguration.configureBatchSpanProcessor(EMPTY, mockSpanExporter);
try {
assertThat(processor)
@ -107,7 +107,7 @@ class TracerProviderConfigurationTest {
}
@Test
void configureSpanProcessor_configured() {
void configureBatchSpanProcessor_configured() {
Map<String, String> properties = new HashMap<>();
properties.put("otel.bsp.schedule.delay", "100000");
properties.put("otel.bsp.max.queue.size", "2");
@ -115,7 +115,7 @@ class TracerProviderConfigurationTest {
properties.put("otel.bsp.export.timeout", "4");
BatchSpanProcessor processor =
TracerProviderConfiguration.configureSpanProcessor(
TracerProviderConfiguration.configureBatchSpanProcessor(
DefaultConfigProperties.createForTest(properties), mockSpanExporter);
try {

View File

@ -10,59 +10,146 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.google.common.collect.ImmutableMap;
import io.opentelemetry.exporter.logging.LoggingSpanExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import java.util.Collections;
import java.util.Map;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
public class ConfigurableSpanExporterTest {
@Test
void configuration() {
void configureSpanExporters_spiExporter() {
ConfigProperties config =
DefaultConfigProperties.createForTest(ImmutableMap.of("test.option", "true"));
SpanExporter spanExporter = SpanExporterConfiguration.configureExporter("testExporter", config);
DefaultConfigProperties.createForTest(
ImmutableMap.of("test.option", "true", "otel.traces.exporter", "testExporter"));
Map<String, SpanExporter> exportersByName =
SpanExporterConfiguration.configureSpanExporters(config);
assertThat(spanExporter)
assertThat(exportersByName)
.hasSize(1)
.containsKey("testExporter")
.extracting(map -> map.get("testExporter"))
.isInstanceOf(TestConfigurableSpanExporterProvider.TestSpanExporter.class)
.extracting("config")
.isSameAs(config);
}
@Test
void configureSpanExporters_duplicates() {
ConfigProperties config =
DefaultConfigProperties.createForTest(
ImmutableMap.of("otel.traces.exporter", "otlp,otlp,logging"));
assertThatThrownBy(() -> SpanExporterConfiguration.configureSpanExporters(config))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("otel.traces.exporter contains duplicates: [otlp]");
}
@Test
void configureSpanExporters_multipleWithNone() {
ConfigProperties config =
DefaultConfigProperties.createForTest(ImmutableMap.of("otel.traces.exporter", "otlp,none"));
assertThatThrownBy(() -> SpanExporterConfiguration.configureSpanExporters(config))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("otel.traces.exporter contains none along with other exporters");
}
@Test
void exporterNotFound() {
assertThatThrownBy(
() ->
SpanExporterConfiguration.configureExporter(
"catExporter", DefaultConfigProperties.createForTest(Collections.emptyMap())))
"catExporter",
DefaultConfigProperties.createForTest(Collections.emptyMap()),
Collections.emptyMap()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("catExporter");
}
@Test
void configureSpanProcessor_simpleSpanProcessor() {
void configureSpanProcessors_simpleSpanProcessor() {
String exporterName = "logging";
Map<String, String> propMap = Collections.singletonMap("otel.traces.exporter", exporterName);
SpanExporter exporter = new LoggingSpanExporter();
ConfigProperties properties = DefaultConfigProperties.createForTest(propMap);
assertThat(
TracerProviderConfiguration.configureSpanProcessor(properties, exporter, exporterName))
TracerProviderConfiguration.configureSpanProcessors(
properties, ImmutableMap.of(exporterName, exporter)))
.hasSize(1)
.first()
.isInstanceOf(SimpleSpanProcessor.class);
}
@Test
void configureSpanProcessor_batchSpanProcessor() {
void configureSpanProcessors_batchSpanProcessor() {
String exporterName = "zipkin";
Map<String, String> propMap = Collections.singletonMap("otel.traces.exporter", exporterName);
SpanExporter exporter = ZipkinSpanExporter.builder().build();
ConfigProperties properties = DefaultConfigProperties.createForTest(propMap);
assertThat(
TracerProviderConfiguration.configureSpanProcessor(properties, exporter, exporterName))
TracerProviderConfiguration.configureSpanProcessors(
properties, ImmutableMap.of(exporterName, exporter)))
.hasSize(1)
.first()
.isInstanceOf(BatchSpanProcessor.class);
}
@Test
void configureSpanProcessors_multipleExporters() {
SpanExporter otlpExporter = OtlpGrpcSpanExporter.builder().build();
SpanExporter zipkinExporter = ZipkinSpanExporter.builder().build();
ConfigProperties properties =
DefaultConfigProperties.createForTest(
Collections.singletonMap("otel.traces.exporter", "otlp,zipkin"));
assertThat(
TracerProviderConfiguration.configureSpanProcessors(
properties, ImmutableMap.of("otlp", otlpExporter, "zipkin", zipkinExporter)))
.hasSize(1)
.hasAtLeastOneElementOfType(BatchSpanProcessor.class)
.first()
.extracting("worker")
.extracting("spanExporter")
.asInstanceOf(InstanceOfAssertFactories.type(SpanExporter.class))
.satisfies(
spanExporter -> {
assertThat(spanExporter)
.extracting(spanExporter1 -> spanExporter1.getClass().getSimpleName())
.isEqualTo("MultiSpanExporter");
assertThat(spanExporter)
.extracting("spanExporters")
.asInstanceOf(InstanceOfAssertFactories.type(SpanExporter[].class))
.satisfies(
spanExporters -> {
assertThat(spanExporters.length).isEqualTo(2);
assertThat(spanExporters)
.hasAtLeastOneElementOfType(ZipkinSpanExporter.class)
.hasAtLeastOneElementOfType(OtlpGrpcSpanExporter.class);
});
});
}
@Test
void configureSpanProcessors_multipleExportersWithLogging() {
SpanExporter loggingExporter = new LoggingSpanExporter();
SpanExporter zipkinExporter = ZipkinSpanExporter.builder().build();
ConfigProperties properties =
DefaultConfigProperties.createForTest(
Collections.singletonMap("otel.traces.exporter", "logging,zipkin"));
assertThat(
TracerProviderConfiguration.configureSpanProcessors(
properties, ImmutableMap.of("logging", loggingExporter, "zipkin", zipkinExporter)))
.hasSize(2)
.hasAtLeastOneElementOfType(SimpleSpanProcessor.class)
.hasAtLeastOneElementOfType(BatchSpanProcessor.class);
}
}

View File

@ -36,7 +36,8 @@ class SpanExporterConfigurationTest {
SpanExporterConfiguration.configureExporter(
"otlp",
DefaultConfigProperties.createForTest(
Collections.singletonMap("otel.exporter.otlp.timeout", "10")));
Collections.singletonMap("otel.exporter.otlp.timeout", "10")),
Collections.emptyMap());
try {
assertThat(exporter)
.isInstanceOfSatisfying(
@ -57,7 +58,8 @@ class SpanExporterConfigurationTest {
SpanExporterConfiguration.configureExporter(
"jaeger",
DefaultConfigProperties.createForTest(
Collections.singletonMap("otel.exporter.jaeger.timeout", "10")));
Collections.singletonMap("otel.exporter.jaeger.timeout", "10")),
Collections.emptyMap());
try {
assertThat(exporter)
.isInstanceOfSatisfying(
@ -78,7 +80,8 @@ class SpanExporterConfigurationTest {
SpanExporterConfiguration.configureExporter(
"zipkin",
DefaultConfigProperties.createForTest(
Collections.singletonMap("otel.exporter.zipkin.timeout", "5s")));
Collections.singletonMap("otel.exporter.zipkin.timeout", "5s")),
Collections.emptyMap());
try {
assertThat(exporter).isNotNull();
} finally {

View File

@ -135,7 +135,8 @@ class OtlpGrpcConfigTest {
props.put("otel.exporter.otlp.headers", "header-key=header-value");
props.put("otel.exporter.otlp.timeout", "15s");
ConfigProperties properties = DefaultConfigProperties.createForTest(props);
SpanExporter spanExporter = SpanExporterConfiguration.configureExporter("otlp", properties);
SpanExporter spanExporter =
SpanExporterConfiguration.configureExporter("otlp", properties, Collections.emptyMap());
MetricExporter metricExporter =
MetricExporterConfiguration.configureOtlpMetrics(
properties, SdkMeterProvider.builder().build());
@ -187,7 +188,7 @@ class OtlpGrpcConfigTest {
props.put("otel.exporter.otlp.traces.timeout", "15s");
SpanExporter spanExporter =
SpanExporterConfiguration.configureExporter(
"otlp", DefaultConfigProperties.createForTest(props));
"otlp", DefaultConfigProperties.createForTest(props), Collections.emptyMap());
assertThat(spanExporter).extracting("timeoutNanos").isEqualTo(TimeUnit.SECONDS.toNanos(15));
assertThat(
@ -245,7 +246,10 @@ class OtlpGrpcConfigTest {
props.put("otel.exporter.otlp.certificate", Paths.get("foo", "bar", "baz").toString());
ConfigProperties properties = DefaultConfigProperties.createForTest(props);
assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("otlp", properties))
assertThatThrownBy(
() ->
SpanExporterConfiguration.configureExporter(
"otlp", properties, Collections.emptyMap()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Invalid OTLP certificate path:");

View File

@ -155,7 +155,8 @@ class OtlpHttpConfigTest {
props.put("otel.exporter.otlp.headers", "header-key=header-value");
props.put("otel.exporter.otlp.timeout", "15s");
ConfigProperties properties = DefaultConfigProperties.createForTest(props);
SpanExporter spanExporter = SpanExporterConfiguration.configureExporter("otlp", properties);
SpanExporter spanExporter =
SpanExporterConfiguration.configureExporter("otlp", properties, Collections.emptyMap());
MetricExporter metricExporter =
MetricExporterConfiguration.configureOtlpMetrics(
properties, SdkMeterProvider.builder().build());
@ -212,7 +213,7 @@ class OtlpHttpConfigTest {
props.put("otel.exporter.otlp.traces.timeout", "15s");
SpanExporter spanExporter =
SpanExporterConfiguration.configureExporter(
"otlp", DefaultConfigProperties.createForTest(props));
"otlp", DefaultConfigProperties.createForTest(props), Collections.emptyMap());
assertThat(spanExporter)
.extracting("client", as(InstanceOfAssertFactories.type(OkHttpClient.class)))
@ -276,7 +277,10 @@ class OtlpHttpConfigTest {
props.put("otel.exporter.otlp.certificate", Paths.get("foo", "bar", "baz").toString());
ConfigProperties properties = DefaultConfigProperties.createForTest(props);
assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("otlp", properties))
assertThatThrownBy(
() ->
SpanExporterConfiguration.configureExporter(
"otlp", properties, Collections.emptyMap()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Invalid OTLP certificate path:");