Add autoconfiguration wrapper artifact (#2401)

* Add autoconfiguration wrapper artifact

* WIP

* WIP

* WIP

* WIP

* WIP

* Mostly done

* Propagator classpath

* Finish

* Cleanup

* Cleanup

* Not visible

* Update sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ConfigProperties.java

Co-authored-by: John Watson <jkwatson@gmail.com>

* More merge

Co-authored-by: John Watson <jkwatson@gmail.com>
This commit is contained in:
Anuraag Agrawal 2021-01-07 12:02:09 +09:00 committed by GitHub
parent 0dac3f6157
commit e749f2b262
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1819 additions and 0 deletions

View File

@ -10,6 +10,10 @@ import io.opentelemetry.api.trace.TracerProvider;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.spi.OpenTelemetryFactory;
import io.opentelemetry.spi.trace.TracerProviderFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
@ -30,7 +34,11 @@ import javax.annotation.Nullable;
* @see ContextPropagators
*/
public final class GlobalOpenTelemetry {
private static final Logger logger = Logger.getLogger(GlobalOpenTelemetry.class.getName());
private static final Object mutex = new Object();
@Nullable private static volatile OpenTelemetry globalOpenTelemetry;
private GlobalOpenTelemetry() {}
@ -48,6 +56,13 @@ public final class GlobalOpenTelemetry {
if (globalOpenTelemetry == null) {
synchronized (mutex) {
if (globalOpenTelemetry == null) {
OpenTelemetry autoConfigured = maybeAutoConfigure();
if (autoConfigured != null) {
set(autoConfigured);
return autoConfigured;
}
OpenTelemetryFactory openTelemetryFactory = Utils.loadSpi(OpenTelemetryFactory.class);
if (openTelemetryFactory != null) {
set(openTelemetryFactory.create());
@ -115,4 +130,31 @@ public final class GlobalOpenTelemetry {
public static ContextPropagators getPropagators() {
return get().getPropagators();
}
@Nullable
private static OpenTelemetry maybeAutoConfigure() {
final Class<?> openTelemetrySdkAutoConfiguration;
try {
openTelemetrySdkAutoConfiguration =
Class.forName("io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkAutoConfiguration");
} catch (ClassNotFoundException e) {
return null;
}
try {
Method initialize = openTelemetrySdkAutoConfiguration.getMethod("initialize");
return (OpenTelemetry) initialize.invoke(null);
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new IllegalStateException(
"OpenTelemetrySdkAutoConfiguration detected on classpath "
+ "but could not invoke initialize method. This is a bug in OpenTelemetry.",
e);
} catch (InvocationTargetException t) {
logger.log(
Level.SEVERE,
"Error automatically configuring OpenTelemetry SDK. OpenTelemetry will not be enabled.",
t.getTargetException());
return null;
}
}
}

View File

@ -200,6 +200,7 @@ subprojects {
jsr305 : "com.google.code.findbugs:jsr305:${findBugsJsr305Version}",
prometheus_client : "io.prometheus:simpleclient:${prometheusVersion}",
prometheus_client_common : "io.prometheus:simpleclient_common:${prometheusVersion}",
prometheus_client_httpserver: "io.prometheus:simpleclient_httpserver:${prometheusVersion}",
protobuf : "com.google.protobuf:protobuf-java",
protobuf_util : "com.google.protobuf:protobuf-java-util",
zipkin_reporter : "io.zipkin.reporter2:zipkin-reporter",

View File

@ -0,0 +1,74 @@
plugins {
id "java-library"
id "maven-publish"
id "org.unbroken-dome.test-sets"
id "ru.vyarus.animalsniffer"
}
description = 'OpenTelemetry SDK Auto-configuration'
ext.moduleName = "io.opentelemetry.sdk.autoconfigure"
testSets {
testConfigError
testFullConfig
testPrometheus
}
dependencies {
api project(':sdk:all'),
project(':sdk:metrics')
compileOnly project(':extensions:trace-propagators')
compileOnly project(':exporters:jaeger')
compileOnly project(':exporters:logging')
compileOnly project(':exporters:otlp:all')
compileOnly project(':exporters:otlp:metrics')
compileOnly project(':exporters:prometheus')
compileOnly libraries.prometheus_client_httpserver
compileOnly project(':exporters:zipkin')
testImplementation project(':proto'),
project(':sdk:testing'),
'com.linecorp.armeria:armeria-junit5',
'com.linecorp.armeria:armeria-grpc'
testRuntimeOnly 'io.grpc:grpc-netty-shaded'
testFullConfigImplementation project(':extensions:trace-propagators')
testFullConfigImplementation project(':exporters:jaeger')
testFullConfigImplementation project(':exporters:logging')
testFullConfigImplementation project(':exporters:otlp:all')
testFullConfigImplementation project(':exporters:otlp:metrics')
testFullConfigImplementation project(':exporters:prometheus')
testFullConfigImplementation libraries.prometheus_client_httpserver
testFullConfigImplementation project(':exporters:zipkin')
testConfigErrorImplementation project(':extensions:trace-propagators')
testConfigErrorImplementation project(':exporters:jaeger')
testConfigErrorImplementation project(':exporters:logging')
testConfigErrorImplementation project(':exporters:otlp:all')
testConfigErrorImplementation project(':exporters:otlp:metrics')
testConfigErrorImplementation project(':exporters:prometheus')
testConfigErrorImplementation libraries.prometheus_client_httpserver
testConfigErrorImplementation project(':exporters:zipkin')
testConfigErrorImplementation libraries.junit_pioneer
testPrometheusImplementation project(':exporters:prometheus')
testPrometheusImplementation libraries.prometheus_client_httpserver
}
testFullConfig {
environment("OTEL_RESOURCE_ATTRIBUTES", "service.name=test,cat=meow")
environment("OTEL_EXPORTER", "otlp,jaeger,zipkin")
environment("OTEL_PROPAGATORS", "tracecontext,baggage,b3,b3multi,jaeger,ottracer,xray")
environment("OTEL_BSP_SCHEDULE_DELAY_MILLIS", "10")
environment("OTEL_IMR_EXPORT_INTERVAL", "10")
environment("OTEL_EXPORTER_OTLP_HEADERS", "cat=meow,dog=bark")
environment("OTEL_EXPORTER_OTLP_TIMEOUT", "5000")
environment("OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", "2")
}
testPrometheus {
environment("OTEL_EXPORTER", "prometheus")
environment("OTEL_IMR_EXPORT_INTERVAL", "10")
}

View File

@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
final class ClasspathUtil {
@SuppressWarnings("UnusedException")
static void checkClassExists(String className, String featureName, String requiredLibrary) {
try {
Class.forName(className);
} catch (ClassNotFoundException unused) {
throw new ConfigurationException(
featureName
+ " enabled but "
+ requiredLibrary
+ " not found on classpath. "
+ "Make sure to add it as a dependency to enable this feature.");
}
}
private ClasspathUtil() {}
}

View File

@ -0,0 +1,132 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
class ConfigProperties {
private final Map<String, String> config;
static ConfigProperties get() {
return new ConfigProperties(System.getProperties(), System.getenv());
}
// Visible for testing
static ConfigProperties createForTest(Map<String, String> properties) {
return new ConfigProperties(properties, Collections.emptyMap());
}
private ConfigProperties(Map<?, ?> systemProperties, Map<String, String> environmentVariables) {
Map<String, String> config = new HashMap<>();
environmentVariables.forEach(
(name, value) -> config.put(name.toLowerCase(Locale.ROOT).replace('_', '.'), value));
systemProperties.forEach(
(key, value) -> config.put(((String) key).toLowerCase(Locale.ROOT), (String) value));
this.config = config;
}
@Nullable
String getString(String name) {
return config.get(name);
}
@Nullable
@SuppressWarnings("UnusedException")
Integer getInt(String name) {
String value = config.get(name);
if (value == null || value.isEmpty()) {
return null;
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException ex) {
throw newInvalidPropertyException(name, value, "integer");
}
}
@Nullable
@SuppressWarnings("UnusedException")
Long getLong(String name) {
String value = config.get(name);
if (value == null || value.isEmpty()) {
return null;
}
try {
return Long.parseLong(value);
} catch (NumberFormatException ex) {
throw newInvalidPropertyException(name, value, "long");
}
}
@Nullable
@SuppressWarnings("UnusedException")
Double getDouble(String name) {
String value = config.get(name);
if (value == null || value.isEmpty()) {
return null;
}
try {
return Double.parseDouble(value);
} catch (NumberFormatException ex) {
throw newInvalidPropertyException(name, value, "double");
}
}
List<String> getCommaSeparatedValues(String name) {
String value = config.get(name);
if (value == null) {
return Collections.emptyList();
}
return filterBlanksAndNulls(value.split(","));
}
Map<String, String> getCommaSeparatedMap(String name) {
return getCommaSeparatedValues(name).stream()
.map(keyValuePair -> filterBlanksAndNulls(keyValuePair.split("=", 2)))
.map(
splitKeyValuePairs -> {
if (splitKeyValuePairs.size() != 2) {
throw new ConfigurationException(
"Invalid map property: " + name + "=" + config.get(name));
}
return new AbstractMap.SimpleImmutableEntry<>(
splitKeyValuePairs.get(0), splitKeyValuePairs.get(1));
})
// If duplicate keys, prioritize later ones similar to duplicate system properties on a
// Java command line.
.collect(
Collectors.toMap(
Map.Entry::getKey, Map.Entry::getValue, (first, next) -> next, LinkedHashMap::new));
}
boolean getBoolean(String name) {
return Boolean.parseBoolean(config.get(name));
}
private static ConfigurationException newInvalidPropertyException(
String name, String value, String type) {
throw new ConfigurationException(
"Invalid value for property " + name + "=" + value + ". Must be a " + type + ".");
}
private static List<String> filterBlanksAndNulls(String[] values) {
return Arrays.stream(values)
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,16 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
/** An exception that is thrown if the user-provided configuration is invalid. */
public final class ConfigurationException extends RuntimeException {
private static final long serialVersionUID = 4717640118051490483L;
ConfigurationException(String message) {
super(message);
}
}

View File

@ -0,0 +1,120 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter;
import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder;
import io.opentelemetry.exporter.prometheus.PrometheusCollector;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.export.IntervalMetricReader;
import io.prometheus.client.exporter.HTTPServer;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
final class MetricExporterConfiguration {
static final List<String> RECOGNIZED_NAMES = Arrays.asList("otlp", "otlp_metrics", "prometheus");
static boolean configureExporter(
String name,
ConfigProperties config,
boolean metricsAlreadyRegistered,
SdkMeterProvider meterProvider) {
switch (name) {
case "otlp":
case "otlp_metrics":
if (metricsAlreadyRegistered) {
throw new ConfigurationException(
"Multiple metrics exporters configured. Only one metrics exporter can be "
+ "configured at a time.");
}
configureOtlpMetrics(config, meterProvider);
return true;
case "prometheus":
if (metricsAlreadyRegistered) {
throw new ConfigurationException(
"Multiple metrics exporters configured. Only one metrics exporter can be "
+ "configured at a time.");
}
configurePrometheusMetrics(config, meterProvider);
return true;
default:
return false;
}
}
// Visible for testing
static OtlpGrpcMetricExporter configureOtlpMetrics(
ConfigProperties config, SdkMeterProvider meterProvider) {
ClasspathUtil.checkClassExists(
"io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter",
"OTLP Metrics Exporter",
"opentelemetry-exporter-otlp-metrics");
OtlpGrpcMetricExporterBuilder builder = OtlpGrpcMetricExporter.builder();
String endpoint = config.getString("otel.exporter.otlp.endpoint");
if (endpoint != null) {
builder.setEndpoint(endpoint);
}
boolean insecure = config.getBoolean("otel.exporter.otlp.insecure");
if (!insecure) {
builder.setUseTls(true);
}
config.getCommaSeparatedMap("otel.exporter.otlp.headers").forEach(builder::addHeader);
Long deadlineMs = config.getLong("otel.exporter.otlp.timeout");
if (deadlineMs != null) {
builder.setDeadlineMs(deadlineMs);
}
OtlpGrpcMetricExporter exporter = builder.build();
IntervalMetricReader.Builder readerBuilder =
IntervalMetricReader.builder()
.setMetricProducers(Collections.singleton(meterProvider.getMetricProducer()))
.setMetricExporter(exporter);
Long exportIntervalMillis = config.getLong("otel.imr.export.interval");
if (exportIntervalMillis != null) {
readerBuilder.setExportIntervalMillis(exportIntervalMillis);
}
IntervalMetricReader reader = readerBuilder.build();
Runtime.getRuntime().addShutdownHook(new Thread(reader::shutdown));
return exporter;
}
private static void configurePrometheusMetrics(
ConfigProperties config, SdkMeterProvider meterProvider) {
ClasspathUtil.checkClassExists(
"io.opentelemetry.exporter.prometheus.PrometheusCollector",
"Prometheus Metrics Server",
"opentelemetry-exporter-prometheus");
PrometheusCollector.builder()
.setMetricProducer(meterProvider.getMetricProducer())
.buildAndRegister();
Integer port = config.getInt("otel.exporter.prometheus.port");
if (port == null) {
port = 9464;
}
String host = config.getString("otel.exporter.prometheus.host");
if (host == null) {
host = "0.0.0.0";
}
final HTTPServer server;
try {
server = new HTTPServer(host, port, true);
} catch (IOException e) {
throw new IllegalStateException("Failed to create Prometheus server", e);
}
Runtime.getRuntime().addShutdownHook(new Thread(server::stop));
}
private MetricExporterConfiguration() {}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Auto-configuration for the OpenTelemetry SDK. As an alternative to programmatically configuring
* the SDK using {@link OpenTelemetrySdk#builder()}, this package can be used to automatically
* configure the SDK using environment properties specified by OpenTelemetry.
*/
public final class OpenTelemetrySdkAutoConfiguration {
/**
* Returns an {@link OpenTelemetrySdk} automatically initialized through recognized system
* properties and environment variables.
*/
public static OpenTelemetrySdk initialize() {
ConfigProperties config = ConfigProperties.get();
ContextPropagators propagators = PropagatorConfiguration.configurePropagators(config);
Resource resource = configureResource(config);
Set<String> exporterNames =
new LinkedHashSet<>(config.getCommaSeparatedValues("otel.exporter"));
Set<String> unrecognizedExporters = new LinkedHashSet<>(exporterNames);
unrecognizedExporters.removeAll(SpanExporterConfiguration.RECOGNIZED_NAMES);
unrecognizedExporters.removeAll(MetricExporterConfiguration.RECOGNIZED_NAMES);
if (!unrecognizedExporters.isEmpty()) {
throw new ConfigurationException(
"Unrecognized value for otel.exporter: " + String.join(",", exporterNames));
}
configureMeterProvider(resource, exporterNames, config);
SdkTracerProvider tracerProvider =
TracerProviderConfiguration.configureTracerProvider(resource, exporterNames, config);
return OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.setPropagators(propagators)
.build();
}
private static void configureMeterProvider(
Resource resource, Set<String> exporterNames, ConfigProperties config) {
SdkMeterProvider meterProvider =
SdkMeterProvider.builder().setResource(resource).buildAndRegisterGlobal();
boolean metricsConfigured = false;
for (String exporterName : new ArrayList<>(exporterNames)) {
metricsConfigured =
MetricExporterConfiguration.configureExporter(
exporterName, config, metricsConfigured, meterProvider);
}
}
// Visible for testing
static Resource configureResource(ConfigProperties config) {
AttributesBuilder resourceAttributes = Attributes.builder();
config.getCommaSeparatedMap("otel.resource.attributes").forEach(resourceAttributes::put);
return Resource.create(resourceAttributes.build()).merge(Resource.getDefault());
}
private OpenTelemetrySdkAutoConfiguration() {}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.extension.trace.propagation.AwsXrayPropagator;
import io.opentelemetry.extension.trace.propagation.B3Propagator;
import io.opentelemetry.extension.trace.propagation.JaegerPropagator;
import io.opentelemetry.extension.trace.propagation.OtTracerPropagator;
import java.util.ArrayList;
import java.util.List;
final class PropagatorConfiguration {
static ContextPropagators configurePropagators(ConfigProperties config) {
List<TextMapPropagator> propagators = new ArrayList<>();
for (String propagatorName : config.getCommaSeparatedValues("otel.propagators")) {
propagators.add(PropagatorConfiguration.getPropagator(propagatorName));
}
return ContextPropagators.create(TextMapPropagator.composite(propagators));
}
private static TextMapPropagator getPropagator(String name) {
if (name.equals("tracecontext")) {
return W3CTraceContextPropagator.getInstance();
}
if (name.equals("baggage")) {
return W3CBaggagePropagator.getInstance();
}
// Other propagators are in the extension artifact. Check one of the propagators.
ClasspathUtil.checkClassExists(
"io.opentelemetry.extension.trace.propagation.B3Propagator",
name + " propagator",
"opentelemetry-extension-trace-propagators");
switch (name) {
case "b3":
return B3Propagator.getInstance();
case "b3multi":
return B3Propagator.builder().injectMultipleHeaders().build();
case "jaeger":
return JaegerPropagator.getInstance();
case "ottracer":
return OtTracerPropagator.getInstance();
case "xray":
return AwsXrayPropagator.getInstance();
default:
throw new ConfigurationException("Unrecognized value for otel.propagators: " + name);
}
}
private PropagatorConfiguration() {}
}

View File

@ -0,0 +1,105 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter;
import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporterBuilder;
import io.opentelemetry.exporter.logging.LoggingSpanExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder;
import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter;
import io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
final class SpanExporterConfiguration {
static final List<String> RECOGNIZED_NAMES =
Arrays.asList("otlp", "otlp_span", "jaeger", "zipkin", "logging");
@Nullable
static SpanExporter configureExporter(String name, ConfigProperties config) {
switch (name) {
case "otlp":
case "otlp_span":
return configureOtlpSpans(config);
case "jaeger":
return configureJaeger(config);
case "zipkin":
return configureZipkin(config);
case "logging":
ClasspathUtil.checkClassExists(
"io.opentelemetry.exporter.logging.LoggingSpanExporter",
"Logging Trace Exporter",
"opentelemetry-exporter-logging");
return new LoggingSpanExporter();
default:
return null;
}
}
// Visible for testing
static OtlpGrpcSpanExporter configureOtlpSpans(ConfigProperties config) {
ClasspathUtil.checkClassExists(
"io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter",
"OTLP Trace Exporter",
"opentelemetry-exporter-otlp-trace");
OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder();
String endpoint = config.getString("otel.exporter.otlp.endpoint");
if (endpoint != null) {
builder.setEndpoint(endpoint);
}
boolean insecure = config.getBoolean("otel.exporter.otlp.insecure");
if (!insecure) {
builder.setUseTls(true);
}
config.getCommaSeparatedMap("otel.exporter.otlp.headers").forEach(builder::addHeader);
Long deadlineMs = config.getLong("otel.exporter.otlp.timeout");
if (deadlineMs != null) {
builder.setDeadlineMs(deadlineMs);
}
return builder.build();
}
private static SpanExporter configureJaeger(ConfigProperties config) {
ClasspathUtil.checkClassExists(
"io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter",
"Jaeger gRPC Exporter",
"opentelemetry-exporter-jaeger");
JaegerGrpcSpanExporterBuilder builder = JaegerGrpcSpanExporter.builder();
String endpoint = config.getString("otel.exporter.jaeger.endpoint");
if (endpoint != null) {
builder.setEndpoint(endpoint);
}
return builder.build();
}
private static SpanExporter configureZipkin(ConfigProperties config) {
ClasspathUtil.checkClassExists(
"io.opentelemetry.exporter.zipkin.ZipkinSpanExporter",
"Zipkin Exporter",
"opentelemetry-exporter-zipkin");
ZipkinSpanExporterBuilder builder = ZipkinSpanExporter.builder();
String endpoint = config.getString("otel.exporter.zipkin.endpoint");
if (endpoint != null) {
builder.setEndpoint(endpoint);
}
return builder.build();
}
private SpanExporterConfiguration() {}
}

View File

@ -0,0 +1,150 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder;
import io.opentelemetry.sdk.trace.config.TraceConfig;
import io.opentelemetry.sdk.trace.config.TraceConfigBuilder;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
final class TracerProviderConfiguration {
static SdkTracerProvider configureTracerProvider(
Resource resource, Set<String> exporterNames, ConfigProperties config) {
SdkTracerProviderBuilder tracerProviderBuilder =
SdkTracerProvider.builder()
.setResource(resource)
.setTraceConfig(configureTraceConfig(config));
// Run user configuration before setting exporters from environment to allow user span
// processors to effect export.
for (SdkTracerProviderConfigurer configurer :
ServiceLoader.load(SdkTracerProviderConfigurer.class)) {
configurer.configure(tracerProviderBuilder);
}
List<SpanExporter> spanExporters = new ArrayList<>();
for (String name : new ArrayList<>(exporterNames)) {
SpanExporter exporter = SpanExporterConfiguration.configureExporter(name, config);
if (exporter != null) {
spanExporters.add(exporter);
}
}
if (!spanExporters.isEmpty()) {
tracerProviderBuilder.addSpanProcessor(configureSpanProcessor(config, spanExporters));
}
SdkTracerProvider tracerProvider = tracerProviderBuilder.build();
Runtime.getRuntime().addShutdownHook(new Thread(tracerProvider::shutdown));
return tracerProvider;
}
// VisibleForTesting
static BatchSpanProcessor configureSpanProcessor(
ConfigProperties config, List<SpanExporter> exporters) {
SpanExporter exporter = SpanExporter.composite(exporters);
BatchSpanProcessorBuilder builder = BatchSpanProcessor.builder(exporter);
Long scheduleDelayMillis = config.getLong("otel.bsp.schedule.delay.millis");
if (scheduleDelayMillis != null) {
builder.setScheduleDelayMillis(scheduleDelayMillis);
}
Integer maxQueue = config.getInt("otel.bsp.max.queue.size");
if (maxQueue != null) {
builder.setMaxQueueSize(maxQueue);
}
Integer maxExportBatch = config.getInt("otel.bsp.max.export.batch.size");
if (maxExportBatch != null) {
builder.setMaxExportBatchSize(maxExportBatch);
}
Integer timeout = config.getInt("otel.bsp.export.timeout.millis");
if (timeout != null) {
builder.setExporterTimeoutMillis(timeout);
}
return builder.build();
}
// Visible for testing
static TraceConfig configureTraceConfig(ConfigProperties config) {
TraceConfigBuilder builder = TraceConfig.getDefault().toBuilder();
String sampler = config.getString("otel.trace.sampler");
if (sampler != null) {
builder.setSampler(configureSampler(sampler, config));
}
Integer maxAttrs = config.getInt("otel.span.attribute.count.limit");
if (maxAttrs != null) {
builder.setMaxNumberOfAttributes(maxAttrs);
}
Integer maxEvents = config.getInt("otel.span.event.count.limit");
if (maxEvents != null) {
builder.setMaxNumberOfEvents(maxEvents);
}
Integer maxLinks = config.getInt("otel.span.link.count.limit");
if (maxLinks != null) {
builder.setMaxNumberOfLinks(maxLinks);
}
return builder.build();
}
// Visible for testing
static Sampler configureSampler(String sampler, ConfigProperties config) {
switch (sampler) {
case "always_on":
return Sampler.alwaysOn();
case "always_off":
return Sampler.alwaysOff();
case "traceidratio":
{
Double ratio = config.getDouble("otel.trace.sampler.arg");
if (ratio == null) {
throw new ConfigurationException(
"otel.trace.sampler=traceidratio but otel.trace.sampler.arg is not provided. "
+ "Set otel.trace.sampler.arg to a value in the range [0.0, 1.0].");
}
return Sampler.traceIdRatioBased(ratio);
}
case "parentbased_always_on":
return Sampler.parentBased(Sampler.alwaysOn());
case "parentbased_always_off":
return Sampler.parentBased(Sampler.alwaysOff());
case "parentbased_traceidratio":
{
Double ratio = config.getDouble("otel.trace.sampler.arg");
if (ratio == null) {
throw new ConfigurationException(
"otel.trace.sampler=parentbased_traceidratio but otel.trace.sampler.arg is not "
+ "provided. Set otel.trace.sampler.arg to a value in the "
+ "range [0.0, 1.0].");
}
return Sampler.parentBased(Sampler.traceIdRatioBased(ratio));
}
default:
throw new ConfigurationException("Unrecognized value for otel.trace.sampler: " + sampler);
}
}
private TracerProviderConfiguration() {}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure.spi;
import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder;
/**
* A service provider interface (SPI) for performing additional programmatic configuration of a
* {@link SdkTracerProviderBuilder} during initialization. When using auto-configuration, you should
* prefer to use system properties or environment variables for configuration, but this may be
* useful to register components that are not part of the SDK such as custom exporters.
*/
public interface SdkTracerProviderConfigurer {
/** Configures the {@link SdkTracerProviderBuilder}. */
void configure(SdkTracerProviderBuilder tracerProvider);
}

View File

@ -0,0 +1,157 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.entry;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
class ConfigPropertiesTest {
@Test
void allValid() {
Map<String, String> properties = new HashMap<>();
properties.put("string", "str");
properties.put("int", "10");
properties.put("long", "20");
properties.put("double", "5.4");
properties.put("list", "cat,dog,bear");
properties.put("map", "cat=meow,dog=bark,bear=growl");
ConfigProperties config = ConfigProperties.createForTest(properties);
assertThat(config.getString("string")).isEqualTo("str");
assertThat(config.getInt("int")).isEqualTo(10);
assertThat(config.getLong("long")).isEqualTo(20);
assertThat(config.getDouble("double")).isEqualTo(5.4);
assertThat(config.getCommaSeparatedValues("list")).containsExactly("cat", "dog", "bear");
assertThat(config.getCommaSeparatedMap("map"))
.containsExactly(entry("cat", "meow"), entry("dog", "bark"), entry("bear", "growl"));
}
@Test
void allMissing() {
ConfigProperties config = ConfigProperties.createForTest(Collections.emptyMap());
assertThat(config.getString("string")).isNull();
assertThat(config.getInt("int")).isNull();
assertThat(config.getLong("long")).isNull();
assertThat(config.getDouble("double")).isNull();
assertThat(config.getCommaSeparatedValues("list")).isEmpty();
assertThat(config.getCommaSeparatedMap("map")).isEmpty();
}
@Test
void allEmpty() {
Map<String, String> properties = new HashMap<>();
properties.put("string", "");
properties.put("int", "");
properties.put("long", "");
properties.put("double", "");
properties.put("list", "");
properties.put("map", "");
ConfigProperties config = ConfigProperties.createForTest(properties);
assertThat(config.getString("string")).isEmpty();
assertThat(config.getInt("int")).isNull();
assertThat(config.getLong("long")).isNull();
assertThat(config.getDouble("double")).isNull();
assertThat(config.getCommaSeparatedValues("list")).isEmpty();
assertThat(config.getCommaSeparatedMap("map")).isEmpty();
}
@Test
void invalidInt() {
assertThatThrownBy(
() ->
ConfigProperties.createForTest(Collections.singletonMap("int", "bar"))
.getInt("int"))
.isInstanceOf(ConfigurationException.class)
.hasMessage("Invalid value for property int=bar. Must be a integer.");
assertThatThrownBy(
() ->
ConfigProperties.createForTest(Collections.singletonMap("int", "999999999999999"))
.getInt("int"))
.isInstanceOf(ConfigurationException.class)
.hasMessage("Invalid value for property int=999999999999999. Must be a integer.");
}
@Test
void invalidLong() {
assertThatThrownBy(
() ->
ConfigProperties.createForTest(Collections.singletonMap("long", "bar"))
.getLong("long"))
.isInstanceOf(ConfigurationException.class)
.hasMessage("Invalid value for property long=bar. Must be a long.");
assertThatThrownBy(
() ->
ConfigProperties.createForTest(
Collections.singletonMap("long", "99223372036854775807"))
.getLong("long"))
.isInstanceOf(ConfigurationException.class)
.hasMessage("Invalid value for property long=99223372036854775807. Must be a long.");
}
@Test
void invalidDouble() {
assertThatThrownBy(
() ->
ConfigProperties.createForTest(Collections.singletonMap("double", "bar"))
.getDouble("double"))
.isInstanceOf(ConfigurationException.class)
.hasMessage("Invalid value for property double=bar. Must be a double.");
assertThatThrownBy(
() ->
ConfigProperties.createForTest(Collections.singletonMap("double", "1.0.1"))
.getDouble("double"))
.isInstanceOf(ConfigurationException.class)
.hasMessage("Invalid value for property double=1.0.1. Must be a double.");
}
@Test
void uncleanList() {
assertThat(
ConfigProperties.createForTest(
Collections.singletonMap("list", " a ,b,c , d,, ,"))
.getCommaSeparatedValues("list"))
.containsExactly("a", "b", "c", "d");
}
@Test
void uncleanMap() {
assertThat(
ConfigProperties.createForTest(
Collections.singletonMap("map", " a=1 ,b=2,c = 3 , d= 4,, ,"))
.getCommaSeparatedMap("map"))
.containsExactly(entry("a", "1"), entry("b", "2"), entry("c", "3"), entry("d", "4"));
}
@Test
void invalidMap() {
assertThatThrownBy(
() ->
ConfigProperties.createForTest(Collections.singletonMap("map", "a=1,b="))
.getCommaSeparatedMap("map"))
.isInstanceOf(ConfigurationException.class)
.hasMessage("Invalid map property: map=a=1,b=");
assertThatThrownBy(
() ->
ConfigProperties.createForTest(Collections.singletonMap("map", "a=1,b"))
.getCommaSeparatedMap("map"))
.isInstanceOf(ConfigurationException.class)
.hasMessage("Invalid map property: map=a=1,b");
assertThatThrownBy(
() ->
ConfigProperties.createForTest(Collections.singletonMap("map", "a=1,=b"))
.getCommaSeparatedMap("map"))
.isInstanceOf(ConfigurationException.class)
.hasMessage("Invalid map property: map=a=1,=b");
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import java.util.Collections;
import org.junit.jupiter.api.Test;
class NotOnClasspathTest {
private static final ConfigProperties EMPTY =
ConfigProperties.createForTest(Collections.emptyMap());
@Test
void otlpSpans() {
assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("otlp", EMPTY))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(
"OTLP Trace Exporter enabled but opentelemetry-exporter-otlp-trace not found on "
+ "classpath");
}
@Test
void jaeger() {
assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("jaeger", EMPTY))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(
"Jaeger gRPC Exporter enabled but opentelemetry-exporter-jaeger not found on "
+ "classpath");
}
@Test
void zipkin() {
assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("zipkin", EMPTY))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(
"Zipkin Exporter enabled but opentelemetry-exporter-zipkin not found on classpath");
}
@Test
void logging() {
assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("logging", EMPTY))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(
"Logging Trace Exporter enabled but opentelemetry-exporter-logging not found on "
+ "classpath");
}
@Test
void otlpMetrics() {
assertThatThrownBy(
() ->
MetricExporterConfiguration.configureExporter(
"otlp", EMPTY, false, SdkMeterProvider.builder().build()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(
"OTLP Metrics Exporter enabled but opentelemetry-exporter-otlp-metrics not found on "
+ "classpath");
}
@Test
void prometheus() {
assertThatThrownBy(
() ->
MetricExporterConfiguration.configureExporter(
"prometheus", EMPTY, false, SdkMeterProvider.builder().build()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(
"Prometheus Metrics Server enabled but opentelemetry-exporter-prometheus not found on "
+ "classpath");
}
@Test
void b3propagator() {
assertThatThrownBy(
() ->
PropagatorConfiguration.configurePropagators(
ConfigProperties.createForTest(
Collections.singletonMap("otel.propagators", "b3"))))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(
"b3 propagator enabled but opentelemetry-extension-trace-propagators not found on "
+ "classpath");
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.util.Collections;
import org.junit.jupiter.api.Test;
class OpenTelemetrySdkAutoConfigurationTest {
@Test
void resourcePrioritizesUser() {
Resource resource =
OpenTelemetrySdkAutoConfiguration.configureResource(
ConfigProperties.createForTest(
Collections.singletonMap("otel.resource.attributes", "telemetry.sdk.name=test")));
assertThat(resource.getAttributes().get(SemanticAttributes.TELEMETRY_SDK_NAME))
.isEqualTo("test");
}
}

View File

@ -0,0 +1,188 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.config.TraceConfig;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
// NB: We use AssertJ extracting to reflectively access implementation details to test configuration
// because the use of BatchSpanProcessor makes it difficult to verify values through public means.
@ExtendWith(MockitoExtension.class)
class TracerProviderConfigurationTest {
private static final ConfigProperties EMPTY =
ConfigProperties.createForTest(Collections.emptyMap());
@Mock private SpanExporter exporter;
@Test
void configureTracerProvider() {
Map<String, String> properties = new HashMap<>();
properties.put("otel.bsp.schedule.delay.millis", "100000");
properties.put("otel.trace.sampler", "always_off");
Resource resource = Resource.create(Attributes.builder().put("cat", "meow").build());
// We don't have any exporters on classpath for this test so check no-op case. Exporter cases
// are verified in other test sets like testFullConfig.
SdkTracerProvider tracerProvider =
TracerProviderConfiguration.configureTracerProvider(
resource, Collections.emptySet(), ConfigProperties.createForTest(properties));
try {
assertThat(tracerProvider.getActiveTraceConfig().getSampler()).isEqualTo(Sampler.alwaysOff());
assertThat(tracerProvider)
.extracting("sharedState")
.satisfies(
sharedState -> {
assertThat(sharedState).extracting("resource").isEqualTo(resource);
assertThat(sharedState)
.extracting("activeSpanProcessor")
.isEqualTo(SpanProcessor.composite());
});
} finally {
tracerProvider.shutdown();
}
}
@Test
void configureSpanProcessor_empty() {
BatchSpanProcessor processor =
TracerProviderConfiguration.configureSpanProcessor(
EMPTY, Collections.singletonList(exporter));
try {
assertThat(processor)
.extracting("worker")
.satisfies(
worker -> {
assertThat(worker)
.extracting("scheduleDelayNanos")
.isEqualTo(TimeUnit.MILLISECONDS.toNanos(5000));
assertThat(worker).extracting("exporterTimeoutMillis").isEqualTo(30000);
assertThat(worker).extracting("maxExportBatchSize").isEqualTo(512);
assertThat(worker)
.extracting("queue")
.isInstanceOfSatisfying(
ArrayBlockingQueue.class,
queue -> assertThat(queue.remainingCapacity()).isEqualTo(2048));
assertThat(worker).extracting("spanExporter").isEqualTo(exporter);
});
} finally {
processor.shutdown();
}
}
@Test
void configureSpanProcessor_configured() {
Map<String, String> properties = new HashMap<>();
properties.put("otel.bsp.schedule.delay.millis", "100000");
properties.put("otel.bsp.max.queue.size", "2");
properties.put("otel.bsp.max.export.batch.size", "3");
properties.put("otel.bsp.export.timeout.millis", "4");
BatchSpanProcessor processor =
TracerProviderConfiguration.configureSpanProcessor(
ConfigProperties.createForTest(properties), Collections.singletonList(exporter));
try {
assertThat(processor)
.extracting("worker")
.satisfies(
worker -> {
assertThat(worker)
.extracting("scheduleDelayNanos")
.isEqualTo(TimeUnit.MILLISECONDS.toNanos(100000));
assertThat(worker).extracting("exporterTimeoutMillis").isEqualTo(4);
assertThat(worker).extracting("maxExportBatchSize").isEqualTo(3);
assertThat(worker)
.extracting("queue")
.isInstanceOfSatisfying(
ArrayBlockingQueue.class,
queue -> assertThat(queue.remainingCapacity()).isEqualTo(2));
assertThat(worker).extracting("spanExporter").isEqualTo(exporter);
});
} finally {
processor.shutdown();
}
}
@Test
void configureTraceConfig_empty() {
assertThat(TracerProviderConfiguration.configureTraceConfig(EMPTY))
.isEqualTo(TraceConfig.getDefault());
}
@Test
void configureTraceConfig_full() {
Map<String, String> properties = new HashMap<>();
properties.put("otel.trace.sampler", "always_off");
properties.put("otel.span.attribute.count.limit", "5");
properties.put("otel.span.event.count.limit", "4");
properties.put("otel.span.link.count.limit", "3");
TraceConfig config =
TracerProviderConfiguration.configureTraceConfig(
ConfigProperties.createForTest(properties));
assertThat(config.getSampler()).isEqualTo(Sampler.alwaysOff());
assertThat(config.getMaxNumberOfAttributes()).isEqualTo(5);
assertThat(config.getMaxNumberOfEvents()).isEqualTo(4);
assertThat(config.getMaxNumberOfLinks()).isEqualTo(3);
}
@Test
void configureSampler() {
assertThat(TracerProviderConfiguration.configureSampler("always_on", EMPTY))
.isEqualTo(Sampler.alwaysOn());
assertThat(TracerProviderConfiguration.configureSampler("always_off", EMPTY))
.isEqualTo(Sampler.alwaysOff());
assertThat(
TracerProviderConfiguration.configureSampler(
"traceidratio",
ConfigProperties.createForTest(
Collections.singletonMap("otel.trace.sampler.arg", "0.5"))))
.isEqualTo(Sampler.traceIdRatioBased(0.5));
assertThatThrownBy(() -> TracerProviderConfiguration.configureSampler("traceidratio", EMPTY))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("otel.trace.sampler.arg is not provided");
assertThat(TracerProviderConfiguration.configureSampler("parentbased_always_on", EMPTY))
.isEqualTo(Sampler.parentBased(Sampler.alwaysOn()));
assertThat(TracerProviderConfiguration.configureSampler("parentbased_always_off", EMPTY))
.isEqualTo(Sampler.parentBased(Sampler.alwaysOff()));
assertThat(
TracerProviderConfiguration.configureSampler(
"parentbased_traceidratio",
ConfigProperties.createForTest(
Collections.singletonMap("otel.trace.sampler.arg", "0.4"))))
.isEqualTo(Sampler.parentBased(Sampler.traceIdRatioBased(0.4)));
assertThatThrownBy(
() -> TracerProviderConfiguration.configureSampler("parentbased_traceidratio", EMPTY))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("otel.trace.sampler.arg is not provided");
assertThatThrownBy(() -> TracerProviderConfiguration.configureSampler("catsampler", EMPTY))
.isInstanceOf(ConfigurationException.class)
.hasMessage("Unrecognized value for otel.trace.sampler: catsampler");
}
}

View File

@ -0,0 +1,128 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import io.github.netmikey.logunit.api.LogCapturer;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junitpioneer.jupiter.SetSystemProperty;
import org.slf4j.event.Level;
import org.slf4j.event.LoggingEvent;
// All tests fail due to config errors so never register a global. We can test everything here
// without separating test sets.
class ConfigErrorTest {
@RegisterExtension
LogCapturer logs = LogCapturer.create().captureForType(GlobalOpenTelemetry.class);
@Test
@SetSystemProperty(key = "otel.exporter", value = "otlp_metrics,prometheus")
void multipleMetricExportersPrometheusThrows() {
assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize)
.isInstanceOf(ConfigurationException.class)
.hasMessage(
"Multiple metrics exporters configured. "
+ "Only one metrics exporter can be configured at a time.");
}
@Test
@SetSystemProperty(key = "otel.exporter", value = "prometheus,otlp_metrics")
void multipleMetricExportersOtlpThrows() {
assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize)
.isInstanceOf(ConfigurationException.class)
.hasMessage(
"Multiple metrics exporters configured. "
+ "Only one metrics exporter can be configured at a time.");
}
@Test
@SetSystemProperty(key = "otel.propagators", value = "cat")
void invalidPropagator() {
assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize)
.isInstanceOf(ConfigurationException.class)
.hasMessage("Unrecognized value for otel.propagators: cat");
}
@Test
@SetSystemProperty(key = "otel.trace.sampler", value = "traceidratio")
void missingTraceIdRatio() {
assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize)
.isInstanceOf(ConfigurationException.class)
.hasMessage(
"otel.trace.sampler=traceidratio but otel.trace.sampler.arg is not provided. "
+ "Set otel.trace.sampler.arg to a value in the range [0.0, 1.0].");
}
@Test
@SetSystemProperty(key = "otel.trace.sampler", value = "traceidratio")
@SetSystemProperty(key = "otel.trace.sampler.arg", value = "bar")
void invalidTraceIdRatio() {
assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize)
.isInstanceOf(ConfigurationException.class)
.hasMessage("Invalid value for property otel.trace.sampler.arg=bar. Must be a double.");
}
@Test
@SetSystemProperty(key = "otel.trace.sampler", value = "parentbased_traceidratio")
void missingTraceIdRatioWithParent() {
assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize)
.isInstanceOf(ConfigurationException.class)
.hasMessage(
"otel.trace.sampler=parentbased_traceidratio but otel.trace.sampler.arg is "
+ "not provided. Set otel.trace.sampler.arg to a value in the range [0.0, 1.0].");
}
@Test
@SetSystemProperty(key = "otel.trace.sampler", value = "parentbased_traceidratio")
@SetSystemProperty(key = "otel.trace.sampler.arg", value = "bar")
void invalidTraceIdRatioWithParent() {
assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize)
.isInstanceOf(ConfigurationException.class)
.hasMessage("Invalid value for property otel.trace.sampler.arg=bar. Must be a double.");
}
@Test
@SetSystemProperty(key = "otel.trace.sampler", value = "cat")
void invalidSampler() {
assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize)
.isInstanceOf(ConfigurationException.class)
.hasMessage("Unrecognized value for otel.trace.sampler: cat");
}
@Test
@SetSystemProperty(key = "otel.exporter", value = "otlp,cat,dog")
void invalidExporter() {
assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize)
.isInstanceOf(ConfigurationException.class)
.hasMessage("Unrecognized value for otel.exporter: cat,dog");
}
@Test
@SetSystemProperty(key = "otel.exporter", value = "bar")
void globalOpenTelemetryWhenError() {
assertThat(GlobalOpenTelemetry.get())
.isInstanceOf(OpenTelemetrySdk.class)
.extracting("propagators")
// Failed to initialize so is no-op
.isEqualTo(ContextPropagators.noop());
LoggingEvent log =
logs.assertContains(
"Error automatically configuring OpenTelemetry SDK. "
+ "OpenTelemetry will not be enabled.");
assertThat(log.getLevel()).isEqualTo(Level.ERROR);
assertThat(log.getThrowable())
.isInstanceOf(ConfigurationException.class)
.hasMessage("Unrecognized value for otel.exporter: bar");
}
}

View File

@ -0,0 +1,238 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.RequestHeaders;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.ServiceRequestContext;
import com.linecorp.armeria.server.grpc.GrpcService;
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
import io.grpc.stub.StreamObserver;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.baggage.Baggage;
import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.api.trace.SpanId;
import io.opentelemetry.api.trace.TraceFlags;
import io.opentelemetry.api.trace.TraceId;
import io.opentelemetry.api.trace.TraceState;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.Context;
import io.opentelemetry.exporter.jaeger.proto.api_v2.Collector;
import io.opentelemetry.exporter.jaeger.proto.api_v2.CollectorServiceGrpc;
import io.opentelemetry.extension.trace.propagation.AwsXrayPropagator;
import io.opentelemetry.extension.trace.propagation.B3Propagator;
import io.opentelemetry.extension.trace.propagation.JaegerPropagator;
import io.opentelemetry.extension.trace.propagation.OtTracerPropagator;
import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest;
import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse;
import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc;
import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse;
import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc;
import io.opentelemetry.proto.common.v1.AnyValue;
import io.opentelemetry.proto.common.v1.KeyValue;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@SuppressWarnings("InterruptedExceptionSwallowed")
class FullConfigTest {
private static final BlockingQueue<Collector.PostSpansRequest> jaegerRequests =
new LinkedBlockingDeque<>();
private static final BlockingQueue<ExportTraceServiceRequest> otlpTraceRequests =
new LinkedBlockingDeque<>();
private static final BlockingQueue<ExportMetricsServiceRequest> otlpMetricsRequests =
new LinkedBlockingDeque<>();
private static final BlockingQueue<String> zipkinJsonRequests = new LinkedBlockingDeque<>();
@RegisterExtension
public static final ServerExtension server =
new ServerExtension() {
@Override
protected void configure(ServerBuilder sb) {
sb.service(
GrpcService.builder()
// OTLP spans
.addService(
new TraceServiceGrpc.TraceServiceImplBase() {
@Override
public void export(
ExportTraceServiceRequest request,
StreamObserver<ExportTraceServiceResponse> responseObserver) {
try {
RequestHeaders headers =
ServiceRequestContext.current().request().headers();
assertThat(headers.get("cat")).isEqualTo("meow");
assertThat(headers.get("dog")).isEqualTo("bark");
} catch (Throwable t) {
responseObserver.onError(t);
return;
}
otlpTraceRequests.add(request);
responseObserver.onNext(ExportTraceServiceResponse.getDefaultInstance());
responseObserver.onCompleted();
}
})
// OTLP metrics
.addService(
new MetricsServiceGrpc.MetricsServiceImplBase() {
@Override
public void export(
ExportMetricsServiceRequest request,
StreamObserver<ExportMetricsServiceResponse> responseObserver) {
try {
RequestHeaders headers =
ServiceRequestContext.current().request().headers();
assertThat(headers.get("cat")).isEqualTo("meow");
assertThat(headers.get("dog")).isEqualTo("bark");
} catch (Throwable t) {
responseObserver.onError(t);
return;
}
if (request.getResourceMetricsCount() > 0) {
otlpMetricsRequests.add(request);
}
responseObserver.onNext(
ExportMetricsServiceResponse.getDefaultInstance());
responseObserver.onCompleted();
}
})
// Jaeger
.addService(
new CollectorServiceGrpc.CollectorServiceImplBase() {
@Override
public void postSpans(
Collector.PostSpansRequest request,
StreamObserver<Collector.PostSpansResponse> responseObserver) {
jaegerRequests.add(request);
responseObserver.onNext(Collector.PostSpansResponse.getDefaultInstance());
responseObserver.onCompleted();
}
})
.useBlockingTaskExecutor(true)
.build());
// Zipkin
sb.service(
"/api/v2/spans",
(ctx, req) ->
HttpResponse.from(
req.aggregate()
.thenApply(
aggRes -> {
zipkinJsonRequests.add(aggRes.contentUtf8());
return HttpResponse.of(HttpStatus.OK);
})));
}
};
@BeforeEach
void setUp() {
otlpTraceRequests.clear();
otlpMetricsRequests.clear();
jaegerRequests.clear();
zipkinJsonRequests.clear();
}
@Test
void configures() throws Exception {
String endpoint = "localhost:" + server.httpPort();
System.setProperty("otel.exporter.otlp.endpoint", endpoint);
System.setProperty("otel.exporter.otlp.insecure", "true");
System.setProperty("otel.exporter.otlp.timeout", "10000");
System.setProperty("otel.exporter.jaeger.endpoint", endpoint);
System.setProperty("otel.exporter.zipkin.endpoint", "http://" + endpoint + "/api/v2/spans");
OpenTelemetrySdkAutoConfiguration.initialize();
Map<String, String> headers = new HashMap<>();
GlobalOpenTelemetry.get()
.getPropagators()
.getTextMapPropagator()
.inject(
Context.root()
.with(
Span.wrap(
SpanContext.create(
TraceId.fromLongs(1, 1),
SpanId.fromLong(2),
TraceFlags.getDefault(),
TraceState.builder().set("cat", "meow").build())))
.with(Baggage.builder().put("airplane", "luggage").build()),
headers,
Map::put);
List<String> keys = new ArrayList<>();
keys.addAll(W3CTraceContextPropagator.getInstance().fields());
keys.addAll(W3CBaggagePropagator.getInstance().fields());
keys.addAll(B3Propagator.getInstance().fields());
keys.addAll(JaegerPropagator.getInstance().fields());
// Legacy baggage format.
keys.add("uberctx-airplane");
keys.addAll(OtTracerPropagator.getInstance().fields());
keys.addAll(AwsXrayPropagator.getInstance().fields());
assertThat(headers).containsOnlyKeys(keys);
GlobalOpenTelemetry.get()
.getTracer("test")
.spanBuilder("test")
.startSpan()
.setAttribute("cat", "meow")
.setAttribute("dog", "bark")
.end();
await()
.untilAsserted(
() -> {
assertThat(jaegerRequests).hasSize(1);
assertThat(otlpTraceRequests).hasSize(1);
assertThat(zipkinJsonRequests).hasSize(1);
// Not well defined how many metric exports would have happened by now, check that
// any
// did. The metrics will be BatchSpanProcessor metrics.
assertThat(otlpMetricsRequests).isNotEmpty();
});
ExportTraceServiceRequest traceRequest = otlpTraceRequests.take();
assertThat(traceRequest.getResourceSpans(0).getResource().getAttributesList())
.contains(
KeyValue.newBuilder()
.setKey("service.name")
.setValue(AnyValue.newBuilder().setStringValue("test").build())
.build(),
KeyValue.newBuilder()
.setKey("cat")
.setValue(AnyValue.newBuilder().setStringValue("meow").build())
.build());
io.opentelemetry.proto.trace.v1.Span span =
traceRequest.getResourceSpans(0).getInstrumentationLibrarySpans(0).getSpans(0);
// Dog dropped by attribute limit.
assertThat(span.getAttributesList())
.containsExactlyInAnyOrder(
KeyValue.newBuilder()
.setKey("configured")
.setValue(AnyValue.newBuilder().setBoolValue(true).build())
.build(),
KeyValue.newBuilder()
.setKey("cat")
.setValue(AnyValue.newBuilder().setStringValue("meow").build())
.build());
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import java.util.Collections;
import org.junit.jupiter.api.Test;
class MetricExporterConfigurationTest {
// Timeout difficult to test using real exports so just check implementation detail here.
@Test
void configureOtlpTimeout() {
OtlpGrpcMetricExporter exporter =
MetricExporterConfiguration.configureOtlpMetrics(
ConfigProperties.createForTest(
Collections.singletonMap("otel.exporter.otlp.timeout", "10")),
SdkMeterProvider.builder().build());
try {
assertThat(exporter)
.isInstanceOfSatisfying(
OtlpGrpcMetricExporter.class,
otlp -> assertThat(otlp).extracting("deadlineMs").isEqualTo(10L));
} finally {
exporter.shutdown();
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import java.util.Collections;
import org.junit.jupiter.api.Test;
class SpanExporterConfigurationTest {
private static final ConfigProperties EMPTY =
ConfigProperties.createForTest(Collections.emptyMap());
// Timeout difficult to test using real exports so just check implementation detail here.
@Test
void configureOtlpTimeout() {
SpanExporter exporter =
SpanExporterConfiguration.configureExporter(
"otlp",
ConfigProperties.createForTest(
Collections.singletonMap("otel.exporter.otlp.timeout", "10")));
try {
assertThat(exporter)
.isInstanceOfSatisfying(
OtlpGrpcSpanExporter.class,
otlp -> assertThat(otlp).extracting("deadlineMs").isEqualTo(10L));
} finally {
exporter.shutdown();
}
}
@Test
void configureExporterOtlpSpan() {
SpanExporter exporter = SpanExporterConfiguration.configureExporter("otlp_span", EMPTY);
try {
assertThat(exporter).isInstanceOf(OtlpGrpcSpanExporter.class);
} finally {
exporter.shutdown();
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer;
import io.opentelemetry.sdk.trace.ReadWriteSpan;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder;
import io.opentelemetry.sdk.trace.SpanProcessor;
public class TestTracerProviderConfigurer implements SdkTracerProviderConfigurer {
@Override
public void configure(SdkTracerProviderBuilder tracerProvider) {
tracerProvider.addSpanProcessor(
new SpanProcessor() {
@Override
public void onStart(Context parentContext, ReadWriteSpan span) {
span.setAttribute("configured", true);
}
@Override
public boolean isStartRequired() {
return true;
}
@Override
public void onEnd(ReadableSpan span) {}
@Override
public boolean isEndRequired() {
return false;
}
});
}
}

View File

@ -0,0 +1 @@
io.opentelemetry.sdk.autoconfigure.TestTracerProviderConfigurer

View File

@ -0,0 +1,48 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure;
import static org.assertj.core.api.Assertions.assertThat;
import com.linecorp.armeria.client.WebClient;
import com.linecorp.armeria.common.AggregatedHttpResponse;
import io.opentelemetry.api.common.Labels;
import io.opentelemetry.api.metrics.GlobalMetricsProvider;
import java.io.IOException;
import java.net.ServerSocket;
import org.junit.jupiter.api.Test;
class PrometheusTest {
@Test
void prometheusExporter() throws Exception {
int port = 9464;
// Just use prometheus standard port if it's available
try (ServerSocket unused = new ServerSocket(port)) {
// Port available
} catch (IOException e) {
// Otherwise use a random 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.
try (ServerSocket socket2 = new ServerSocket(0)) {
port = socket2.getLocalPort();
}
}
System.setProperty("otel.exporter.prometheus.host", "127.0.0.1");
System.setProperty("otel.exporter.prometheus.port", String.valueOf(port));
OpenTelemetrySdkAutoConfiguration.initialize();
GlobalMetricsProvider.get()
.get("test")
.longValueObserverBuilder("test")
.setUpdater(result -> result.observe(2, Labels.empty()))
.build();
WebClient client = WebClient.of("http://127.0.0.1:" + port);
AggregatedHttpResponse response = client.get("/metrics").aggregate().join();
assertThat(response.contentUtf8()).contains("test 2.0");
}
}

View File

@ -56,6 +56,7 @@ include ":all",
":sdk:testing",
":sdk:trace",
":sdk-extensions:async-processor",
":sdk-extensions:autoconfigure",
":sdk-extensions:aws",
":sdk-extensions:logging",
":sdk-extensions:otproto",