From af278ba4f43337231e3551b7923d6f5fd0034dc5 Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Thu, 12 Oct 2023 14:33:02 -0500 Subject: [PATCH 1/2] Add ExtendedConfigProperties to access complex types from file configuration --- .../spi/internal/DefaultConfigProperties.java | 2 +- .../internal/ExtendedConfigProperties.java | 45 +++ ...AutoConfiguredOpenTelemetrySdkBuilder.java | 17 +- .../autoconfigure/FileConfigurationTest.java | 32 +- .../fileconfig/ConfigurationFactory.java | 55 +++- .../fileconfig/ConfigurationReader.java | 32 -- .../fileconfig/FileConfigProperties.java | 274 ++++++++++++++++++ ...ava => ConfigurationFactoryParseTest.java} | 10 +- .../fileconfig/FileConfigPropertiesTest.java | 226 +++++++++++++++ 9 files changed, 643 insertions(+), 50 deletions(-) create mode 100644 sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/ExtendedConfigProperties.java delete mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigurationReader.java create mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfigProperties.java rename sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/{ConfigurationReaderTest.java => ConfigurationFactoryParseTest.java} (98%) create mode 100644 sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfigPropertiesTest.java diff --git a/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/DefaultConfigProperties.java b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/DefaultConfigProperties.java index b8673993fe3..0bd9794fc14 100644 --- a/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/DefaultConfigProperties.java +++ b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/DefaultConfigProperties.java @@ -35,7 +35,7 @@ *

