Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions instrumentation-api-incubator/README.md
Original file line number Diff line number Diff line change
@@ -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:
Comment on lines +50 to +51
Copy link
Contributor

Choose a reason for hiding this comment

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

Could be worth adding for extra clarity that this allows to use declarative/yaml configuration in addition to the java properties and environment variables, in short we have to change how configuration is consumed to support both, not how it's being set.

Copy link
Member Author

Choose a reason for hiding this comment

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

no, if you use declarative config you can't use env vars any more

Copy link
Contributor

Choose a reason for hiding this comment

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

Do the java properties still work ? Also, do we still have a way to handle compatibility with existing dotted syntax even when configuration options do not strictly fit yaml structure ? (disclamer: I have little knowledge about declarative configuration, maybe this is documented somewhere).

Copy link
Member Author

@zeitlinger zeitlinger Aug 26, 2025

Choose a reason for hiding this comment

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

Do the java properties still work ?

Not by default - but you can add that capability back.
The migration schema provides the same capabilities as before, e.g. value: ${OTEL_SERVICE_NAME:-unknown_service} adds support for the env var and sys property for the service name

Also, do we still have a way to handle compatibility with existing dotted syntax even when configuration options do not strictly fit yaml structure ?

  • this PR allows existing instrumentations to read simple values or lists from either yaml or sys props
  • if a new instrumentation needs to access data that only fits in yaml, then it can't use the bridge - e.g. here
    InstrumentationConfig config = AgentInstrumentationConfig.get();
    List<TypeInstrumentation> list =
        config.isDeclarative()
            ? MethodsConfig.parseDeclarativeConfig(config.getDeclarativeConfig("methods")) <-- this has access to yaml tree
            : parseConfigProperties();

(disclamer: I have little knowledge about declarative configuration, maybe this is documented somewhere).

no worries - the user facing documentation is not there yet

Copy link
Contributor

Choose a reason for hiding this comment

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

The only thing that I worry (at least for now) is having the capability to preserve the current support for environment variables and JVM system properties in a distribution (also includes some elements from contrib), so any documentation how to achieve this would be welcome. If users decide to use declarative configuration, then it's a breaking change and we don't have such requirement (and they should be able to use system properties and env variables in yaml if needed).

Copy link
Member Author

Choose a reason for hiding this comment

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

we're not changing anything for user that are not using declarative config


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;
}
}
```
4 changes: 4 additions & 0 deletions instrumentation-api-incubator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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) + "'";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -169,7 +169,7 @@ private <T> T getPropertyValue(
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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -21,9 +21,6 @@
/**
* 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 {
/**
Expand Down Expand Up @@ -82,6 +79,17 @@ public ConfigProperties build(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenT
"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.
Expand All @@ -94,12 +102,7 @@ public ConfigProperties build(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenT
*/
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"));
}
}
Original file line number Diff line number Diff line change
@@ -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'");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,7 +46,7 @@ void shouldUseConfigProviderForDeclarativeConfiguration() {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
file_format: 0.4
file_format: 1.0-rc.1
instrumentation/development:
java:
acme:
Expand Down
1 change: 1 addition & 0 deletions javaagent-tooling/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
30 changes: 23 additions & 7 deletions javaagent/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand All @@ -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"))
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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/")
Expand All @@ -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/**")
Copy link
Member

Choose a reason for hiding this comment

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

if a new package was added and we forgot to add an exclusion for it here, would something fail in a way we'd know to add it?

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 believe this would fail

"Failed to load bootstrap class: " + info.getName() + " in " + bootstrapPrefix, e);

It failed for me until I changed this at least.

Copy link
Member

Choose a reason for hiding this comment

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

cool, i was wondering how that change was related

}

class JavaagentProvider(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,15 @@ void classPathSetUp() throws ClassNotFoundException {
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);
}
}
}
Expand Down