Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.incubator.config;

/** Listener notified when declarative configuration changes. */
@FunctionalInterface
public interface ConfigChangeListener {

/**
* Called when the watched path changes.
*
* <p>{@code path} is the changed declarative configuration path, for example {@code
* .instrumentation/development.general.http} or {@code
* .instrumentation/development.java.methods}.
*
* <p>{@code newConfig} is never null. If the watched node is unset or cleared, {@code newConfig}
* is {@link DeclarativeConfigProperties#empty()}.
*
* @param path the declarative configuration path that changed
* @param newConfig the updated configuration for the changed path
*/
void onChange(String path, DeclarativeConfigProperties newConfig);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.incubator.config;

/** Registration handle returned by {@link ConfigProvider#addConfigChangeListener}. */
@FunctionalInterface
public interface ConfigChangeRegistration {

/**
* Unregister the listener associated with this registration.
*
* <p>Subsequent calls have no effect.
*/
void close();
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,60 @@ default DeclarativeConfigProperties getGeneralInstrumentationConfig() {
return getInstrumentationConfig().get("general");
}

/**
* Registers a {@link ConfigChangeListener} for changes to a specific declarative configuration
* path.
*
* <p>Example paths include {@code .instrumentation/development.general.http} and {@code
* .instrumentation/development.java.methods}.
*
* <p>When a watched path changes, {@link ConfigChangeListener#onChange(String,
* DeclarativeConfigProperties)} is invoked with the changed path and updated configuration for
* that path.
*
* <p>The default implementation performs no registration and returns a no-op handle.
*
* @param path the declarative configuration path to watch
* @param listener the listener to notify when the watched path changes
* @return a {@link ConfigChangeRegistration} that can be closed to unregister the listener
*/
default ConfigChangeRegistration addConfigChangeListener(
String path, ConfigChangeListener listener) {
return () -> {};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need for a default implementation. This is still experimental so we can add new methods without worrying about compatibility.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

True. But this default is a safe fallback, I added it for that reason rather than for compatibility. I'm not against removing and implementing it in the implementations, but it's a good default

}

/**
* Updates the declarative configuration subtree at the given path.
*
* <p>The path uses {@code .} as a separator (e.g., {@code
* ".instrumentation/development.java.myLib"}). The subtree at that path is replaced with {@code
* newSubtree}, and any registered {@link ConfigChangeListener}s watching affected paths are
* notified.
*
* <p>The default implementation is a no-op.
*
* @param path the declarative configuration path to update
* @param newSubtree the new configuration subtree to set at the path
*/
default void updateConfig(String path, DeclarativeConfigProperties newSubtree) {}

/**
* Sets a single scalar configuration property at the given path.
*
* <p>The path uses {@code .} as a separator (e.g., {@code
* ".instrumentation/development.java.myLib"}). The property identified by {@code key} within that
* path is set to {@code value}, and any registered {@link ConfigChangeListener}s watching
* affected paths are notified.
*
* <p>The default implementation is a no-op.
*
* @param path the declarative configuration path containing the property
* @param key the property key within the path
* @param value the new value for the property (must be a scalar: String, Boolean, Long, Double,
* Integer, or a List of scalars)
*/
default void setConfigProperty(String path, String key, Object value) {}

/** Returns a no-op {@link ConfigProvider}. */
static ConfigProvider noop() {
return DeclarativeConfigProperties::empty;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ static Map<String, Object> toMap(DeclarativeConfigProperties declarativeConfigPr
return DeclarativeConfigPropertyUtil.toMap(declarativeConfigProperties);
}

/**
* Create a {@link DeclarativeConfigProperties} from a {@code Map<String, Object>}.
*
* <p>This is the inverse of {@link #toMap(DeclarativeConfigProperties)}. Values in the map are
* expected to follow the same conventions: scalars, lists of scalars, nested maps, and lists of
* maps.
*
* @param map the map to wrap
* @param componentLoader the component loader to use
* @return a {@link DeclarativeConfigProperties} backed by the map
*/
static DeclarativeConfigProperties fromMap(
Map<String, Object> map, ComponentLoader componentLoader) {
return new MapBackedDeclarativeConfigProperties(map, componentLoader);
}

/**
* Returns a {@link String} configuration property.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.incubator.config;

import io.opentelemetry.common.ComponentLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
* A {@link DeclarativeConfigProperties} implementation backed by a {@code Map<String, Object>}.
*
* <p>This is the inverse of {@link DeclarativeConfigProperties#toMap(DeclarativeConfigProperties)}.
* Values in the map are expected to follow the same conventions as YAML parsing output: scalars
* (String, Boolean, Long, Double, Integer), lists of scalars, maps (structured children), and lists
* of maps (structured lists).
*/
final class MapBackedDeclarativeConfigProperties implements DeclarativeConfigProperties {

private final Map<String, Object> values;
private final ComponentLoader componentLoader;

MapBackedDeclarativeConfigProperties(
Map<String, Object> values, ComponentLoader componentLoader) {
this.values = values;
this.componentLoader = componentLoader;
}

@Nullable
@Override
public String getString(String name) {
Object value = values.get(name);
return value instanceof String ? (String) value : null;
}

@Nullable
@Override
public Boolean getBoolean(String name) {
Object value = values.get(name);
return value instanceof Boolean ? (Boolean) value : null;
}

@Nullable
@Override
public Integer getInt(String name) {
Object value = values.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) {
Object value = values.get(name);
if (value instanceof Long) {
return (Long) value;
}
if (value instanceof Integer) {
return ((Integer) value).longValue();
}
return null;
}

@Nullable
@Override
public Double getDouble(String name) {
Object value = values.get(name);
if (value instanceof Double) {
return (Double) value;
}
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
return null;
}

@SuppressWarnings("unchecked")
@Nullable
@Override
public <T> List<T> getScalarList(String name, Class<T> scalarType) {
Object value = values.get(name);
if (!(value instanceof List)) {
return null;
}
List<Object> raw = (List<Object>) value;
List<T> casted = new ArrayList<>(raw.size());
for (Object element : raw) {
if (!scalarType.isInstance(element)) {
return null;
}
casted.add(scalarType.cast(element));
}
return casted;
}

@SuppressWarnings("unchecked")
@Nullable
@Override
public DeclarativeConfigProperties getStructured(String name) {
Object value = values.get(name);
if (!(value instanceof Map)) {
return null;
}
return new MapBackedDeclarativeConfigProperties((Map<String, Object>) value, componentLoader);
}

@SuppressWarnings("unchecked")
@Nullable
@Override
public List<DeclarativeConfigProperties> getStructuredList(String name) {
Object value = values.get(name);
if (!(value instanceof List)) {
return null;
}
List<Object> raw = (List<Object>) value;
List<DeclarativeConfigProperties> result = new ArrayList<>(raw.size());
for (Object element : raw) {
if (!(element instanceof Map)) {
return null;
}
result.add(
new MapBackedDeclarativeConfigProperties((Map<String, Object>) element, componentLoader));
}
return result;
}

@Override
public Set<String> getPropertyKeys() {
return values.keySet();
}

@Override
public ComponentLoader getComponentLoader() {
return componentLoader;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
package io.opentelemetry.api.incubator;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;

import io.opentelemetry.api.incubator.config.ConfigChangeRegistration;
import io.opentelemetry.api.incubator.config.ConfigProvider;
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
import org.junit.jupiter.api.Test;

class ConfigProviderTest {
Expand All @@ -24,5 +27,23 @@ void instrumentationConfigFallback() {
assertThat(configProvider.getInstrumentationConfig()).isNotNull();
assertThat(configProvider.getInstrumentationConfig("servlet")).isNotNull();
assertThat(configProvider.getGeneralInstrumentationConfig()).isNotNull();
ConfigChangeRegistration listenerRegistration =
configProvider.addConfigChangeListener(
".instrumentation/development.java.servlet", (path, newConfig) -> {});
assertThatCode(listenerRegistration::close).doesNotThrowAnyException();
}

@Test
void defaultUpdateConfig_isNoop() {
ConfigProvider configProvider = ConfigProvider.noop();
assertThatCode(() -> configProvider.updateConfig(".foo", DeclarativeConfigProperties.empty()))
.doesNotThrowAnyException();
}

@Test
void defaultSetConfigProperty_isNoop() {
ConfigProvider configProvider = ConfigProvider.noop();
assertThatCode(() -> configProvider.setConfigProperty(".foo", "key", "value"))
.doesNotThrowAnyException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -111,6 +112,32 @@ void instrumentationConfig() {
.isEqualTo(Arrays.asList("client-request-header1", "client-request-header2"));
}

@Test
void close_shutsDownConfigProvider() {
String configYaml =
"instrumentation/development:\n"
+ " general:\n"
+ " http:\n"
+ " enabled: \"false\"";
SdkConfigProvider configProvider =
SdkConfigProvider.create(
DeclarativeConfiguration.toConfigProperties(
new ByteArrayInputStream(configYaml.getBytes(StandardCharsets.UTF_8))));
ExtendedOpenTelemetrySdk sdk =
ExtendedOpenTelemetrySdk.create(OpenTelemetrySdk.builder().build(), configProvider);

AtomicInteger callbackCount = new AtomicInteger();
configProvider.addConfigChangeListener(
".instrumentation/development.general.http",
(path, newConfig) -> callbackCount.incrementAndGet());

sdk.close();

configProvider.setConfigProperty(
".instrumentation/development.general.http", "enabled", "true");
assertThat(callbackCount.get()).isEqualTo(0);
}

@Test
void instrumentationConfigFallback() {
ConfigProvider configProvider = ConfigProvider.noop();
Expand Down
Loading
Loading