This class is internal and is hence not for public use. Its APIs are unstable and can change * at any time. */ -public final class DefaultConfigProperties implements ConfigProperties { +public final class DefaultConfigProperties implements ExtendedConfigProperties { private final Map config; diff --git a/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/ExtendedConfigProperties.java b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/ExtendedConfigProperties.java new file mode 100644 index 00000000000..2e0481c136e --- /dev/null +++ b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/ExtendedConfigProperties.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.autoconfigure.spi.ConfigProperties; +import java.util.List; +import javax.annotation.Nullable; + +/** + * An extended version of {@link ConfigProperties} that supports accessing complex types - nested + * maps and arrays of maps. {@link ExtendedConfigProperties} is used as a representation of a map, + * since it has (type safe) accessors for string keys. + */ +public interface ExtendedConfigProperties extends ConfigProperties { + + /** + * Returns a map-valued configuration property, represented as {@link ExtendedConfigProperties}. + * + * @return a map-valued configuration property, or {@code null} if {@code name} has not been + * configured. + * @throws io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException if the property is not a + * map + */ + @Nullable + default ExtendedConfigProperties getConfigProperties(String name) { + return null; + } + + /** + * Returns a list of map-valued configuration property, represented as {@link + * ExtendedConfigProperties}. + * + * @return a list of map-valued configuration property, or {@code null} if {@code name} has not + * been configured. + * @throws io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException if the property is not a + * list of maps + */ + @Nullable + default List getListConfigProperties(String name) { + return null; + } +} 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 7a89123ef65..4b9ec2853a2 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 @@ -19,6 +19,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.ExtendedConfigProperties; import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; import io.opentelemetry.sdk.logs.export.LogRecordExporter; @@ -441,12 +442,20 @@ private static AutoConfiguredOpenTelemetrySdk maybeConfigureFromFile(ConfigPrope try { Class configurationFactory = Class.forName("io.opentelemetry.sdk.extension.incubator.fileconfig.ConfigurationFactory"); - Method parseAndInterpret = - configurationFactory.getMethod("parseAndInterpret", InputStream.class); - OpenTelemetrySdk sdk = (OpenTelemetrySdk) parseAndInterpret.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 interpret = configurationFactory.getMethod("interpret", openTelemetryConfiguration); + OpenTelemetrySdk sdk = (OpenTelemetrySdk) interpret.invoke(null, model); + Method toConfigProperties = + configurationFactory.getMethod("toConfigProperties", openTelemetryConfiguration); + ExtendedConfigProperties configProperties = + (ExtendedConfigProperties) 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(), configProperties); } 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/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/FileConfigurationTest.java b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/FileConfigurationTest.java index dd11494958f..55b2416c753 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 @@ -28,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.ExtendedConfigProperties; import io.opentelemetry.sdk.logs.internal.SdkEventEmitterProvider; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; @@ -44,7 +45,7 @@ class FileConfigurationTest { - @RegisterExtension private static final CleanupExtension cleanup = new CleanupExtension(); + @RegisterExtension static final CleanupExtension cleanup = new CleanupExtension(); @TempDir private Path tempDir; private Path configFilePath; @@ -60,7 +61,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(); @@ -175,4 +180,27 @@ void configFile_Error(@TempDir Path tempDir) throws IOException { .isInstanceOf(ConfigurationException.class) .hasMessage("Unrecognized span exporter(s): [foo]"); } + + @Test + void configFile_ExtendedConfigProperties() { + ConfigProperties config = + DefaultConfigProperties.createFromMap( + Collections.singletonMap("OTEL_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 + ConfigProperties config1 = autoConfiguredOpenTelemetrySdk.getConfig(); + assertThat(config1).isInstanceOf(ExtendedConfigProperties.class); + ExtendedConfigProperties extendedConfigProps = (ExtendedConfigProperties) config1; + ExtendedConfigProperties otherProps = extendedConfigProps.getConfigProperties("other"); + assertThat(otherProps).isNotNull(); + assertThat(otherProps.getString("str_key")).isEqualTo("str_value"); + ExtendedConfigProperties otherMapKeyProps = otherProps.getConfigProperties("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/ConfigurationFactory.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigurationFactory.java index 1e0dddf3074..dcd7a79d496 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigurationFactory.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigurationFactory.java @@ -5,16 +5,24 @@ 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.ExtendedConfigProperties; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfiguration; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.logging.Logger; +import org.snakeyaml.engine.v2.api.Load; +import org.snakeyaml.engine.v2.api.LoadSettings; /** * Parses YAML configuration files conforming to the schema in closeables = new ArrayList<>(); try { return OpenTelemetryConfigurationFactory.getInstance() @@ -67,4 +96,18 @@ public static OpenTelemetrySdk parseAndInterpret(InputStream inputStream) { throw new ConfigurationException("Unexpected configuration error", e); } } + + /** + * Convert the {@code model} to a generic {@link ExtendedConfigProperties}, which can be used to + * read configuration not part of the model. + * + * @param model the configuration model + * @return a generic {@link ExtendedConfigProperties} representation of the model + */ + public static ExtendedConfigProperties toConfigProperties(OpenTelemetryConfiguration model) { + Map configurationMap = + ConfigurationFactory.MAPPER.convertValue( + model, new TypeReference>() {}); + return new FileConfigProperties(configurationMap); + } } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigurationReader.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigurationReader.java deleted file mode 100644 index 4fe765ee344..00000000000 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigurationReader.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.fileconfig; - -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfiguration; -import java.io.InputStream; -import org.snakeyaml.engine.v2.api.Load; -import org.snakeyaml.engine.v2.api.LoadSettings; - -final class ConfigurationReader { - - private static final ObjectMapper MAPPER = - new ObjectMapper() - // Create empty object instances for keys which are present but have null values - .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)); - - private ConfigurationReader() {} - - /** Parse the {@code configuration} YAML and return the {@link OpenTelemetryConfiguration}. */ - static OpenTelemetryConfiguration parse(InputStream configuration) { - LoadSettings settings = LoadSettings.builder().build(); - Load yaml = new Load(settings); - Object yamlObj = yaml.loadFromInputStream(configuration); - return MAPPER.convertValue(yamlObj, OpenTelemetryConfiguration.class); - } -} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfigProperties.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfigProperties.java new file mode 100644 index 00000000000..bd9272eea46 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfigProperties.java @@ -0,0 +1,274 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.fileconfig; + +import static java.util.stream.Collectors.toList; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ExtendedConfigProperties; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfiguration; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; +import java.util.function.BiFunction; +import javax.annotation.Nullable; + +/** + * Implementation of {@link ExtendedConfigProperties} which uses the file {@link + * OpenTelemetryConfiguration} model as a source. + * + * @see #getConfigProperties(String) Accessing nested maps + * @see #getListConfigProperties(String) Accessing lists of maps + * @see ConfigurationFactory#toConfigProperties(OpenTelemetryConfiguration) Converting configuration + * model to properties + */ +final class FileConfigProperties implements ExtendedConfigProperties { + + /** + * Values are {@link #isPrimitive(Object)}, {@code List}, or {@code Map}. + */ + private final Map simpleEntries = new HashMap<>(); + + private final Map> listEntries = new HashMap<>(); + private final Map mapEntries = new HashMap<>(); + + @SuppressWarnings("unchecked") + FileConfigProperties(Map properties) { + 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, ((List) value).stream().map(String::valueOf).collect(toList())); + continue; + } + if (isListOfMaps(value)) { + List list = + ((List>) value) + .stream().map(FileConfigProperties::new).collect(toList()); + listEntries.put(key, list); + continue; + } + if (isMap(value)) { + FileConfigProperties configProperties = + new FileConfigProperties((Map) value); + mapEntries.put(key, configProperties); + continue; + } + throw new ConfigurationException( + "Unable to initialize ExtendedConfigProperties. Key \"" + + key + + "\" has unrecognized object type " + + value.getClass().getName()); + } + } + + private static boolean isPrimitiveList(Object object) { + if (object instanceof List) { + List list = (List) object; + return list.stream().allMatch(FileConfigProperties::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) { + Object value = simpleEntries.get(name); + if (value == null && hasDotNotation(name)) { + return applyToChild(name, ExtendedConfigProperties::getString); + } + if (value instanceof String) { + return (String) value; + } + return null; + } + + @Nullable + @Override + public Boolean getBoolean(String name) { + Object value = simpleEntries.get(name); + if (value == null && hasDotNotation(name)) { + return applyToChild(name, ExtendedConfigProperties::getBoolean); + } + if (value instanceof Boolean) { + return (Boolean) value; + } + return null; + } + + @Nullable + @Override + public Integer getInt(String name) { + Object value = simpleEntries.get(name); + if (value == null && hasDotNotation(name)) { + return applyToChild(name, ExtendedConfigProperties::getInt); + } + if (value instanceof Integer) { + return (Integer) value; + } + return null; + } + + @Nullable + @Override + public Long getLong(String name) { + Object value = simpleEntries.get(name); + if (value == null && hasDotNotation(name)) { + return applyToChild(name, ExtendedConfigProperties::getLong); + } + if (value instanceof Integer) { + return ((Integer) value).longValue(); + } + if (value instanceof Long) { + return (Long) value; + } + return null; + } + + @Nullable + @Override + public Double getDouble(String name) { + Object value = simpleEntries.get(name); + if (value == null && hasDotNotation(name)) { + return applyToChild(name, ExtendedConfigProperties::getDouble); + } + if (value instanceof Float) { + return ((Float) value).doubleValue(); + } + if (value instanceof Double) { + return (Double) value; + } + return null; + } + + @Nullable + @Override + public Duration getDuration(String name) { + // Note: unlike DefaultConfigProperties, all FileConfigProperties durations are integers / longs + // interpreted as millis. The unit suffixes (ms, s, m, h, d) are not supported. + Long millis = getLong(name); + if (millis == null) { + return null; + } + return Duration.ofMillis(millis); + } + + @Override + @SuppressWarnings({"unchecked", "NullAway"}) + public List getList(String name) { + Object value = simpleEntries.get(name); + if (value == null && hasDotNotation(name)) { + List result = applyToChild(name, ExtendedConfigProperties::getList); + if (result != null) { + return result; + } + return Collections.emptyList(); + } + if (value instanceof List) { + return (List) value; + } + return Collections.emptyList(); + } + + @Override + public Map getMap(String name) { + // NOTE: we can return more generic ExtendedConfigProperties map entries, but not a simpler + // Map version. So we return an empty map rather than throwing or trying to + // downgrade ExtendedConfigProperties to Map + return Collections.emptyMap(); + } + + @Nullable + @Override + public ExtendedConfigProperties getConfigProperties(String name) { + ExtendedConfigProperties value = getConfigPropertiesInternal(name); + if (value == null && hasDotNotation(name)) { + return applyToChild(name, ExtendedConfigProperties::getConfigProperties); + } + return value; + } + + @Nullable + @Override + public List getListConfigProperties(String name) { + List value = listEntries.get(name); + if (value == null && hasDotNotation(name)) { + return applyToChild(name, ExtendedConfigProperties::getListConfigProperties); + } + return value; + } + + @Nullable + private ExtendedConfigProperties getConfigPropertiesInternal(String name) { + return mapEntries.get(name); + } + + private static boolean hasDotNotation(String name) { + return name.contains("."); + } + + /** Should only be called after {@link #hasDotNotation(String)} returns {@code true}. */ + @Nullable + private T applyToChild( + String name, BiFunction function) { + String[] parts = name.split("\\.", 2); + if (parts.length != 2) { + throw new IllegalStateException( + "applyToChild called without hasDotNotation. This is a programming error."); + } + ExtendedConfigProperties childProps = getConfigPropertiesInternal(parts[0]); + if (childProps == null) { + return null; + } + return function.apply(childProps, parts[1]); + } + + @Override + public String toString() { + StringJoiner joiner = new StringJoiner(", ", "FileConfigProperties{", "}"); + 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/ConfigurationReaderTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigurationFactoryParseTest.java similarity index 98% rename from sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigurationReaderTest.java rename to sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigurationFactoryParseTest.java index afdb4cac33e..a5a8c5eff19 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigurationReaderTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigurationFactoryParseTest.java @@ -53,10 +53,10 @@ import java.util.Collections; import org.junit.jupiter.api.Test; -class ConfigurationReaderTest { +class ConfigurationFactoryParseTest { @Test - void read_KitchenSinkExampleFile() throws IOException { + void parse_KitchenSinkExampleFile() throws IOException { OpenTelemetryConfiguration expected = new OpenTelemetryConfiguration(); expected.withFileFormat("0.1"); @@ -250,7 +250,7 @@ void read_KitchenSinkExampleFile() throws IOException { try (FileInputStream configExampleFile = new FileInputStream(System.getenv("CONFIG_EXAMPLE_DIR") + "/kitchen-sink.yaml")) { - OpenTelemetryConfiguration config = ConfigurationReader.parse(configExampleFile); + OpenTelemetryConfiguration config = ConfigurationFactory.parse(configExampleFile); // General config assertThat(config.getFileFormat()).isEqualTo("0.1"); @@ -299,7 +299,7 @@ void nullValuesParsedToEmptyObjects() { + " aggregation:\n" + " drop: {}\n"; OpenTelemetryConfiguration objectPlaceholderModel = - ConfigurationReader.parse( + ConfigurationFactory.parse( new ByteArrayInputStream(objectPlaceholderString.getBytes(StandardCharsets.UTF_8))); String noOjbectPlaceholderString = @@ -317,7 +317,7 @@ void nullValuesParsedToEmptyObjects() { + " aggregation:\n" + " drop:\n"; OpenTelemetryConfiguration noObjectPlaceholderModel = - ConfigurationReader.parse( + ConfigurationFactory.parse( new ByteArrayInputStream(noOjbectPlaceholderString.getBytes(StandardCharsets.UTF_8))); SpanExporter exporter = diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfigPropertiesTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfigPropertiesTest.java new file mode 100644 index 00000000000..2acd5d84f49 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfigPropertiesTest.java @@ -0,0 +1,226 @@ +/* + * 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 io.opentelemetry.sdk.autoconfigure.spi.internal.ExtendedConfigProperties; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfiguration; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +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 FileConfigPropertiesTest { + + private static final String extendedSchema = + "file_format: \"0.1\"\n" + + "disabled: false\n" + + "\n" + + "resource:\n" + + " attributes:\n" + + " service.name: !!str \"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" + + " 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 ExtendedConfigProperties extendedConfigProps; + + @BeforeEach + void setup() { + OpenTelemetryConfiguration configuration = + ConfigurationFactory.parse( + new ByteArrayInputStream(extendedSchema.getBytes(StandardCharsets.UTF_8))); + extendedConfigProps = ConfigurationFactory.toConfigProperties(configuration); + } + + @Test + void configurationSchema() { + // Validate can read file configuration schema properties + assertThat(extendedConfigProps.getString("file_format")).isEqualTo("0.1"); + ExtendedConfigProperties resourceProps = extendedConfigProps.getConfigProperties("resource"); + assertThat(resourceProps).isNotNull(); + ExtendedConfigProperties resourceAttributesProps = + resourceProps.getConfigProperties("attributes"); + assertThat(resourceAttributesProps).isNotNull(); + assertThat(resourceAttributesProps.getString("service.name")).isEqualTo("unknown_service"); + } + + @Test + void additionalProperties() { + // Validate can read properties not part of configuration schema + // .other + ExtendedConfigProperties otherProps = extendedConfigProps.getConfigProperties("other"); + assertThat(otherProps).isNotNull(); + 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.getDuration("int_key")).isEqualTo(Duration.ofMillis(1)); + assertThat(otherProps.getList("str_list_key")).isEqualTo(Arrays.asList("val1", "val2")); + assertThat(otherProps.getList("int_list_key")).isEqualTo(Arrays.asList("1", "2")); + assertThat(otherProps.getList("float_list_key")).isEqualTo(Arrays.asList("1.1", "2.2")); + assertThat(otherProps.getList("bool_list_key")).isEqualTo(Arrays.asList("true", "false")); + + // .other.map_key + ExtendedConfigProperties otherMapKeyProps = otherProps.getConfigProperties("map_key"); + assertThat(otherMapKeyProps).isNotNull(); + assertThat(otherMapKeyProps.getString("str_key1")).isEqualTo("str_value1"); + assertThat(otherMapKeyProps.getInt("int_key1")).isEqualTo(2); + // other.map_key.map_key1 + ExtendedConfigProperties otherMapKeyMapKey1Props = + otherMapKeyProps.getConfigProperties("map_key1"); + assertThat(otherMapKeyMapKey1Props).isNotNull(); + assertThat(otherMapKeyMapKey1Props.getString("str_key2")).isEqualTo("str_value2"); + assertThat(otherMapKeyMapKey1Props.getInt("int_key2")).isEqualTo(3); + + // .other.list_key + List listKey = otherProps.getListConfigProperties("list_key"); + assertThat(listKey).hasSize(2); + ExtendedConfigProperties listKeyProps1 = listKey.get(0); + assertThat(listKeyProps1.getString("str_key1")).isEqualTo("str_value1"); + assertThat(listKeyProps1.getInt("int_key1")).isEqualTo(2); + // .other.list_key[0] + ExtendedConfigProperties listKeyProps1MapKeyProps = + listKeyProps1.getConfigProperties("map_key1"); + assertThat(listKeyProps1MapKeyProps).isNotNull(); + assertThat(listKeyProps1MapKeyProps.getString("str_key2")).isEqualTo("str_value2"); + assertThat(listKeyProps1MapKeyProps.getInt("int_key2")).isEqualTo(3); + // .other.list_key[1] + ExtendedConfigProperties listKeyProps2 = listKey.get(0); + assertThat(listKeyProps2.getString("str_key1")).isEqualTo("str_value1"); + assertThat(listKeyProps2.getInt("int_key1")).isEqualTo(2); + } + + @Test + void dotNotation() { + assertThat(extendedConfigProps.getString("other.str_key")).isEqualTo("str_value"); + assertThat(extendedConfigProps.getInt("other.int_key")).isEqualTo(1); + assertThat(extendedConfigProps.getDouble("other.float_key")).isEqualTo(1.1); + assertThat(extendedConfigProps.getBoolean("other.bool_key")).isEqualTo(true); + assertThat(extendedConfigProps.getDuration("other.int_key")).isEqualTo(Duration.ofMillis(1)); + assertThat(extendedConfigProps.getList("other.str_list_key")) + .isEqualTo(Arrays.asList("val1", "val2")); + assertThat(extendedConfigProps.getList("other.int_list_key")) + .isEqualTo(Arrays.asList("1", "2")); + assertThat(extendedConfigProps.getList("other.float_list_key")) + .isEqualTo(Arrays.asList("1.1", "2.2")); + assertThat(extendedConfigProps.getList("other.bool_list_key")) + .isEqualTo(Arrays.asList("true", "false")); + assertThat(extendedConfigProps.getString("other.map_key.str_key1")).isEqualTo("str_value1"); + assertThat(extendedConfigProps.getInt("other.map_key.int_key1")).isEqualTo(2); + assertThat(extendedConfigProps.getString("other.map_key.map_key1.str_key2")) + .isEqualTo("str_value2"); + assertThat(extendedConfigProps.getInt("other.map_key.map_key1.int_key2")).isEqualTo(3); + List fileConfigProperties = + extendedConfigProps.getListConfigProperties("other.list_key"); + assertThat(fileConfigProperties).isNotNull().hasSize(2); + ExtendedConfigProperties otherMapKeyProps = + extendedConfigProps.getConfigProperties("other.map_key"); + assertThat(otherMapKeyProps).isNotNull(); + assertThat(otherMapKeyProps.getString("str_key1")).isEqualTo("str_value1"); + assertThat(otherMapKeyProps.getInt("int_key1")).isEqualTo(2); + assertThat(otherMapKeyProps.getConfigProperties("map_key1")).isNotNull(); + } + + @Test + void defaults() { + assertThat(extendedConfigProps.getString("foo", "bar")).isEqualTo("bar"); + assertThat(extendedConfigProps.getInt("foo", 1)).isEqualTo(1); + assertThat(extendedConfigProps.getLong("foo", 1)).isEqualTo(1); + assertThat(extendedConfigProps.getDouble("foo", 1.1)).isEqualTo(1.1); + assertThat(extendedConfigProps.getBoolean("foo", true)).isTrue(); + assertThat(extendedConfigProps.getDuration("foo", Duration.ofMillis(1))) + .isEqualTo(Duration.ofMillis(1)); + assertThat(extendedConfigProps.getList("foo", Collections.singletonList("bar"))) + .isEqualTo(Collections.singletonList("bar")); + + // Dot notation + assertThat(extendedConfigProps.getString("foo.bar", "baz")).isEqualTo("baz"); + assertThat(extendedConfigProps.getInt("foo.bar", 1)).isEqualTo(1); + assertThat(extendedConfigProps.getLong("foo.bar", 1)).isEqualTo(1); + assertThat(extendedConfigProps.getDouble("foo.bar", 1.1)).isEqualTo(1.1); + assertThat(extendedConfigProps.getBoolean("foo.bar", true)).isTrue(); + assertThat(extendedConfigProps.getDuration("foo.bar", Duration.ofMillis(1))) + .isEqualTo(Duration.ofMillis(1)); + assertThat(extendedConfigProps.getList("foo.bar", Collections.singletonList("baz"))) + .isEqualTo(Collections.singletonList("baz")); + } + + @Test + void missingKeys() { + assertThat(extendedConfigProps.getString("foo")).isNull(); + assertThat(extendedConfigProps.getInt("foo")).isNull(); + assertThat(extendedConfigProps.getLong("foo")).isNull(); + assertThat(extendedConfigProps.getDouble("foo")).isNull(); + assertThat(extendedConfigProps.getBoolean("foo")).isNull(); + assertThat(extendedConfigProps.getDuration("foo")).isNull(); + assertThat(extendedConfigProps.getList("foo")).isEmpty(); + assertThat(extendedConfigProps.getConfigProperties("foo")).isNull(); + assertThat(extendedConfigProps.getListConfigProperties("foo")).isNull(); + + // Dot notation + assertThat(extendedConfigProps.getString("other.missing_key")).isNull(); + assertThat(extendedConfigProps.getString("other.map_key.missing_key")).isNull(); + } + + @Test + void wrongType() { + ExtendedConfigProperties otherProps = extendedConfigProps.getConfigProperties("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.getList("str_key")).isEmpty(); + assertThat(otherProps.getConfigProperties("str_key")).isNull(); + assertThat(otherProps.getListConfigProperties("str_key")).isNull(); + + // Dot notation + assertThat(extendedConfigProps.getString("other.int_key")).isNull(); + assertThat(extendedConfigProps.getInt("other.str_key")).isNull(); + assertThat(extendedConfigProps.getLong("other.str_key")).isNull(); + assertThat(extendedConfigProps.getDouble("other.str_key")).isNull(); + assertThat(extendedConfigProps.getBoolean("other.str_key")).isNull(); + assertThat(extendedConfigProps.getList("other.str_key")).isEmpty(); + assertThat(extendedConfigProps.getConfigProperties("other.str_key")).isNull(); + assertThat(extendedConfigProps.getListConfigProperties("other.str_key")).isNull(); + } + + @Test + void unsupportedOperations() { + assertThat(extendedConfigProps.getMap("foo")).isEqualTo(Collections.emptyMap()); + assertThat(extendedConfigProps.getMap("foo", Collections.emptyMap())) + .isEqualTo(Collections.emptyMap()); + } +} From 9d00503af6ebe529c26ee78f9c593291bd5425d0 Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Thu, 12 Oct 2023 14:38:49 -0500 Subject: [PATCH 2/2] Add getConfigProperties implementation to DefaultConfigProperties --- .../spi/internal/DefaultConfigProperties.java | 10 +++++++ .../spi/internal/ConfigPropertiesTest.java | 28 +++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/DefaultConfigProperties.java b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/DefaultConfigProperties.java index 0bd9794fc14..2bac61f1436 100644 --- a/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/DefaultConfigProperties.java +++ b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/DefaultConfigProperties.java @@ -232,6 +232,16 @@ public Map getMap(String name) { Map.Entry::getKey, Map.Entry::getValue, (first, next) -> next, LinkedHashMap::new)); } + @Nullable + @Override + public ExtendedConfigProperties getConfigProperties(String name) { + Map mapProperties = getMap(name); + if (mapProperties.isEmpty()) { + return null; + } + return DefaultConfigProperties.createFromMap(mapProperties); + } + /** * Return a new {@link DefaultConfigProperties} by overriding the {@code previousProperties} with * the {@code overrides}. diff --git a/sdk-extensions/autoconfigure-spi/src/test/java/io/opentelemetry/sdk/autoconfigure/spi/internal/ConfigPropertiesTest.java b/sdk-extensions/autoconfigure-spi/src/test/java/io/opentelemetry/sdk/autoconfigure/spi/internal/ConfigPropertiesTest.java index e0fe198dc9e..0c9ee5680ff 100644 --- a/sdk-extensions/autoconfigure-spi/src/test/java/io/opentelemetry/sdk/autoconfigure/spi/internal/ConfigPropertiesTest.java +++ b/sdk-extensions/autoconfigure-spi/src/test/java/io/opentelemetry/sdk/autoconfigure/spi/internal/ConfigPropertiesTest.java @@ -26,14 +26,24 @@ class ConfigPropertiesTest { void allValid() { Map properties = makeTestProps(); - ConfigProperties config = DefaultConfigProperties.createFromMap(properties); + DefaultConfigProperties config = DefaultConfigProperties.createFromMap(properties); assertThat(config.getString("test.string")).isEqualTo("str"); assertThat(config.getInt("test.int")).isEqualTo(10); assertThat(config.getLong("test.long")).isEqualTo(20); assertThat(config.getDouble("test.double")).isEqualTo(5.4); assertThat(config.getList("test.list")).containsExactly("cat", "dog", "bear"); assertThat(config.getMap("test.map")) - .containsExactly(entry("cat", "meow"), entry("dog", "bark"), entry("bear", "growl")); + .containsExactly( + entry("cat", "meow"), + entry("dog", "bark"), + entry("bear", "growl"), + entry("number", "1")); + ExtendedConfigProperties testMapConfig = config.getConfigProperties("test.map"); + assertThat(testMapConfig.getString("cat")).isEqualTo("meow"); + assertThat(testMapConfig.getString("dog")).isEqualTo("bark"); + assertThat(testMapConfig.getString("bear")).isEqualTo("growl"); + assertThat(testMapConfig.getString("number")).isEqualTo("1"); + assertThat(testMapConfig.getInt("number")).isEqualTo(1); assertThat(config.getDuration("test.duration")).isEqualTo(Duration.ofSeconds(1)); } @@ -48,19 +58,24 @@ void allValidUsingHyphens() { assertThat(config.getDouble("test-double")).isEqualTo(5.4); assertThat(config.getList("test-list")).containsExactly("cat", "dog", "bear"); assertThat(config.getMap("test-map")) - .containsExactly(entry("cat", "meow"), entry("dog", "bark"), entry("bear", "growl")); + .containsExactly( + entry("cat", "meow"), + entry("dog", "bark"), + entry("bear", "growl"), + entry("number", "1")); assertThat(config.getDuration("test-duration")).isEqualTo(Duration.ofSeconds(1)); } @Test void allMissing() { - ConfigProperties config = DefaultConfigProperties.createFromMap(emptyMap()); + DefaultConfigProperties config = DefaultConfigProperties.createFromMap(emptyMap()); assertThat(config.getString("test.string")).isNull(); assertThat(config.getInt("test.int")).isNull(); assertThat(config.getLong("test.long")).isNull(); assertThat(config.getDouble("test.double")).isNull(); assertThat(config.getList("test.list")).isEmpty(); assertThat(config.getMap("test.map")).isEmpty(); + assertThat(config.getConfigProperties("test.map")).isNull(); assertThat(config.getDuration("test.duration")).isNull(); } @@ -75,13 +90,14 @@ void allEmpty() { properties.put("test.map", ""); properties.put("test.duration", ""); - ConfigProperties config = DefaultConfigProperties.createFromMap(properties); + DefaultConfigProperties config = DefaultConfigProperties.createFromMap(properties); assertThat(config.getString("test.string")).isEmpty(); assertThat(config.getInt("test.int")).isNull(); assertThat(config.getLong("test.long")).isNull(); assertThat(config.getDouble("test.double")).isNull(); assertThat(config.getList("test.list")).isEmpty(); assertThat(config.getMap("test.map")).isEmpty(); + assertThat(config.getConfigProperties("test.map")).isNull(); assertThat(config.getDuration("test.duration")).isNull(); } @@ -275,7 +291,7 @@ private static Map makeTestProps() { properties.put("test.double", "5.4"); properties.put("test.boolean", "true"); properties.put("test.list", "cat,dog,bear"); - properties.put("test.map", "cat=meow,dog=bark,bear=growl,bird="); + properties.put("test.map", "cat=meow,dog=bark,bear=growl,bird=,number=1"); properties.put("test.duration", "1s"); return properties; }