Define dedicated file configuration SPI ComponentProvider (#6457)

This commit is contained in:
jack-berg 2024-06-03 12:49:41 -05:00 committed by GitHub
parent 809457d2b0
commit d9cef81e29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 939 additions and 66 deletions

View File

@ -0,0 +1,47 @@
/*
* 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.internal.ComponentProvider;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import io.opentelemetry.sdk.metrics.export.MetricReader;
/**
* File configuration 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 PrometheusComponentProvider implements ComponentProvider<MetricReader> {
@Override
public Class<MetricReader> getType() {
return MetricReader.class;
}
@Override
public String getName() {
return "prometheus";
}
@Override
public MetricReader create(StructuredConfigProperties config) {
PrometheusHttpServerBuilder prometheusBuilder = PrometheusHttpServer.builder();
Integer port = config.getInt("port");
if (port != null) {
prometheusBuilder.setPort(port);
}
String host = config.getString("host");
if (host != null) {
prometheusBuilder.setHost(host);
}
return prometheusBuilder.build();
}
}

View File

@ -0,0 +1 @@
io.opentelemetry.exporter.prometheus.internal.PrometheusComponentProvider

View File

@ -0,0 +1,45 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure.spi.internal;
import io.opentelemetry.sdk.trace.export.SpanExporter;
/**
* Provides configured instances of SDK extension components. {@link ComponentProvider} allows SDK
* extension components which are not part of the core SDK to be referenced in file based
* configuration.
*
* @param <T> the type of the SDK extension component. See {@link #getType()}.
*/
// TODO (jack-berg): list the specific types which are supported in file configuration
public interface ComponentProvider<T> {
/**
* The type of SDK extension component. For example, if providing instances of a custom span
* exporter, the type would be {@link SpanExporter}.
*/
Class<T> getType();
/**
* The name of the exporter, to be referenced in configuration files. For example, if providing
* instances of a custom span exporter for the "acme" protocol, the name might be "acme".
*
* <p>This name MUST not be the same as any other component provider name which returns components
* of the same {@link #getType() type}.
*/
String getName();
/**
* Configure an instance of the SDK extension component according to the {@code config}.
*
* @param config the configuration provided where the component is referenced in a configuration
* file.
* @return an instance the SDK extension component
*/
// TODO (jack-berg): consider dynamic configuration use case before stabilizing in case that
// affects any API decisions
T create(StructuredConfigProperties config);
}

View File

@ -0,0 +1,187 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.autoconfigure.spi.internal;
import static io.opentelemetry.api.internal.ConfigUtil.defaultIfNull;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
/**
* An interface for accessing structured configuration data.
*
* <p>An instance of {@link StructuredConfigProperties} is equivalent to a <a
* href="https://yaml.org/spec/1.2.2/#3211-nodes">YAML mapping node</a>. It has accessors for
* reading scalar properties, {@link #getStructured(String)} for reading children which are
* themselves mappings, and {@link #getStructuredList(String)} for reading children which are
* sequences of mappings.
*/
public interface StructuredConfigProperties {
/**
* Returns a {@link String} configuration property.
*
* @return null if the property has not been configured
* @throws ConfigurationException if the property is not a valid scalar string
*/
@Nullable
String getString(String name);
/**
* Returns a {@link String} configuration property.
*
* @return a {@link String} configuration property or {@code defaultValue} if a property with
* {@code name} has not been configured
* @throws ConfigurationException if the property is not a valid scalar string
*/
default String getString(String name, String defaultValue) {
return defaultIfNull(getString(name), defaultValue);
}
/**
* Returns a {@link Boolean} configuration property. Implementations should use the same rules as
* {@link Boolean#parseBoolean(String)} for handling the values.
*
* @return null if the property has not been configured
* @throws ConfigurationException if the property is not a valid scalar boolean
*/
@Nullable
Boolean getBoolean(String name);
/**
* Returns a {@link Boolean} configuration property.
*
* @return a {@link Boolean} configuration property or {@code defaultValue} if a property with
* {@code name} has not been configured
* @throws ConfigurationException if the property is not a valid scalar boolean
*/
default boolean getBoolean(String name, boolean defaultValue) {
return defaultIfNull(getBoolean(name), defaultValue);
}
/**
* Returns a {@link Integer} configuration property.
*
* <p>If the underlying config property is {@link Long}, it is converted to {@link Integer} with
* {@link Long#intValue()} which may result in loss of precision.
*
* @return null if the property has not been configured
* @throws ConfigurationException if the property is not a valid scalar integer
*/
@Nullable
Integer getInt(String name);
/**
* Returns a {@link Integer} configuration property.
*
* <p>If the underlying config property is {@link Long}, it is converted to {@link Integer} with
* {@link Long#intValue()} which may result in loss of precision.
*
* @return a {@link Integer} configuration property or {@code defaultValue} if a property with
* {@code name} has not been configured
* @throws ConfigurationException if the property is not a valid scalar integer
*/
default int getInt(String name, int defaultValue) {
return defaultIfNull(getInt(name), defaultValue);
}
/**
* Returns a {@link Long} configuration property.
*
* @return null if the property has not been configured
* @throws ConfigurationException if the property is not a valid scalar long
*/
@Nullable
Long getLong(String name);
/**
* Returns a {@link Long} configuration property.
*
* @return a {@link Long} configuration property or {@code defaultValue} if a property with {@code
* name} has not been configured
* @throws ConfigurationException if the property is not a valid scalar long
*/
default long getLong(String name, long defaultValue) {
return defaultIfNull(getLong(name), defaultValue);
}
/**
* Returns a {@link Double} configuration property.
*
* @return null if the property has not been configured
* @throws ConfigurationException if the property is not a valid scalar double
*/
@Nullable
Double getDouble(String name);
/**
* Returns a {@link Double} configuration property.
*
* @return a {@link Double} configuration property or {@code defaultValue} if a property with
* {@code name} has not been configured
* @throws ConfigurationException if the property is not a valid scalar double
*/
default double getDouble(String name, double defaultValue) {
return defaultIfNull(getDouble(name), defaultValue);
}
/**
* Returns a {@link List} configuration property. Empty values and values which do not map to the
* {@code scalarType} will be removed.
*
* @param name the property name
* @param scalarType the scalar type, one of {@link String}, {@link Boolean}, {@link Long} or
* {@link Double}
* @return a {@link List} configuration property, or null if the property has not been configured
* @throws ConfigurationException if the property is not a valid sequence of scalars, or if {@code
* scalarType} is not supported
*/
@Nullable
<T> List<T> getScalarList(String name, Class<T> scalarType);
/**
* Returns a {@link List} configuration property. Entries which are not strings are converted to
* their string representation.
*
* @see ConfigProperties#getList(String name)
* @return a {@link List} configuration property or {@code defaultValue} if a property with {@code
* name} has not been configured
* @throws ConfigurationException if the property is not a valid sequence of scalars
*/
default <T> List<T> getScalarList(String name, Class<T> scalarType, List<T> defaultValue) {
return defaultIfNull(getScalarList(name, scalarType), defaultValue);
}
/**
* Returns a {@link StructuredConfigProperties} configuration property.
*
* @return a map-valued configuration property, or {@code null} if {@code name} has not been
* configured
* @throws ConfigurationException if the property is not a mapping
*/
@Nullable
StructuredConfigProperties getStructured(String name);
/**
* Returns a list of {@link StructuredConfigProperties} configuration property.
*
* @return a list of map-valued configuration property, or {@code null} if {@code name} has not
* been configured
* @throws ConfigurationException if the property is not a sequence of mappings
*/
@Nullable
List<StructuredConfigProperties> getStructuredList(String name);
/**
* Returns a set of all configuration property keys.
*
* @return the configuration property keys
*/
Set<String> getPropertyKeys();
}

View File

@ -10,7 +10,9 @@ import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import io.opentelemetry.sdk.resources.Resource;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
/**
@ -43,8 +45,12 @@ public abstract class AutoConfiguredOpenTelemetrySdk {
}
static AutoConfiguredOpenTelemetrySdk create(
OpenTelemetrySdk sdk, Resource resource, ConfigProperties config) {
return new AutoValue_AutoConfiguredOpenTelemetrySdk(sdk, resource, config);
OpenTelemetrySdk sdk,
Resource resource,
@Nullable ConfigProperties config,
@Nullable StructuredConfigProperties structuredConfigProperties) {
return new AutoValue_AutoConfiguredOpenTelemetrySdk(
sdk, resource, config, structuredConfigProperties);
}
/**
@ -60,8 +66,23 @@ public abstract class AutoConfiguredOpenTelemetrySdk {
/** Returns the {@link Resource} that was auto-configured. */
abstract Resource getResource();
/** Returns the {@link ConfigProperties} used for auto-configuration. */
/**
* Returns the {@link ConfigProperties} used for auto-configuration, or {@code null} if file
* configuration was used.
*
* @see #getStructuredConfig()
*/
@Nullable
abstract ConfigProperties getConfig();
/**
* Returns the {@link StructuredConfigProperties} used for auto-configuration, or {@code null} if
* file configuration was not used.
*
* @see #getConfig()
*/
@Nullable
abstract StructuredConfigProperties getStructuredConfig();
AutoConfiguredOpenTelemetrySdk() {}
}

