Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce ConfigProvider API #6549

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions api/incubator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ dependencies {

annotationProcessor("com.google.auto.value:auto-value")

// To use parsed config file as input for YamlStructuredConfigPropertiesTest
testImplementation(project(":sdk-extensions:incubator"))
// TODO (jack-berg): Why is this dependency not brought in as transitive dependency of :sdk-extensions:incubator?
testImplementation("com.fasterxml.jackson.core:jackson-databind")

testImplementation(project(":sdk:testing"))

testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.incubator.config;

import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;

/**
* A registry for accessing configuration.
*
* <p>The name <i>Provider</i> is for consistency with other languages and it is <b>NOT</b> loaded
* using reflection.
*
* <p>See {@link InstrumentationConfigUtil} for convenience methods for extracting config from
* {@link ConfigProvider}.
*/
@ThreadSafe
public interface ConfigProvider {

/**
* Returns the {@link StructuredConfigProperties} corresponding to <a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to say anything about the return value at all? such as that it can be cached or shouldn't be?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know enough about the domain yet. There are definitely use cases for dynamic configuration, where an SDK would connect to a remote config server via opamp, and periodically receiver commands to update the config. But a lot needs to be built to enable that, including an opamp client implementation in java, and SDK APIs for updating SdkMeterProvider, SdkLoggerProvider, SdkTracerProvder which are currently all statically configured.

Then there's the question of what types of dynamic config scenarios even make sense for instrumentation. If the otel java agent makes decisions to install or omit bytecode instrumentation based on ConfigProvider, it won't always be possible to go install later if the config were to change. Other instrumentation options may be better fits for dynamic config, but then there's the question of how often to check for updates since we probably don't want to be looking for / applying config updates on the hot path.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not to anchor things too much, but certain dynamic elements may be better feature driven anyway. For example, recently in the zipkin java stuff there were requests for the sender (exporter here) to have dynamic endpoints (supplier function), provided by eureka etc. So, this is a code change as basically deferring endpoint resolution isn't default in anything. After that, there's what is the initial value as well. Point being is indeed components need to change to be dynamic, but specifically what they are dynamic about may help flex the api. Meanwhile basic impl that's easy to reason with could be a step forward.

* href="https://github.com/open-telemetry/opentelemetry-configuration/blob/main/schema/instrumentation.json">instrumentation
* config</a>, or {@code null} if file configuration is not used.
jack-berg marked this conversation as resolved.
Show resolved Hide resolved
*
* @return the instrumentation {@link StructuredConfigProperties}
*/
@Nullable
StructuredConfigProperties getInstrumentationConfig();

/** Returns a no-op {@link ConfigProvider}. */
static ConfigProvider noop() {
jack-berg marked this conversation as resolved.
Show resolved Hide resolved
return () -> null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.incubator.config;

import io.opentelemetry.api.GlobalOpenTelemetry;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;

/**
* This class provides a temporary global accessor for {@link ConfigProvider} until the config API
* is marked stable. It will eventually be merged into {@link GlobalOpenTelemetry}.
*/
// We intentionally assign to be used for error reporting.
@SuppressWarnings("StaticAssignmentOfThrowable")
public final class GlobalConfigProvider {

private static final AtomicReference<ConfigProvider> instance =
new AtomicReference<>(ConfigProvider.noop());

@SuppressWarnings("NonFinalStaticField")
@Nullable
private static volatile Throwable setInstanceCaller;

private GlobalConfigProvider() {}

/** Returns the globally registered {@link ConfigProvider}. */
// instance cannot be set to null
@SuppressWarnings("NullAway")
public static ConfigProvider get() {
return instance.get();
}

/**
* Sets the global {@link ConfigProvider}. Future calls to {@link #get()} will return the provided
* {@link ConfigProvider} instance. This should be called once as early as possible in your
* application initialization logic.
jack-berg marked this conversation as resolved.
Show resolved Hide resolved
*/
public static void set(ConfigProvider configProvider) {
boolean changed = instance.compareAndSet(ConfigProvider.noop(), configProvider);
if (!changed && (configProvider != ConfigProvider.noop())) {
throw new IllegalStateException(
"GlobalConfigProvider.set has already been called. GlobalConfigProvider.set "
+ "must be called only once before any calls to GlobalConfigProvider.get. "
+ "Previous invocation set to cause of this exception.",
setInstanceCaller);
}
setInstanceCaller = new Throwable();
}

/**
* Unsets the global {@link ConfigProvider}. This is only meant to be used from tests which need
* to reconfigure {@link ConfigProvider}.
*/
public static void resetForTest() {
jack-berg marked this conversation as resolved.
Show resolved Hide resolved
instance.set(ConfigProvider.noop());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.incubator.config;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;

/**
* A collection of convenience methods to extract instrumentation config from {@link
* ConfigProvider#getInstrumentationConfig()}.
*/
public class InstrumentationConfigUtil {

// TODO (jack-berg): add helper function to access nested structures with dot notation

/**
* Return a map representation of the peer service map entries in {@code
* .instrumentation.general.peer.service_mapping}, or null if none is configured.
*/
@Nullable
public static Map<String, String> peerServiceMapping(ConfigProvider configProvider) {
Optional<List<StructuredConfigProperties>> optServiceMappingList =
Optional.ofNullable(configProvider.getInstrumentationConfig())
.map(instrumentationConfig -> instrumentationConfig.getStructured("general"))
.map(generalConfig -> generalConfig.getStructured("peer"))
.map(httpConfig -> httpConfig.getStructuredList("service_mapping"));
if (!optServiceMappingList.isPresent()) {
return null;
}
Map<String, String> serviceMapping = new HashMap<>();
jack-berg marked this conversation as resolved.
Show resolved Hide resolved
optServiceMappingList
.get()
.forEach(
entry -> {
String peer = entry.getString("peer");
String service = entry.getString("service");
if (peer != null && service != null) {
serviceMapping.put(peer, service);
}
});
return serviceMapping;
jack-berg marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Return {@code .instrumentation.general.http.client.request_captured_headers}, or null if none
* is configured.
*/
@Nullable
public static List<String> httpClientRequestCapturedHeaders(ConfigProvider configProvider) {
return Optional.ofNullable(configProvider.getInstrumentationConfig())
.map(instrumentationConfig -> instrumentationConfig.getStructured("general"))
.map(generalConfig -> generalConfig.getStructured("http"))
.map(httpConfig -> httpConfig.getStructured("client"))
.map(clientConfig -> clientConfig.getScalarList("request_captured_headers", String.class))
jack-berg marked this conversation as resolved.
Show resolved Hide resolved
.orElse(null);
}

/**
* Return {@code .instrumentation.general.http.client.response_captured_headers}, or null if none
* is configured.
*/
jack-berg marked this conversation as resolved.
Show resolved Hide resolved
@Nullable
public static List<String> httpClientResponseCapturedHeaders(ConfigProvider configProvider) {
return Optional.ofNullable(configProvider.getInstrumentationConfig())
.map(instrumentationConfig -> instrumentationConfig.getStructured("general"))
.map(generalConfig -> generalConfig.getStructured("http"))
.map(httpConfig -> httpConfig.getStructured("client"))
.map(clientConfig -> clientConfig.getScalarList("response_captured_headers", String.class))
.orElse(null);
}

/**
* Return {@code .instrumentation.general.http.server.request_captured_headers}, or null if none
* is configured.
*/
@Nullable
public static List<String> httpServerRequestCapturedHeaders(ConfigProvider configProvider) {
return Optional.ofNullable(configProvider.getInstrumentationConfig())
.map(instrumentationConfig -> instrumentationConfig.getStructured("general"))
.map(generalConfig -> generalConfig.getStructured("http"))
.map(httpConfig -> httpConfig.getStructured("server"))
.map(clientConfig -> clientConfig.getScalarList("request_captured_headers", String.class))
.orElse(null);
}

/**
* Return {@code .instrumentation.general.http.server.response_captured_headers}, or null if none
* is configured.
*/
@Nullable
public static List<String> httpSeverResponseCapturedHeaders(ConfigProvider configProvider) {
return Optional.ofNullable(configProvider.getInstrumentationConfig())
.map(instrumentationConfig -> instrumentationConfig.getStructured("general"))
.map(generalConfig -> generalConfig.getStructured("http"))
.map(httpConfig -> httpConfig.getStructured("server"))
.map(clientConfig -> clientConfig.getScalarList("response_captured_headers", String.class))
.orElse(null);
}

/** Return {@code .instrumentation.java.<instrumentationName>}, or null if none is configured. */
@Nullable
public static StructuredConfigProperties javaInstrumentationConfig(
ConfigProvider configProvider, String instrumentationName) {
return Optional.ofNullable(configProvider.getInstrumentationConfig())
.map(instrumentationConfig -> instrumentationConfig.getStructured("java"))
.map(generalConfig -> generalConfig.getStructured(instrumentationName))
.orElse(null);
}

private InstrumentationConfigUtil() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.incubator.config;

/** An exception that is thrown if the user-provided file configuration is invalid. */
public final class StructuredConfigException extends RuntimeException {

private static final long serialVersionUID = 3036584181551130522L;

/** Create a new configuration exception with specified {@code message} and without a cause. */
public StructuredConfigException(String message) {
super(message);
}

/** Create a new configuration exception with specified {@code message} and {@code cause}. */
public StructuredConfigException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.sdk.autoconfigure.spi.internal;
package io.opentelemetry.api.incubator.config;

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;
Expand All @@ -28,7 +26,7 @@ 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
* @throws StructuredConfigException if the property is not a valid scalar string
*/
@Nullable
String getString(String name);
Expand All @@ -38,7 +36,7 @@ public interface StructuredConfigProperties {
*
* @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
* @throws StructuredConfigException if the property is not a valid scalar string
*/
default String getString(String name, String defaultValue) {
return defaultIfNull(getString(name), defaultValue);
Expand All @@ -49,7 +47,7 @@ default String getString(String name, String defaultValue) {
* {@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
* @throws StructuredConfigException if the property is not a valid scalar boolean
*/
@Nullable
Boolean getBoolean(String name);
Expand All @@ -59,7 +57,7 @@ default String getString(String name, String defaultValue) {
*
* @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
* @throws StructuredConfigException if the property is not a valid scalar boolean
*/
default boolean getBoolean(String name, boolean defaultValue) {
return defaultIfNull(getBoolean(name), defaultValue);
Expand All @@ -72,7 +70,7 @@ default boolean getBoolean(String name, boolean defaultValue) {
* {@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
* @throws StructuredConfigException if the property is not a valid scalar integer
*/
@Nullable
Integer getInt(String name);
Expand All @@ -85,7 +83,7 @@ default boolean getBoolean(String name, boolean defaultValue) {
*
* @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
* @throws StructuredConfigException if the property is not a valid scalar integer
*/
default int getInt(String name, int defaultValue) {
return defaultIfNull(getInt(name), defaultValue);
Expand All @@ -95,7 +93,7 @@ default int getInt(String name, int 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
* @throws StructuredConfigException if the property is not a valid scalar long
*/
@Nullable
Long getLong(String name);
Expand All @@ -105,7 +103,7 @@ default int getInt(String name, int defaultValue) {
*
* @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
* @throws StructuredConfigException if the property is not a valid scalar long
*/
default long getLong(String name, long defaultValue) {
return defaultIfNull(getLong(name), defaultValue);
Expand All @@ -115,7 +113,7 @@ default long getLong(String name, long 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
* @throws StructuredConfigException if the property is not a valid scalar double
*/
@Nullable
Double getDouble(String name);
Expand All @@ -125,7 +123,7 @@ default long getLong(String name, long defaultValue) {
*
* @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
* @throws StructuredConfigException if the property is not a valid scalar double
*/
default double getDouble(String name, double defaultValue) {
return defaultIfNull(getDouble(name), defaultValue);
Expand All @@ -139,8 +137,8 @@ default double getDouble(String name, double defaultValue) {
* @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
* @throws StructuredConfigException 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);
Expand All @@ -149,10 +147,12 @@ default double getDouble(String name, double defaultValue) {
* Returns a {@link List} configuration property. Entries which are not strings are converted to
* their string representation.
*
* @see ConfigProperties#getList(String name)
* @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 {@code defaultValue} if a property with {@code
* name} has not been configured
* @throws ConfigurationException if the property is not a valid sequence of scalars
* @throws StructuredConfigException 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);
Expand All @@ -163,7 +163,7 @@ default <T> List<T> getScalarList(String name, Class<T> scalarType, List<T> defa
*
* @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
* @throws StructuredConfigException if the property is not a mapping
*/
@Nullable
StructuredConfigProperties getStructured(String name);
Expand All @@ -173,7 +173,7 @@ default <T> List<T> getScalarList(String name, Class<T> scalarType, List<T> defa
*
* @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
* @throws StructuredConfigException if the property is not a sequence of mappings
*/
@Nullable
List<StructuredConfigProperties> getStructuredList(String name);
Expand Down
Loading
Loading