make declarative config bridge usable by spring starter and contrib (#14497)

Co-authored-by: otelbot <197425009+otelbot@users.noreply.github.com>
This commit is contained in:
Gregor Zeitlinger 2025-08-28 17:45:17 +02:00 committed by GitHub
parent 99b89ee108
commit 136efcb48e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 191 additions and 30 deletions

View File

@ -0,0 +1,91 @@
# OpenTelemetry Instrumentation API Incubator
Instrumentation API Incubator is a collection of libraries that provide additional functionality
for OpenTelemetry instrumentation and auto-configuration. It is intended to be used by
instrumentation authors and auto-configuration providers to enhance their capabilities and provide a
more consistent experience when working with OpenTelemetry.
## Declarative Config Bridge
Declarative Config Bridge allows instrumentation authors to access configuration in a uniform way,
regardless of the configuration source.
The bridge allows you to read configuration using the system property style when dealing with
declarative configuration.
### Example
As an example, let's look at the inferred spans configuration.
First, there is a configuration method that reads the properties and is unaware of the source of the
configuration:
```java
class InferredSpansConfig {
static SpanProcessor create(ConfigProperties properties) {
// read properties here
boolean backupDiagnosticFiles =
properties.getBoolean("otel.inferred.spans.backup.diagnostic.files", false);
}
}
```
The auto configuration **without declarative config** passes the provided properties directly:
```java
@AutoService(AutoConfigurationCustomizerProvider.class)
public class InferredSpansAutoConfig implements AutoConfigurationCustomizerProvider {
@Override
public void customize(AutoConfigurationCustomizer config) {
config.addTracerProviderCustomizer(
(providerBuilder, properties) -> {
providerBuilder.addSpanProcessor(InferredSpansConfig.create(properties));
return providerBuilder;
});
}
}
```
The auto configuration **with declarative config** uses the Declarative Config Bridge to be able to
use common configuration method:
Let's first look at the yaml file that is used to configure the inferred spans processor:
```yaml
file_format: 1.0-rc.1
tracer_provider:
processors:
- inferred_spans:
backup:
diagnostic:
files: true
```
And now the component provider that uses the Declarative Config Bridge:
```java
@AutoService(ComponentProvider.class)
public class InferredSpansComponentProvider implements ComponentProvider<SpanProcessor> {
@Override
public String getName() {
return "inferred_spans";
}
@Override
public SpanProcessor create(DeclarativeConfigProperties config) {
return InferredSpansConfig.create(
new DeclarativeConfigPropertiesBridgeBuilder()
// crop the prefix, because the properties are under the "inferred_spans" processor
.addMapping("otel.inferred.spans.", "")
.build(config));
}
@Override
public Class<SpanProcessor> getType() {
return SpanProcessor.class;
}
}
```

View File

@ -14,6 +14,8 @@ dependencies {
api("io.opentelemetry.semconv:opentelemetry-semconv")
api(project(":instrumentation-api"))
api("io.opentelemetry:opentelemetry-api-incubator")
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator")
compileOnly("com.google.auto.value:auto-value-annotations")
annotationProcessor("com.google.auto.value:auto-value")
@ -22,6 +24,8 @@ dependencies {
testImplementation("io.opentelemetry:opentelemetry-sdk")
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator")
}
tasks {

View File

@ -0,0 +1,23 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.api.incubator.sdk.config.bridge;
public final class ConfigPropertiesUtil {
private ConfigPropertiesUtil() {}
public static String propertyYamlPath(String propertyName) {
return yamlPath(propertyName);
}
static String yamlPath(String property) {
String[] segments = DeclarativeConfigPropertiesBridge.getSegments(property);
if (segments.length == 0) {
throw new IllegalArgumentException("Invalid property: " + property);
}
return "'instrumentation/development' / 'java' / '" + String.join("' / '", segments) + "'";
}
}

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.extension.internal;
package io.opentelemetry.instrumentation.api.incubator.sdk.config.bridge;
import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty;
@ -169,7 +169,7 @@ final class DeclarativeConfigPropertiesBridge implements ConfigProperties {
return extractor.apply(target, lastPart);
}
private static String[] getSegments(String property) {
static String[] getSegments(String property) {
if (property.startsWith(OTEL_INSTRUMENTATION_PREFIX)) {
property = property.substring(OTEL_INSTRUMENTATION_PREFIX.length());
}

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.extension.internal;
package io.opentelemetry.instrumentation.api.incubator.sdk.config.bridge;
import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty;
@ -21,9 +21,6 @@ import javax.annotation.Nullable;
/**
* A builder for {@link DeclarativeConfigPropertiesBridge} that allows adding translations and fixed
* values for properties.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*/
public class DeclarativeConfigPropertiesBridgeBuilder {
/**
@ -82,6 +79,17 @@ public class DeclarativeConfigPropertiesBridgeBuilder {
"AutoConfiguredOpenTelemetrySdk does not have ConfigProperties or DeclarativeConfigProperties. This is likely a programming error in opentelemetry-java");
}
/**
* Build {@link ConfigProperties} from the provided {@link DeclarativeConfigProperties} node.
*
* @param node the declarative config properties to build from
* @return a new instance of {@link ConfigProperties}
*/
public ConfigProperties build(@Nullable DeclarativeConfigProperties node) {
return new DeclarativeConfigPropertiesBridge(
node == null ? empty() : node, mappings, overrideValues);
}
/**
* Build {@link ConfigProperties} from the {@link DeclarativeConfigProperties} provided by the
* instrumentation configuration.
@ -94,12 +102,7 @@ public class DeclarativeConfigPropertiesBridgeBuilder {
*/
public ConfigProperties buildFromInstrumentationConfig(
@Nullable DeclarativeConfigProperties instrumentationConfig) {
// leave the name "build" for a future method that builds from a DeclarativeConfigProperties
// instance that doesn't come from the top-level instrumentation config
if (instrumentationConfig == null) {
instrumentationConfig = DeclarativeConfigProperties.empty();
}
return new DeclarativeConfigPropertiesBridge(
instrumentationConfig.getStructured("java", empty()), mappings, overrideValues);
return build(
instrumentationConfig == null ? null : instrumentationConfig.getStructured("java"));
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.api.incubator.sdk.config.bridge;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
class ConfigPropertiesUtilTest {
@Test
void propertyYamlPath() {
assertThat(ConfigPropertiesUtil.propertyYamlPath("google.otel.auth.target.signals"))
.isEqualTo(
"'instrumentation/development' / 'java' / 'google' / 'otel' / 'auth' / 'target' / 'signals'");
}
}

View File

@ -3,10 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.extension.internal;
package io.opentelemetry.instrumentation.api.incubator.sdk.config.bridge;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -47,7 +46,7 @@ class DeclarativeConfigPropertiesBridgeBuilderTest {
when(javaNodeMock.getString(propertyName)).thenReturn(expectedValue);
DeclarativeConfigProperties instrumentationConfigMock = mock(DeclarativeConfigProperties.class);
when(instrumentationConfigMock.getStructured(eq("java"), any())).thenReturn(javaNodeMock);
when(instrumentationConfigMock.getStructured(eq("java"))).thenReturn(javaNodeMock);
ConfigProvider configProviderMock = mock(ConfigProvider.class);
when(configProviderMock.getInstrumentationConfig()).thenReturn(instrumentationConfigMock);

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.extension.internal;
package io.opentelemetry.instrumentation.api.incubator.sdk.config.bridge;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -1,4 +1,4 @@
file_format: 0.4
file_format: 1.0-rc.1
instrumentation/development:
java:
acme:

View File

@ -20,6 +20,7 @@ dependencies {
implementation("io.opentelemetry:opentelemetry-api")
implementation("io.opentelemetry:opentelemetry-sdk")
implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
implementation("io.opentelemetry:opentelemetry-extension-kotlin")
implementation("io.opentelemetry:opentelemetry-extension-trace-propagators")
// the incubator's ViewConfigCustomizer is used to support loading yaml-based metric views

View File

@ -6,8 +6,8 @@
package io.opentelemetry.javaagent.tooling;
import io.opentelemetry.api.incubator.config.ConfigProvider;
import io.opentelemetry.instrumentation.api.incubator.sdk.config.bridge.DeclarativeConfigPropertiesBridgeBuilder;
import io.opentelemetry.javaagent.bootstrap.OpenTelemetrySdkAccess;
import io.opentelemetry.javaagent.extension.internal.DeclarativeConfigPropertiesBridgeBuilder;
import io.opentelemetry.javaagent.tooling.config.EarlyInitAgentConfig;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;

View File

@ -57,7 +57,6 @@ dependencies {
bootstrapLibs(project(":instrumentation-api"))
// opentelemetry-api is an api dependency of :instrumentation-api, but opentelemetry-api-incubator is not
bootstrapLibs("io.opentelemetry:opentelemetry-api-incubator")
bootstrapLibs(project(":instrumentation-api-incubator"))
bootstrapLibs(project(":instrumentation-annotations-support"))
bootstrapLibs(project(":javaagent-bootstrap"))
@ -71,8 +70,11 @@ dependencies {
exclude("io.opentelemetry", "opentelemetry-sdk-extension-autoconfigure-spi")
}
baseJavaagentLibs(project(":javaagent-extension-api"))
baseJavaagentLibs(project(":instrumentation-api-incubator"))
baseJavaagentLibs(project(":javaagent-tooling"))
baseJavaagentLibs(project(":javaagent-tooling")) {
exclude("io.opentelemetry", "opentelemetry-sdk-extension-autoconfigure-spi")
}
baseJavaagentLibs(project(":javaagent-internal-logging-application"))
baseJavaagentLibs(project(":javaagent-internal-logging-simple", configuration = "shadow"))
baseJavaagentLibs(project(":muzzle"))
@ -147,8 +149,7 @@ tasks {
val buildBootstrapLibs by registering(ShadowJar::class) {
configurations = listOf(bootstrapLibs)
// exclude the agent part of the javaagent-extension-api; these classes will be added in relocate tasks
exclude("io/opentelemetry/javaagent/extension/**")
excludeNonBootstrapClasses()
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
@ -286,7 +287,8 @@ tasks {
doLast {
val filePath = rootDir.toPath().resolve("licenses").resolve("licenses.md")
if (Files.exists(filePath)) {
val datePattern = Pattern.compile("^_[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} .*_$")
val datePattern =
Pattern.compile("^_[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} .*_$")
val lines = Files.readAllLines(filePath)
// 4th line contains the timestamp of when the license report was generated, replace it with
// an empty line
@ -412,7 +414,8 @@ fun CopySpec.copyByteBuddy(jar: Provider<RegularFile>) {
eachFile {
if (path.startsWith("net/bytebuddy/") &&
// this is our class that we have placed in the byte buddy package, need to preserve it
!path.startsWith("net/bytebuddy/agent/builder/AgentBuilderUtil")) {
!path.startsWith("net/bytebuddy/agent/builder/AgentBuilderUtil")
) {
exclude()
} else if (path.startsWith("META-INF/versions/9/net/bytebuddy/")) {
path = path.removePrefix("META-INF/versions/9/")
@ -422,17 +425,30 @@ fun CopySpec.copyByteBuddy(jar: Provider<RegularFile>) {
}
}
// exclude bootstrap projects from javaagent libs - they won't be added to inst/
fun ShadowJar.excludeNonBootstrapClasses() {
// exclude the agent part of the javaagent-extension-api; these classes will be added in relocate tasks
exclude("io/opentelemetry/javaagent/extension/**")
exclude("**/instrumentation/api/incubator/sdk/**")
}
// exclude bootstrap projects from javaagent libs - they won't be added to inst/
fun ShadowJar.excludeBootstrapClasses() {
dependencies {
exclude(project(":instrumentation-api"))
exclude(project(":instrumentation-api-incubator"))
exclude(project(":instrumentation-annotations-support"))
exclude(project(":javaagent-bootstrap"))
}
// exclude the bootstrap part of the javaagent-extension-api
exclude("io/opentelemetry/javaagent/bootstrap/**")
// all in instrumentation-api-incubator except the bridge package
exclude("io/opentelemetry/instrumentation/api/incubator/builder/**")
exclude("io/opentelemetry/instrumentation/api/incubator/config/**")
exclude("io/opentelemetry/instrumentation/api/incubator/instrumenter/**")
exclude("io/opentelemetry/instrumentation/api/incubator/log/**")
exclude("io/opentelemetry/instrumentation/api/incubator/semconv/**")
}
class JavaagentProvider(

View File

@ -50,10 +50,15 @@ class AgentInstrumentationTest {
for (ClassPath.ClassInfo info : getTestClasspath().getAllClasses()) {
for (String bootstrapPrefix : BOOTSTRAP_PACKAGE_PREFIXES) {
if (info.getName().startsWith(bootstrapPrefix)) {
Class<?> bootstrapClass = Class.forName(info.getName());
ClassLoader loader = bootstrapClass.getClassLoader();
if (loader != BOOTSTRAP_CLASSLOADER) {
bootstrapClassesIncorrectlyLoaded.add(bootstrapClass);
try {
Class<?> bootstrapClass = Class.forName(info.getName());
ClassLoader loader = bootstrapClass.getClassLoader();
if (loader != BOOTSTRAP_CLASSLOADER) {
bootstrapClassesIncorrectlyLoaded.add(bootstrapClass);
}
} catch (ClassNotFoundException | NoClassDefFoundError e) {
throw new RuntimeException(
"Failed to load bootstrap class: " + info.getName() + " in " + bootstrapPrefix, e);
}
}
}