Support declarative configuration (#12265)

This commit is contained in:
jack-berg 2024-10-15 22:56:27 -05:00 committed by GitHub
parent d8eddfc726
commit 780cdf4a93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 329 additions and 16 deletions

View File

@ -15,7 +15,6 @@ import io.opentelemetry.instrumentation.jmx.engine.MetricConfiguration;
import io.opentelemetry.instrumentation.jmx.yaml.RuleParser;
import io.opentelemetry.javaagent.extension.AgentListener;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.io.InputStream;
import java.nio.file.Files;
@ -29,7 +28,7 @@ public class JmxMetricInsightInstaller implements AgentListener {
@Override
public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) {
ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredSdk);
ConfigProperties config = AgentListener.resolveConfigProperties(autoConfiguredSdk);
if (config.getBoolean("otel.jmx.enabled", true)) {
JmxMetricInsight service =

View File

@ -8,7 +8,6 @@ package io.opentelemetry.javaagent.instrumentation.oshi;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.AgentListener;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.lang.reflect.Method;
@ -21,7 +20,7 @@ public class OshiMetricsInstaller implements AgentListener {
@Override
public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) {
ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredSdk);
ConfigProperties config = AgentListener.resolveConfigProperties(autoConfiguredSdk);
boolean defaultEnabled = config.getBoolean("otel.instrumentation.common.default-enabled", true);
if (!config.getBoolean("otel.instrumentation.oshi.enabled", defaultEnabled)) {

View File

@ -12,7 +12,6 @@ import io.opentelemetry.instrumentation.runtimemetrics.java17.RuntimeMetrics;
import io.opentelemetry.instrumentation.runtimemetrics.java17.RuntimeMetricsBuilder;
import io.opentelemetry.javaagent.extension.AgentListener;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
/** An {@link AgentListener} that enables runtime metrics during agent startup. */
@ -21,7 +20,7 @@ public class Java17RuntimeMetricsInstaller implements AgentListener {
@Override
public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) {
ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredSdk);
ConfigProperties config = AgentListener.resolveConfigProperties(autoConfiguredSdk);
OpenTelemetry openTelemetry = GlobalOpenTelemetry.get();
RuntimeMetricsBuilder builder = null;

View File

@ -7,9 +7,9 @@ package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.bootstrap.InstrumentationHolder;
import io.opentelemetry.javaagent.extension.AgentListener;
import io.opentelemetry.javaagent.tooling.BeforeAgentListener;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.lang.instrument.Instrumentation;
@ -19,7 +19,8 @@ public class JarAnalyzerInstaller implements BeforeAgentListener {
@Override
public void beforeAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) {
ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredOpenTelemetrySdk);
ConfigProperties config = AgentListener.resolveConfigProperties(autoConfiguredOpenTelemetrySdk);
boolean enabled =
config.getBoolean("otel.instrumentation.runtime-telemetry.package-emitter.enabled", false);
if (!enabled) {

View File

@ -19,7 +19,6 @@ import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.Experiment
import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsUtil;
import io.opentelemetry.javaagent.extension.AgentListener;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.util.ArrayList;
import java.util.List;
@ -30,7 +29,7 @@ public class Java8RuntimeMetricsInstaller implements AgentListener {
@Override
public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) {
ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredSdk);
ConfigProperties config = AgentListener.resolveConfigProperties(autoConfiguredSdk);
boolean defaultEnabled = config.getBoolean("otel.instrumentation.common.default-enabled", true);
if (!config.getBoolean("otel.instrumentation.runtime-telemetry.enabled", defaultEnabled)

View File

@ -17,6 +17,8 @@ dependencies {
// autoconfigure is unstable, do not expose as api
implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator")
// Used by byte-buddy but not brought in as a transitive dependency.
compileOnly("com.google.code.findbugs:annotations")
}

View File

@ -6,7 +6,10 @@
package io.opentelemetry.javaagent.extension;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.Ordered;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import java.lang.instrument.Instrumentation;
import net.bytebuddy.agent.builder.AgentBuilder;
@ -25,4 +28,22 @@ public interface AgentListener extends Ordered {
* on an {@link Instrumentation}.
*/
void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk);
/** Resolve {@link ConfigProperties} from the {@code autoConfiguredOpenTelemetrySdk}. */
static ConfigProperties resolveConfigProperties(
AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) {
ConfigProperties sdkConfigProperties =
AutoConfigureUtil.getConfig(autoConfiguredOpenTelemetrySdk);
if (sdkConfigProperties != null) {
return sdkConfigProperties;
}
StructuredConfigProperties structuredConfigProperties =
AutoConfigureUtil.getStructuredConfig(autoConfiguredOpenTelemetrySdk);
if (structuredConfigProperties != null) {
return new StructuredConfigPropertiesBridge(structuredConfigProperties);
}
// Should never happen
throw new IllegalStateException(
"AutoConfiguredOpenTelemetrySdk does not have ConfigProperties or StructuredConfigProperties. This is likely a programming error in opentelemetry-java");
}
}

View File

@ -0,0 +1,153 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.extension;
import static io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties.empty;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import javax.annotation.Nullable;
/**
* A {@link ConfigProperties} which resolves properties based on {@link StructuredConfigProperties}.
*
* <p>Only properties starting with "otel.instrumentation." are resolved. Others return null (or
* default value if provided).
*
* <p>To resolve:
*
* <ul>
* <li>"otel.instrumentation" refers to the ".instrumentation.java" node
* <li>The portion of the property after "otel.instrumentation." is split into segments based on
* ".".
* <li>For each N-1 segment, we walk down the tree to find the relevant leaf {@link
* StructuredConfigProperties}.
* <li>We extract the property from the resolved {@link StructuredConfigProperties} using the last
* segment as the property key.
* </ul>
*
* <p>For example, given the following YAML, asking for {@code
* ConfigProperties#getString("otel.instrumentation.common.string_key")} yields "value":
*
* <pre>
* instrumentation:
* java:
* common:
* string_key: value
* </pre>
*/
final class StructuredConfigPropertiesBridge implements ConfigProperties {
private static final String OTEL_INSTRUMENTATION_PREFIX = "otel.instrumentation.";
// The node at .instrumentation.java
private final StructuredConfigProperties instrumentationJavaNode;
StructuredConfigPropertiesBridge(StructuredConfigProperties rootStructuredConfigProperties) {
instrumentationJavaNode =
rootStructuredConfigProperties
.getStructured("instrumentation", empty())
.getStructured("java", empty());
}
@Nullable
@Override
public String getString(String propertyName) {
return getPropertyValue(propertyName, StructuredConfigProperties::getString);
}
@Nullable
@Override
public Boolean getBoolean(String propertyName) {
return getPropertyValue(propertyName, StructuredConfigProperties::getBoolean);
}
@Nullable
@Override
public Integer getInt(String propertyName) {
return getPropertyValue(propertyName, StructuredConfigProperties::getInt);
}
@Nullable
@Override
public Long getLong(String propertyName) {
return getPropertyValue(propertyName, StructuredConfigProperties::getLong);
}
@Nullable
@Override
public Double getDouble(String propertyName) {
return getPropertyValue(propertyName, StructuredConfigProperties::getDouble);
}
@Nullable
@Override
public Duration getDuration(String propertyName) {
Long millis = getPropertyValue(propertyName, StructuredConfigProperties::getLong);
if (millis == null) {
return null;
}
return Duration.ofMillis(millis);
}
@Override
public List<String> getList(String propertyName) {
List<String> propertyValue =
getPropertyValue(
propertyName,
(properties, lastPart) -> properties.getScalarList(lastPart, String.class));
return propertyValue == null ? Collections.emptyList() : propertyValue;
}
@Override
public Map<String, String> getMap(String propertyName) {
StructuredConfigProperties propertyValue =
getPropertyValue(propertyName, StructuredConfigProperties::getStructured);
if (propertyValue == null) {
return Collections.emptyMap();
}
Map<String, String> result = new HashMap<>();
propertyValue
.getPropertyKeys()
.forEach(
key -> {
String value = propertyValue.getString(key);
if (value == null) {
return;
}
result.put(key, value);
});
return Collections.unmodifiableMap(result);
}
@Nullable
private <T> T getPropertyValue(
String property, BiFunction<StructuredConfigProperties, String, T> extractor) {
if (!property.startsWith(OTEL_INSTRUMENTATION_PREFIX)) {
return null;
}
String suffix = property.substring(OTEL_INSTRUMENTATION_PREFIX.length());
// Split the remainder of the property on ".", and walk to the N-1 entry
String[] segments = suffix.split("\\.");
if (segments.length == 0) {
return null;
}
StructuredConfigProperties target = instrumentationJavaNode;
if (segments.length > 1) {
for (int i = 0; i < segments.length - 1; i++) {
target = target.getStructured(segments[i], empty());
}
}
String lastPart = segments[segments.length - 1];
return extractor.apply(target, lastPart);
}
}

View File

@ -0,0 +1,120 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.extension;
import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import io.opentelemetry.sdk.extension.incubator.fileconfig.FileConfiguration;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
class StructuredConfigPropertiesBridgeTest {
private static final String YAML =
"file_format: 0.3\n"
+ "instrumentation:\n"
+ " java:\n"
+ " common:\n"
+ " default-enabled: true\n"
+ " runtime-telemetry:\n"
+ " enabled: false\n"
+ " example-instrumentation:\n"
+ " string_key: value\n"
+ " bool_key: true\n"
+ " int_key: 1\n"
+ " double_key: 1.1\n"
+ " list_key:\n"
+ " - value1\n"
+ " - value2\n"
+ " - true\n"
+ " map_key:\n"
+ " string_key1: value1\n"
+ " string_key2: value2\n"
+ " bool_key: true\n";
private final StructuredConfigProperties structuredConfigProperties =
FileConfiguration.toConfigProperties(
new ByteArrayInputStream(YAML.getBytes(StandardCharsets.UTF_8)));
private final ConfigProperties bridge =
new StructuredConfigPropertiesBridge(structuredConfigProperties);
private final ConfigProperties emptyBridge =
new StructuredConfigPropertiesBridge(
FileConfiguration.toConfigProperties(
new ByteArrayInputStream("file_format: 0.3\n".getBytes(StandardCharsets.UTF_8))));
@Test
void getProperties() {
// only properties starting with "otel.instrumentation." are resolved
// asking for properties which don't exist or inaccessible shouldn't result in an error
assertThat(bridge.getString("file_format")).isNull();
assertThat(bridge.getString("file_format", "foo")).isEqualTo("foo");
assertThat(emptyBridge.getBoolean("otel.instrumentation.common.default-enabled")).isNull();
assertThat(emptyBridge.getBoolean("otel.instrumentation.common.default-enabled", true))
.isTrue();
// common cases
assertThat(bridge.getBoolean("otel.instrumentation.common.default-enabled")).isTrue();
assertThat(bridge.getBoolean("otel.instrumentation.runtime-telemetry.enabled")).isFalse();
// check all the types
Map<String, String> expectedMap = new HashMap<>();
expectedMap.put("string_key1", "value1");
expectedMap.put("string_key2", "value2");
assertThat(bridge.getString("otel.instrumentation.example-instrumentation.string_key"))
.isEqualTo("value");
assertThat(bridge.getBoolean("otel.instrumentation.example-instrumentation.bool_key")).isTrue();
assertThat(bridge.getInt("otel.instrumentation.example-instrumentation.int_key")).isEqualTo(1);
assertThat(bridge.getLong("otel.instrumentation.example-instrumentation.int_key"))
.isEqualTo(1L);
assertThat(bridge.getDuration("otel.instrumentation.example-instrumentation.int_key"))
.isEqualTo(Duration.ofMillis(1));
assertThat(bridge.getDouble("otel.instrumentation.example-instrumentation.double_key"))
.isEqualTo(1.1);
assertThat(bridge.getList("otel.instrumentation.example-instrumentation.list_key"))
.isEqualTo(Arrays.asList("value1", "value2"));
assertThat(bridge.getMap("otel.instrumentation.example-instrumentation.map_key"))
.isEqualTo(expectedMap);
// asking for properties with the wrong type returns null
assertThat(bridge.getBoolean("otel.instrumentation.example-instrumentation.string_key"))
.isNull();
assertThat(bridge.getString("otel.instrumentation.example-instrumentation.bool_key")).isNull();
assertThat(bridge.getString("otel.instrumentation.example-instrumentation.int_key")).isNull();
assertThat(bridge.getString("otel.instrumentation.example-instrumentation.double_key"))
.isNull();
assertThat(bridge.getString("otel.instrumentation.example-instrumentation.list_key")).isNull();
assertThat(bridge.getString("otel.instrumentation.example-instrumentation.map_key")).isNull();
// check all the types
assertThat(bridge.getString("otel.instrumentation.other-instrumentation.string_key", "value"))
.isEqualTo("value");
assertThat(bridge.getBoolean("otel.instrumentation.other-instrumentation.bool_key", true))
.isTrue();
assertThat(bridge.getInt("otel.instrumentation.other-instrumentation.int_key", 1)).isEqualTo(1);
assertThat(bridge.getLong("otel.instrumentation.other-instrumentation.int_key", 1L))
.isEqualTo(1L);
assertThat(
bridge.getDuration(
"otel.instrumentation.other-instrumentation.int_key", Duration.ofMillis(1)))
.isEqualTo(Duration.ofMillis(1));
assertThat(bridge.getDouble("otel.instrumentation.other-instrumentation.double_key", 1.1))
.isEqualTo(1.1);
assertThat(
bridge.getList(
"otel.instrumentation.other-instrumentation.list_key",
Arrays.asList("value1", "value2")))
.isEqualTo(Arrays.asList("value1", "value2"));
assertThat(bridge.getMap("otel.instrumentation.other-instrumentation.map_key", expectedMap))
.isEqualTo(expectedMap);
}
}

View File

@ -42,7 +42,6 @@ import io.opentelemetry.javaagent.tooling.muzzle.AgentTooling;
import io.opentelemetry.javaagent.tooling.util.Trie;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.SdkAutoconfigureAccess;
import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.lang.instrument.Instrumentation;
import java.util.ArrayList;
@ -121,7 +120,7 @@ public class AgentInstaller {
AutoConfiguredOpenTelemetrySdk autoConfiguredSdk =
installOpenTelemetrySdk(extensionClassLoader);
ConfigProperties sdkConfig = AutoConfigureUtil.getConfig(autoConfiguredSdk);
ConfigProperties sdkConfig = AgentListener.resolveConfigProperties(autoConfiguredSdk);
AgentInstrumentationConfig.internalInitializeConfig(new ConfigPropertiesBridge(sdkConfig));
copyNecessaryConfigToSystemProperties(sdkConfig);
@ -196,7 +195,7 @@ public class AgentInstaller {
addHttpServerResponseCustomizers(extensionClassLoader);
runAfterAgentListeners(agentListeners, autoConfiguredSdk);
runAfterAgentListeners(agentListeners, autoConfiguredSdk, sdkConfig);
}
private static void copyNecessaryConfigToSystemProperties(ConfigProperties config) {
@ -268,7 +267,9 @@ public class AgentInstaller {
}
private static void runAfterAgentListeners(
Iterable<AgentListener> agentListeners, AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) {
Iterable<AgentListener> agentListeners,
AutoConfiguredOpenTelemetrySdk autoConfiguredSdk,
ConfigProperties sdkConfigProperties) {
// java.util.logging.LogManager maintains a final static LogManager, which is created during
// class initialization. Some AgentListener implementations may use JRE bootstrap classes
// which touch this class (e.g. JFR classes or some MBeans).
@ -286,8 +287,7 @@ public class AgentInstaller {
// the application is already setting the global LogManager and AgentListener won't be able
// to touch it due to class loader locking.
boolean shouldForceSynchronousAgentListenersCalls =
AutoConfigureUtil.getConfig(autoConfiguredSdk)
.getBoolean(FORCE_SYNCHRONOUS_AGENT_LISTENERS_CONFIG, false);
sdkConfigProperties.getBoolean(FORCE_SYNCHRONOUS_AGENT_LISTENERS_CONFIG, false);
boolean javaBefore9 = isJavaBefore9();
if (!shouldForceSynchronousAgentListenersCalls && javaBefore9 && isAppUsingCustomLogManager()) {
logger.fine("Custom JUL LogManager detected: delaying AgentListener#afterAgent() calls");

View File

@ -0,0 +1,20 @@
instrumentation:
java:
common:
default-enabled: true
runtime-telemetry:
enabled: false
external-annotations:
enabled: true
example-instrumentation:
string_key: value
bool_key: true
int_key: 1
double_key: 1.1
list_key:
- value1
- value2
map_key:
string_key1: value1
string_key2: value2
bool_key: true