View File

@ -21,6 +21,7 @@ import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.autoconfigure.spi.internal.AutoConfigureListener;
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import io.opentelemetry.sdk.logs.LogRecordProcessor;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder;
@ -505,7 +506,7 @@ public final class AutoConfiguredOpenTelemetrySdkBuilder implements AutoConfigur
maybeSetAsGlobal(openTelemetrySdk);
callAutoConfigureListeners(spiHelper, openTelemetrySdk);
return AutoConfiguredOpenTelemetrySdk.create(openTelemetrySdk, resource, config);
return AutoConfiguredOpenTelemetrySdk.create(openTelemetrySdk, resource, config, null);
} catch (RuntimeException e) {
logger.info(
"Error encountered during autoconfiguration. Closing partially configured components.");
@ -546,11 +547,21 @@ public final class AutoConfiguredOpenTelemetrySdkBuilder implements AutoConfigur
try {
Class<?> configurationFactory =
Class.forName("io.opentelemetry.sdk.extension.incubator.fileconfig.FileConfiguration");
Method parseAndCreate = configurationFactory.getMethod("parseAndCreate", InputStream.class);
OpenTelemetrySdk sdk = (OpenTelemetrySdk) parseAndCreate.invoke(null, fis);
Method parse = configurationFactory.getMethod("parse", InputStream.class);
Object model = parse.invoke(null, fis);
Class<?> openTelemetryConfiguration =
Class.forName(
"io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfiguration");
Method create = configurationFactory.getMethod("create", openTelemetryConfiguration);
OpenTelemetrySdk sdk = (OpenTelemetrySdk) create.invoke(null, model);
Method toConfigProperties =
configurationFactory.getMethod("toConfigProperties", openTelemetryConfiguration);
StructuredConfigProperties structuredConfigProperties =
(StructuredConfigProperties) toConfigProperties.invoke(null, model);
// Note: can't access file configuration resource without reflection so setting a dummy
// resource
return AutoConfiguredOpenTelemetrySdk.create(sdk, Resource.getDefault(), config);
return AutoConfiguredOpenTelemetrySdk.create(
sdk, Resource.getDefault(), null, structuredConfigProperties);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
throw new ConfigurationException(
"Error configuring from file. Is opentelemetry-sdk-extension-incubator on the classpath?",

View File

@ -8,9 +8,11 @@ package io.opentelemetry.sdk.autoconfigure.internal;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.function.Function;
import javax.annotation.Nullable;
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
@ -20,7 +22,12 @@ public final class AutoConfigureUtil {
private AutoConfigureUtil() {}
/** Returns the {@link ConfigProperties} used for auto-configuration. */
/**
* Returns the {@link ConfigProperties} used for auto-configuration.
*
* @return the config properties, or {@code null} if file based configuration is used
*/
@Nullable
public static ConfigProperties getConfig(
AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) {
try {
@ -33,6 +40,25 @@ public final class AutoConfigureUtil {
}
}
/**
* Returns the {@link StructuredConfigProperties} used for auto-configuration when file based
* configuration is used.
*
* @return the config properties, or {@code null} if file based configuration is NOT used
*/
@Nullable
public static StructuredConfigProperties getStructuredConfig(
AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) {
try {
Method method = AutoConfiguredOpenTelemetrySdk.class.getDeclaredMethod("getStructuredConfig");
method.setAccessible(true);
return (StructuredConfigProperties) method.invoke(autoConfiguredOpenTelemetrySdk);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new IllegalStateException(
"Error calling getStructuredConfig on AutoConfiguredOpenTelemetrySdk", e);
}
}
/** Sets the {@link ComponentLoader} to be used in the auto-configuration process. */
public static AutoConfiguredOpenTelemetrySdkBuilder setComponentLoader(
AutoConfiguredOpenTelemetrySdkBuilder builder, ComponentLoader componentLoader) {

View File

@ -6,7 +6,6 @@
package io.opentelemetry.sdk.autoconfigure;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
@ -29,6 +28,7 @@ import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import io.opentelemetry.sdk.logs.internal.SdkEventLoggerProvider;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
@ -67,7 +67,11 @@ class FileConfigurationTest {
+ " processors:\n"
+ " - simple:\n"
+ " exporter:\n"
+ " console: {}\n";
+ " console: {}\n"
+ "other:\n"
+ " str_key: str_value\n"
+ " map_key:\n"
+ " str_key1: str_value1\n";
configFilePath = tempDir.resolve("otel-config.yaml");
Files.write(configFilePath, yaml.getBytes(StandardCharsets.UTF_8));
GlobalOpenTelemetry.resetForTest();
@ -183,4 +187,27 @@ class FileConfigurationTest {
.isInstanceOf(ConfigurationException.class)
.hasMessage("Unrecognized span exporter(s): [foo]");
}
@Test
void configFile_StructuredConfigProperties() {
ConfigProperties config =
DefaultConfigProperties.createFromMap(
Collections.singletonMap("otel.experimental.config.file", configFilePath.toString()));
AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk =
AutoConfiguredOpenTelemetrySdk.builder().setConfig(config).setResultAsGlobal().build();
OpenTelemetrySdk openTelemetrySdk = autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk();
cleanup.addCloseable(openTelemetrySdk);
// getConfig() should return ExtendedConfigProperties generic representation of the config file
StructuredConfigProperties structuredConfigProps =
autoConfiguredOpenTelemetrySdk.getStructuredConfig();
assertThat(structuredConfigProps).isNotNull();
StructuredConfigProperties otherProps = structuredConfigProps.getStructured("other");
assertThat(otherProps).isNotNull();
assertThat(otherProps.getString("str_key")).isEqualTo("str_value");
StructuredConfigProperties otherMapKeyProps = otherProps.getStructured("map_key");
assertThat(otherMapKeyProps).isNotNull();
assertThat(otherMapKeyProps.getString("str_key1")).isEqualTo("str_value1");
}
}

View File

@ -5,8 +5,14 @@
package io.opentelemetry.sdk.extension.incubator.fileconfig;
import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import java.io.Closeable;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
final class FileConfigUtil {
@ -27,4 +33,54 @@ final class FileConfigUtil {
}
return object;
}
/**
* Find a registered {@link ComponentProvider} which {@link ComponentProvider#getType()} matching
* {@code type}, {@link ComponentProvider#getName()} matching {@code name}, and call {@link
* ComponentProvider#create(StructuredConfigProperties)} with the given {@code model}.
*
* @throws ConfigurationException if no matching providers are found, or if multiple are found
* (i.e. conflict), or if {@link ComponentProvider#create(StructuredConfigProperties)} throws
*/
@SuppressWarnings({"unchecked", "rawtypes"})
static <T> T loadComponent(SpiHelper spiHelper, Class<T> type, String name, Object model) {
// TODO(jack-berg): cache loaded component providers
List<ComponentProvider> componentProviders = spiHelper.load(ComponentProvider.class);
List<ComponentProvider<?>> matchedProviders =
componentProviders.stream()
.map(
(Function<ComponentProvider, ComponentProvider<?>>)
componentProvider -> componentProvider)
.filter(
componentProvider ->
componentProvider.getType() == type && name.equals(componentProvider.getName()))
.collect(Collectors.toList());
if (matchedProviders.isEmpty()) {
throw new ConfigurationException(
"No component provider detected for " + type.getName() + " with name \"" + name + "\".");
}
if (matchedProviders.size() > 1) {
throw new ConfigurationException(
"Component provider conflict. Multiple providers detected for "
+ type.getName()
+ " with name \""
+ name
+ "\": "
+ componentProviders.stream()
.map(provider -> provider.getClass().getName())
.collect(Collectors.joining(",", "[", "]")));
}
// Exactly one matching component provider
ComponentProvider<T> provider = (ComponentProvider<T>) matchedProviders.get(0);
// Map model to generic structured config properties
StructuredConfigProperties config = FileConfiguration.toConfigProperties(model);
try {
return provider.create(config);
} catch (Throwable throwable) {
throw new ConfigurationException(
"Error configuring " + type.getName() + " with name \"" + name + "\"", throwable);
}
}
}

View File

@ -7,10 +7,12 @@ package io.opentelemetry.sdk.extension.incubator.fileconfig;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfiguration;
import java.io.Closeable;
import java.io.IOException;
@ -138,6 +140,23 @@ public final class FileConfiguration {
return yaml.loadFromInputStream(inputStream);
}
/**
* Convert the {@code model} to a generic {@link StructuredConfigProperties}, which can be used to
* read configuration not part of the model.
*
* @param model the configuration model
* @return a generic {@link StructuredConfigProperties} representation of the model
*/
public static StructuredConfigProperties toConfigProperties(OpenTelemetryConfiguration model) {
return toConfigProperties((Object) model);
}
static StructuredConfigProperties toConfigProperties(Object model) {
Map<String, Object> configurationMap =
MAPPER.convertValue(model, new TypeReference<Map<String, Object>>() {});
return YamlStructuredConfigProperties.create(configurationMap);
}
/**
* {@link StandardConstructor} which substitutes environment variables.
*

View File

@ -5,12 +5,8 @@
package io.opentelemetry.sdk.extension.incubator.fileconfig;
import io.opentelemetry.sdk.autoconfigure.internal.NamedSpiManager;
import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider;
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.MetricExporter;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.PeriodicMetricReader;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.Prometheus;
@ -19,9 +15,7 @@ import io.opentelemetry.sdk.metrics.export.MetricReader;
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReaderBuilder;
import java.io.Closeable;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
final class MetricReaderFactory
@ -77,25 +71,10 @@ final class MetricReaderFactory
}
Prometheus prometheusModel = exporterModel.getPrometheus();
if (prometheusModel != null) {
// Translate from file configuration scheme to environment variable scheme. This is
// ultimately
// interpreted by PrometheusMetricReaderProvider, but we want to avoid the dependency on
// opentelemetry-exporter-prometheus
Map<String, String> properties = new HashMap<>();
if (prometheusModel.getHost() != null) {
properties.put("otel.exporter.prometheus.host", prometheusModel.getHost());
}
if (prometheusModel.getPort() != null) {
properties.put(
"otel.exporter.prometheus.port", String.valueOf(prometheusModel.getPort()));
}
ConfigProperties configProperties = DefaultConfigProperties.createFromMap(properties);
return FileConfigUtil.addAndReturn(
closeables,
FileConfigUtil.assertNotNull(
metricReaderSpiManager(configProperties, spiHelper).getByName("prometheus"),
"prometheus reader"));
MetricReader metricReader =
FileConfigUtil.loadComponent(
spiHelper, MetricReader.class, "prometheus", prometheusModel);
return FileConfigUtil.addAndReturn(closeables, metricReader);
}
throw new ConfigurationException("prometheus is the only currently supported pull reader");
@ -103,13 +82,4 @@ final class MetricReaderFactory
return null;
}
private static NamedSpiManager<io.opentelemetry.sdk.metrics.export.MetricReader>
metricReaderSpiManager(ConfigProperties config, SpiHelper spiHelper) {
return spiHelper.loadConfigurable(
ConfigurableMetricReaderProvider.class,
ConfigurableMetricReaderProvider::getName,
ConfigurableMetricReaderProvider::createMetricReader,
config);
}
}

View File

@ -0,0 +1,280 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.extension.incubator.fileconfig;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfiguration;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import javax.annotation.Nullable;
/**
* Implementation of {@link StructuredConfigProperties} which uses a file configuration model as a
* source.
*
* @see #getStructured(String) Accessing nested maps
* @see #getStructuredList(String) Accessing lists of maps
* @see FileConfiguration#toConfigProperties(Object) Converting configuration model to properties
*/
final class YamlStructuredConfigProperties implements StructuredConfigProperties {
/** Values are {@link #isPrimitive(Object)}, {@link List} of scalars. */
private final Map<String, Object> simpleEntries;
private final Map<String, List<StructuredConfigProperties>> listEntries;
private final Map<String, StructuredConfigProperties> mapEntries;
private YamlStructuredConfigProperties(
Map<String, Object> simpleEntries,
Map<String, List<StructuredConfigProperties>> listEntries,
Map<String, StructuredConfigProperties> mapEntries) {
this.simpleEntries = simpleEntries;
this.listEntries = listEntries;
this.mapEntries = mapEntries;
}
/**
* Create a {@link YamlStructuredConfigProperties} from the {@code properties} map.
*
* <p>{@code properties} is expected to be the output of YAML parsing (i.e. with Jackson {@link
* com.fasterxml.jackson.databind.ObjectMapper}), and have values which are scalars, lists of
* scalars, lists of maps, and maps.
*
* @see FileConfiguration#toConfigProperties(OpenTelemetryConfiguration)
*/
@SuppressWarnings("unchecked")
static YamlStructuredConfigProperties create(Map<String, Object> properties) {
Map<String, Object> simpleEntries = new HashMap<>();
Map<String, List<StructuredConfigProperties>> listEntries = new HashMap<>();
Map<String, StructuredConfigProperties> mapEntries = new HashMap<>();
for (Map.Entry<String, Object> entry : properties.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (isPrimitive(value)) {
simpleEntries.put(key, value);
continue;
}
if (isPrimitiveList(value)) {
simpleEntries.put(key, value);
continue;
}
if (isListOfMaps(value)) {
List<StructuredConfigProperties> list =
((List<Map<String, Object>>) value)
.stream().map(YamlStructuredConfigProperties::create).collect(toList());
listEntries.put(key, list);
continue;
}
if (isMap(value)) {
YamlStructuredConfigProperties configProperties =
YamlStructuredConfigProperties.create((Map<String, Object>) value);
mapEntries.put(key, configProperties);
continue;
}
throw new ConfigurationException(
"Unable to initialize ExtendedConfigProperties. Key \""
+ key
+ "\" has unrecognized object type "
+ value.getClass().getName());
}
return new YamlStructuredConfigProperties(simpleEntries, listEntries, mapEntries);
}
private static boolean isPrimitiveList(Object object) {
if (object instanceof List) {
List<?> list = (List<?>) object;
return list.stream().allMatch(YamlStructuredConfigProperties::isPrimitive);
}
return false;
}
private static boolean isPrimitive(Object object) {
return object instanceof String
|| object instanceof Integer
|| object instanceof Long
|| object instanceof Float
|| object instanceof Double
|| object instanceof Boolean;
}
private static boolean isListOfMaps(Object object) {
if (object instanceof List) {
List<?> list = (List<?>) object;
return list.stream()
.allMatch(
entry ->
entry instanceof Map
&& ((Map<?, ?>) entry)
.keySet().stream().allMatch(key -> key instanceof String));
}
return false;
}
private static boolean isMap(Object object) {
if (object instanceof Map) {
Map<?, ?> map = (Map<?, ?>) object;
return map.keySet().stream().allMatch(entry -> entry instanceof String);
}
return false;
}
@Nullable
@Override
public String getString(String name) {
return stringOrNull(simpleEntries.get(name));
}
@Nullable
@Override
public Boolean getBoolean(String name) {
return booleanOrNull(simpleEntries.get(name));
}
@Nullable
@Override
public Integer getInt(String name) {
Object value = simpleEntries.get(name);
if (value instanceof Integer) {
return (Integer) value;
}
if (value instanceof Long) {
return ((Long) value).intValue();
}
return null;
}
@Nullable
@Override
public Long getLong(String name) {
return longOrNull(simpleEntries.get(name));
}
@Nullable
@Override
public Double getDouble(String name) {
return doubleOrNull(simpleEntries.get(name));
}
private static final Set<Class<?>> SUPPORTED_SCALAR_TYPES =
Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(String.class, Boolean.class, Long.class, Double.class)));
@Nullable
@Override
@SuppressWarnings("unchecked")
public <T> List<T> getScalarList(String name, Class<T> scalarType) {
if (!SUPPORTED_SCALAR_TYPES.contains(scalarType)) {
throw new ConfigurationException(
"Unsupported scalar type "
+ scalarType.getName()
+ ". Supported types include "
+ SUPPORTED_SCALAR_TYPES.stream()
.map(Class::getName)
.collect(joining(",", "[", "]")));
}
Object value = simpleEntries.get(name);
if (value instanceof List) {
return (List<T>)
((List<Object>) value)
.stream()
.map(
entry -> {
if (scalarType == String.class) {
return stringOrNull(entry);
} else if (scalarType == Boolean.class) {
return booleanOrNull(entry);
} else if (scalarType == Long.class) {
return longOrNull(entry);
} else if (scalarType == Double.class) {
return doubleOrNull(entry);
}
return null;
})
.filter(Objects::nonNull)
.collect(toList());
}
return null;
}
@Nullable
private static String stringOrNull(@Nullable Object value) {
if (value instanceof String) {
return (String) value;
}
return null;
}
@Nullable
private static Boolean booleanOrNull(@Nullable Object value) {
if (value instanceof Boolean) {
return (Boolean) value;
}
return null;
}
@Nullable
private static Long longOrNull(@Nullable Object value) {
if (value instanceof Integer) {
return ((Integer) value).longValue();
}
if (value instanceof Long) {
return (Long) value;
}
return null;
}
@Nullable
private static Double doubleOrNull(@Nullable Object value) {
if (value instanceof Float) {
return ((Float) value).doubleValue();
}
if (value instanceof Double) {
return (Double) value;
}
return null;
}
@Nullable
@Override
public StructuredConfigProperties getStructured(String name) {
return mapEntries.get(name);
}
@Nullable
@Override
public List<StructuredConfigProperties> getStructuredList(String name) {
return listEntries.get(name);
}
@Override
public Set<String> getPropertyKeys() {
Set<String> keys = new HashSet<>();
keys.addAll(simpleEntries.keySet());
keys.addAll(listEntries.keySet());
keys.addAll(mapEntries.keySet());
return Collections.unmodifiableSet(keys);
}
@Override
public String toString() {
StringJoiner joiner = new StringJoiner(", ", "YamlStructuredConfigProperties{", "}");
simpleEntries.forEach((key, value) -> joiner.add(key + "=" + value));
listEntries.forEach((key, value) -> joiner.add(key + "=" + value));
mapEntries.forEach((key, value) -> joiner.add(key + "=" + value));
return joiner.toString();
}
}

View File

@ -6,10 +6,7 @@
package io.opentelemetry.sdk.extension.incubator.fileconfig;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@ -18,9 +15,8 @@ import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter;
import io.opentelemetry.exporter.prometheus.PrometheusHttpServer;
import io.opentelemetry.internal.testing.CleanupExtension;
import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider;
import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.MetricExporter;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.MetricReader;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OtlpMetric;
@ -36,7 +32,6 @@ import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
class MetricReaderFactoryTest {
@ -141,14 +136,8 @@ class MetricReaderFactoryTest {
cleanup.addCloseables(closeables);
assertThat(reader.toString()).isEqualTo(expectedReader.toString());
ArgumentCaptor<ConfigProperties> configCaptor = ArgumentCaptor.forClass(ConfigProperties.class);
verify(spiHelper)
.loadConfigurable(
eq(ConfigurableMetricReaderProvider.class), any(), any(), configCaptor.capture());
ConfigProperties configProperties = configCaptor.getValue();
assertThat(configProperties.getString("otel.exporter.prometheus.host")).isNull();
assertThat(configProperties.getInt("otel.exporter.prometheus.port")).isEqualTo(port);
// TODO(jack-berg): validate prometheus component provider was invoked with correct arguments
verify(spiHelper).load(ComponentProvider.class);
}
@Test
@ -178,14 +167,8 @@ class MetricReaderFactoryTest {
cleanup.addCloseables(closeables);
assertThat(reader.toString()).isEqualTo(expectedReader.toString());
ArgumentCaptor<ConfigProperties> configCaptor = ArgumentCaptor.forClass(ConfigProperties.class);
verify(spiHelper)
.loadConfigurable(
eq(ConfigurableMetricReaderProvider.class), any(), any(), configCaptor.capture());
ConfigProperties configProperties = configCaptor.getValue();
assertThat(configProperties.getString("otel.exporter.prometheus.host")).isEqualTo("localhost");
assertThat(configProperties.getInt("otel.exporter.prometheus.port")).isEqualTo(port);
// TODO(jack-berg): validate prometheus component provider was invoked with correct arguments
verify(spiHelper).load(ComponentProvider.class);
}
@Test

View File

@ -0,0 +1,200 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.extension.incubator.fileconfig;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.common.collect.ImmutableSet;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfiguration;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class YamlStructuredConfigPropertiesTest {
private static final String extendedSchema =
"file_format: \"0.1\"\n"
+ "disabled: false\n"
+ "\n"
+ "resource:\n"
+ " attributes:\n"
+ " service.name: \"unknown_service\"\n"
+ "\n"
+ "other:\n"
+ " str_key: str_value\n"
+ " int_key: 1\n"
+ " float_key: 1.1\n"
+ " bool_key: true\n"
+ " str_list_key: [val1, val2]\n"
+ " int_list_key: [1, 2]\n"
+ " float_list_key: [1.1, 2.2]\n"
+ " bool_list_key: [true, false]\n"
+ " mixed_list_key: [val1, 1, 1.1, true]\n"
+ " map_key:\n"
+ " str_key1: str_value1\n"
+ " int_key1: 2\n"
+ " map_key1:\n"
+ " str_key2: str_value2\n"
+ " int_key2: 3\n"
+ " list_key:\n"
+ " - str_key1: str_value1\n"
+ " int_key1: 2\n"
+ " map_key1:\n"
+ " str_key2: str_value2\n"
+ " int_key2: 3\n"
+ " - str_key1: str_value1\n"
+ " int_key1: 2";
private StructuredConfigProperties structuredConfigProps;
@BeforeEach
void setup() {
OpenTelemetryConfiguration configuration =
FileConfiguration.parse(
new ByteArrayInputStream(extendedSchema.getBytes(StandardCharsets.UTF_8)));
structuredConfigProps = FileConfiguration.toConfigProperties(configuration);
}
@Test
void configurationSchema() {
// Validate can read file configuration schema properties
assertThat(structuredConfigProps.getString("file_format")).isEqualTo("0.1");
StructuredConfigProperties resourceProps = structuredConfigProps.getStructured("resource");
assertThat(resourceProps).isNotNull();
StructuredConfigProperties resourceAttributesProps = resourceProps.getStructured("attributes");
assertThat(resourceAttributesProps).isNotNull();
assertThat(resourceAttributesProps.getString("service.name")).isEqualTo("unknown_service");
}
@Test
void additionalProperties() {
assertThat(structuredConfigProps.getPropertyKeys())
.isEqualTo(ImmutableSet.of("file_format", "disabled", "resource", "other"));
// Validate can read properties not part of configuration schema
// .other
StructuredConfigProperties otherProps = structuredConfigProps.getStructured("other");
assertThat(otherProps).isNotNull();
assertThat(otherProps.getPropertyKeys())
.isEqualTo(
ImmutableSet.of(
"str_key",
"int_key",
"float_key",
"bool_key",
"str_list_key",
"int_list_key",
"float_list_key",
"bool_list_key",
"mixed_list_key",
"map_key",
"list_key"));
assertThat(otherProps.getString("str_key")).isEqualTo("str_value");
assertThat(otherProps.getInt("int_key")).isEqualTo(1);
assertThat(otherProps.getLong("int_key")).isEqualTo(1);
assertThat(otherProps.getDouble("float_key")).isEqualTo(1.1);
assertThat(otherProps.getBoolean("bool_key")).isTrue();
assertThat(otherProps.getScalarList("str_list_key", String.class))
.isEqualTo(Arrays.asList("val1", "val2"));
assertThat(otherProps.getScalarList("int_list_key", Long.class))
.isEqualTo(Arrays.asList(1L, 2L));
assertThat(otherProps.getScalarList("float_list_key", Double.class))
.isEqualTo(Arrays.asList(1.1d, 2.2d));
assertThat(otherProps.getScalarList("bool_list_key", Boolean.class))
.isEqualTo(Arrays.asList(true, false));
// If reading a scalar list which is mixed, entries which are not aligned with the requested
// type are filtered out
assertThat(otherProps.getScalarList("mixed_list_key", String.class))
.isEqualTo(Collections.singletonList("val1"));
assertThat(otherProps.getScalarList("mixed_list_key", Long.class))
.isEqualTo(Collections.singletonList(1L));
assertThat(otherProps.getScalarList("mixed_list_key", Double.class))
.isEqualTo(Collections.singletonList(1.1d));
assertThat(otherProps.getScalarList("mixed_list_key", Boolean.class))
.isEqualTo(Collections.singletonList(true));
// .other.map_key
StructuredConfigProperties otherMapKeyProps = otherProps.getStructured("map_key");
assertThat(otherMapKeyProps).isNotNull();
assertThat(otherMapKeyProps.getPropertyKeys())
.isEqualTo(ImmutableSet.of("str_key1", "int_key1", "map_key1"));
assertThat(otherMapKeyProps.getString("str_key1")).isEqualTo("str_value1");
assertThat(otherMapKeyProps.getInt("int_key1")).isEqualTo(2);
// other.map_key.map_key1
StructuredConfigProperties otherMapKeyMapKey1Props = otherMapKeyProps.getStructured("map_key1");
assertThat(otherMapKeyMapKey1Props).isNotNull();
assertThat(otherMapKeyMapKey1Props.getPropertyKeys())
.isEqualTo(ImmutableSet.of("str_key2", "int_key2"));
assertThat(otherMapKeyMapKey1Props.getString("str_key2")).isEqualTo("str_value2");
assertThat(otherMapKeyMapKey1Props.getInt("int_key2")).isEqualTo(3);
// .other.list_key
List<StructuredConfigProperties> listKey = otherProps.getStructuredList("list_key");
assertThat(listKey).hasSize(2);
StructuredConfigProperties listKeyProps1 = listKey.get(0);
assertThat(listKeyProps1.getPropertyKeys())
.isEqualTo(ImmutableSet.of("str_key1", "int_key1", "map_key1"));
assertThat(listKeyProps1.getString("str_key1")).isEqualTo("str_value1");
assertThat(listKeyProps1.getInt("int_key1")).isEqualTo(2);
// .other.list_key[0]
StructuredConfigProperties listKeyProps1MapKeyProps = listKeyProps1.getStructured("map_key1");
assertThat(listKeyProps1MapKeyProps).isNotNull();
assertThat(listKeyProps1MapKeyProps.getPropertyKeys())
.isEqualTo(ImmutableSet.of("str_key2", "int_key2"));
assertThat(listKeyProps1MapKeyProps.getString("str_key2")).isEqualTo("str_value2");
assertThat(listKeyProps1MapKeyProps.getInt("int_key2")).isEqualTo(3);
// .other.list_key[1]
StructuredConfigProperties listKeyProps2 = listKey.get(1);
assertThat(listKeyProps2.getPropertyKeys()).isEqualTo(ImmutableSet.of("str_key1", "int_key1"));
assertThat(listKeyProps2.getString("str_key1")).isEqualTo("str_value1");
assertThat(listKeyProps2.getInt("int_key1")).isEqualTo(2);
}
@Test
void defaults() {
assertThat(structuredConfigProps.getString("foo", "bar")).isEqualTo("bar");
assertThat(structuredConfigProps.getInt("foo", 1)).isEqualTo(1);
assertThat(structuredConfigProps.getLong("foo", 1)).isEqualTo(1);
assertThat(structuredConfigProps.getDouble("foo", 1.1)).isEqualTo(1.1);
assertThat(structuredConfigProps.getBoolean("foo", true)).isTrue();
assertThat(
structuredConfigProps.getScalarList(
"foo", String.class, Collections.singletonList("bar")))
.isEqualTo(Collections.singletonList("bar"));
}
@Test
void missingKeys() {
assertThat(structuredConfigProps.getString("foo")).isNull();
assertThat(structuredConfigProps.getInt("foo")).isNull();
assertThat(structuredConfigProps.getLong("foo")).isNull();
assertThat(structuredConfigProps.getDouble("foo")).isNull();
assertThat(structuredConfigProps.getBoolean("foo")).isNull();
assertThat(structuredConfigProps.getScalarList("foo", String.class)).isNull();
assertThat(structuredConfigProps.getStructured("foo")).isNull();
assertThat(structuredConfigProps.getStructuredList("foo")).isNull();
}
@Test
void wrongType() {
StructuredConfigProperties otherProps = structuredConfigProps.getStructured("other");
assertThat(otherProps).isNotNull();
assertThat(otherProps.getString("int_key")).isNull();
assertThat(otherProps.getInt("str_key")).isNull();
assertThat(otherProps.getLong("str_key")).isNull();
assertThat(otherProps.getDouble("str_key")).isNull();
assertThat(otherProps.getBoolean("str_key")).isNull();
assertThat(otherProps.getScalarList("str_key", String.class)).isNull();
assertThat(otherProps.getStructured("str_key")).isNull();
assertThat(otherProps.getStructuredList("str_key")).isNull();
}
}