diff --git a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/internal/PrometheusComponentProvider.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/internal/PrometheusComponentProvider.java new file mode 100644 index 00000000000..dc57d2bcbe3 --- /dev/null +++ b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/internal/PrometheusComponentProvider.java @@ -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}. + * + *

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 { + + @Override + public Class 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(); + } +} diff --git a/exporters/prometheus/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider b/exporters/prometheus/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider new file mode 100644 index 00000000000..f3c72966e4b --- /dev/null +++ b/exporters/prometheus/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider @@ -0,0 +1 @@ +io.opentelemetry.exporter.prometheus.internal.PrometheusComponentProvider diff --git a/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/ComponentProvider.java b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/ComponentProvider.java new file mode 100644 index 00000000000..344dc18267c --- /dev/null +++ b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/ComponentProvider.java @@ -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 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 { + + /** + * The type of SDK extension component. For example, if providing instances of a custom span + * exporter, the type would be {@link SpanExporter}. + */ + Class 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". + * + *

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); +} diff --git a/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/StructuredConfigProperties.java b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/StructuredConfigProperties.java new file mode 100644 index 00000000000..ad1e3b5de3c --- /dev/null +++ b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/StructuredConfigProperties.java @@ -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. + * + *

An instance of {@link StructuredConfigProperties} is equivalent to a YAML mapping node. 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. + * + *

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. + * + *

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 + List getScalarList(String name, Class 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 List getScalarList(String name, Class scalarType, List 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 getStructuredList(String name); + + /** + * Returns a set of all configuration property keys. + * + * @return the configuration property keys + */ + Set getPropertyKeys(); +} diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdk.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdk.java index 997df49062c..6751da7ad7f 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdk.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdk.java @@ -10,7 +10,9 @@ 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 static AutoConfiguredOpenTelemetrySdkBuilder builder() { } 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 @@ static AutoConfiguredOpenTelemetrySdk create( /** 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() {} } diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdkBuilder.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdkBuilder.java index 923f1535257..7fd5d515f4c 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdkBuilder.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdkBuilder.java @@ -21,6 +21,7 @@ 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 AutoConfiguredOpenTelemetrySdk build() { 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 @@ private static AutoConfiguredOpenTelemetrySdk maybeConfigureFromFile(ConfigPrope 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?", diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/internal/AutoConfigureUtil.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/internal/AutoConfigureUtil.java index 8a50d782031..fe18ac8d41b 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/internal/AutoConfigureUtil.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/internal/AutoConfigureUtil.java @@ -8,9 +8,11 @@ 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 static ConfigProperties getConfig( } } + /** + * 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) { diff --git a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/FileConfigurationTest.java b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/FileConfigurationTest.java index a40884d032b..639f2955da3 100644 --- a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/FileConfigurationTest.java +++ b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/FileConfigurationTest.java @@ -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.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 @@ void setup() throws IOException { + " 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 @@ void configFile_Error(@TempDir Path tempDir) throws IOException { .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"); + } } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfigUtil.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfigUtil.java index f1c882a48b9..7b5bcd9ca2d 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfigUtil.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfigUtil.java @@ -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 @@ static T assertNotNull(@Nullable T object, String description) { } 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 loadComponent(SpiHelper spiHelper, Class type, String name, Object model) { + // TODO(jack-berg): cache loaded component providers + List componentProviders = spiHelper.load(ComponentProvider.class); + List> matchedProviders = + componentProviders.stream() + .map( + (Function>) + 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 provider = (ComponentProvider) 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); + } + } } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfiguration.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfiguration.java index 94150a04b8a..82888ed5735 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfiguration.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfiguration.java @@ -7,10 +7,12 @@ 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 @@ static Object loadYaml(InputStream inputStream, Map environmentV 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 configurationMap = + MAPPER.convertValue(model, new TypeReference>() {}); + return YamlStructuredConfigProperties.create(configurationMap); + } + /** * {@link StandardConstructor} which substitutes environment variables. * diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricReaderFactory.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricReaderFactory.java index bcaafd6c6e3..77750920cf2 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricReaderFactory.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricReaderFactory.java @@ -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.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 @@ public MetricReader create( } 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 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 @@ public MetricReader create( return null; } - - private static NamedSpiManager - metricReaderSpiManager(ConfigProperties config, SpiHelper spiHelper) { - return spiHelper.loadConfigurable( - ConfigurableMetricReaderProvider.class, - ConfigurableMetricReaderProvider::getName, - ConfigurableMetricReaderProvider::createMetricReader, - config); - } } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/YamlStructuredConfigProperties.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/YamlStructuredConfigProperties.java new file mode 100644 index 00000000000..6475bbe1698 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/YamlStructuredConfigProperties.java @@ -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 simpleEntries; + + private final Map> listEntries; + private final Map mapEntries; + + private YamlStructuredConfigProperties( + Map simpleEntries, + Map> listEntries, + Map mapEntries) { + this.simpleEntries = simpleEntries; + this.listEntries = listEntries; + this.mapEntries = mapEntries; + } + + /** + * Create a {@link YamlStructuredConfigProperties} from the {@code properties} map. + * + *

{@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 properties) { + Map simpleEntries = new HashMap<>(); + Map> listEntries = new HashMap<>(); + Map mapEntries = new HashMap<>(); + for (Map.Entry 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 list = + ((List>) value) + .stream().map(YamlStructuredConfigProperties::create).collect(toList()); + listEntries.put(key, list); + continue; + } + if (isMap(value)) { + YamlStructuredConfigProperties configProperties = + YamlStructuredConfigProperties.create((Map) 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> SUPPORTED_SCALAR_TYPES = + Collections.unmodifiableSet( + new HashSet<>(Arrays.asList(String.class, Boolean.class, Long.class, Double.class))); + + @Nullable + @Override + @SuppressWarnings("unchecked") + public List getScalarList(String name, Class 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) + ((List) 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 getStructuredList(String name) { + return listEntries.get(name); + } + + @Override + public Set getPropertyKeys() { + Set 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(); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricReaderFactoryTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricReaderFactoryTest.java index 64da70549a1..3bd03e33c8d 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricReaderFactoryTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricReaderFactoryTest.java @@ -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.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.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import org.mockito.ArgumentCaptor; class MetricReaderFactoryTest { @@ -141,14 +136,8 @@ void create_PullPrometheusDefault() throws IOException { cleanup.addCloseables(closeables); assertThat(reader.toString()).isEqualTo(expectedReader.toString()); - - ArgumentCaptor 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 @@ void create_PullPrometheusConfigured() throws IOException { cleanup.addCloseables(closeables); assertThat(reader.toString()).isEqualTo(expectedReader.toString()); - - ArgumentCaptor 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 diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/YamlStructuredConfigPropertiesTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/YamlStructuredConfigPropertiesTest.java new file mode 100644 index 00000000000..5c4f3d71333 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/YamlStructuredConfigPropertiesTest.java @@ -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 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(); + } +}