diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 63d5b270f..1b7d1663f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -13,6 +13,7 @@ body: - aws-xray - aws-xray-propagator - consistent-sampling + - declarative-config-bridge - disk-buffering - gcp-auth-extension - gcp-resources diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 45edd560b..576032700 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -13,6 +13,7 @@ body: - aws-xray - aws-xray-propagator - consistent-sampling + - declarative-config-bridge - disk-buffering - gcp-auth-extension - gcp-resources diff --git a/.github/component_owners.yml b/.github/component_owners.yml index 6b74234ad..32551dc97 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -32,6 +32,9 @@ components: consistent-sampling: - oertl - PeterF778 + declarative-config-bridge: + - jaydeluca + - zeitlinger disk-buffering: - LikeTheSalad - zeitlinger diff --git a/.github/scripts/draft-change-log-entries.sh b/.github/scripts/draft-change-log-entries.sh index 845c92672..7e3b88603 100755 --- a/.github/scripts/draft-change-log-entries.sh +++ b/.github/scripts/draft-change-log-entries.sh @@ -32,6 +32,7 @@ component_names["baggage-processor/"]="Baggage processor" component_names["cloudfoundry-resources/"]="CloudFoundry resources" component_names["compressors/"]="Compressors" component_names["consistent-sampling/"]="Consistent sampling" +component_names["declarative-config-bridge"]="Declarative config bridge" component_names["disk-buffering/"]="Disk buffering" component_names["gcp-resources/"]="GCP resources" component_names["gcp-auth-extension/"]="GCP authentication extension" diff --git a/README.md b/README.md index c42ea7e27..0392904f3 100644 --- a/README.md +++ b/README.md @@ -14,33 +14,34 @@ feature or via instrumentation, this project is hopefully for you. ## Provided Libraries -| Status* | Library | -|---------|-------------------------------------------------------------------| -| beta | [AWS Resources](./aws-resources/README.md) | -| stable | [AWS X-Ray SDK Support](./aws-xray/README.md) | -| alpha | [AWS X-Ray Propagator](./aws-xray-propagator/README.md) | -| alpha | [Baggage Processors](./baggage-processor/README.md) | -| alpha | [zstd Compressor](./compressors/compressor-zstd/README.md) | -| alpha | [Consistent Sampling](./consistent-sampling/README.md) | -| alpha | [Disk Buffering](./disk-buffering/README.md) | -| alpha | [GCP Authentication Extension](./gcp-auth-extension/README.md) | -| beta | [GCP Resources](./gcp-resources/README.md) | -| beta | [Inferred Spans](./inferred-spans/README.md) | -| alpha | [IBM MQ Metrics](./ibm-mq-metrics/README.md) | -| alpha | [JFR Connection](./jfr-connection/README.md) | -| alpha | [JFR Events](./jfr-events/README.md) | -| alpha | [JMX Metric Gatherer](./jmx-metrics/README.md) | -| alpha | [JMX Metric Scraper](./jmx-scraper/README.md) | -| alpha | [Kafka Support](./kafka-exporter/README.md) | -| alpha | [OpenTelemetry Maven Extension](./maven-extension/README.md) | -| alpha | [Micrometer MeterProvider](./micrometer-meter-provider/README.md) | -| alpha | [No-Op API](./noop-api/README.md) | -| alpha | [Intercept and Process Signals Globally](./processors/README.md) | -| alpha | [Prometheus Client Bridge](./prometheus-client-bridge/README.md) | -| alpha | [Resource Providers](./resource-providers/README.md) | -| alpha | [Runtime Attach](./runtime-attach/README.md) | -| alpha | [Samplers](./samplers/README.md) | -| beta | [Span Stacktrace Capture](./span-stacktrace/README.md) | +| Status* | Library | +|---------|--------------------------------------------------------------------| +| beta | [AWS Resources](./aws-resources/README.md) | +| stable | [AWS X-Ray SDK Support](./aws-xray/README.md) | +| alpha | [AWS X-Ray Propagator](./aws-xray-propagator/README.md) | +| alpha | [Baggage Processors](./baggage-processor/README.md) | +| alpha | [zstd Compressor](./compressors/compressor-zstd/README.md) | +| alpha | [Consistent Sampling](./consistent-sampling/README.md) | +| alpha | [Declarative Config Bridge](./declarative-config-bridge/README.md) | +| alpha | [Disk Buffering](./disk-buffering/README.md) | +| alpha | [GCP Authentication Extension](./gcp-auth-extension/README.md) | +| beta | [GCP Resources](./gcp-resources/README.md) | +| beta | [Inferred Spans](./inferred-spans/README.md) | +| alpha | [IBM MQ Metrics](./ibm-mq-metrics/README.md) | +| alpha | [JFR Connection](./jfr-connection/README.md) | +| alpha | [JFR Events](./jfr-events/README.md) | +| alpha | [JMX Metric Gatherer](./jmx-metrics/README.md) | +| alpha | [JMX Metric Scraper](./jmx-scraper/README.md) | +| alpha | [Kafka Support](./kafka-exporter/README.md) | +| alpha | [OpenTelemetry Maven Extension](./maven-extension/README.md) | +| alpha | [Micrometer MeterProvider](./micrometer-meter-provider/README.md) | +| alpha | [No-Op API](./noop-api/README.md) | +| alpha | [Intercept and Process Signals Globally](./processors/README.md) | +| alpha | [Prometheus Client Bridge](./prometheus-client-bridge/README.md) | +| alpha | [Resource Providers](./resource-providers/README.md) | +| alpha | [Runtime Attach](./runtime-attach/README.md) | +| alpha | [Samplers](./samplers/README.md) | +| beta | [Span Stacktrace Capture](./span-stacktrace/README.md) | \* `alpha`, `beta` and `stable` are currently used to denote library status per [otep 0232](https://github.com/open-telemetry/oteps/blob/main/text/0232-maturity-of-otel.md). To reach stable status, the library needs to have stable APIs, stable semantic conventions, and be production ready. diff --git a/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsComponentProviderTest.java b/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsComponentProviderTest.java index c2869d265..5bcd62137 100644 --- a/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsComponentProviderTest.java +++ b/aws-xray-propagator/src/test/java/io/opentelemetry/contrib/awsxray/propagator/internal/AwsComponentProviderTest.java @@ -21,7 +21,7 @@ class AwsComponentProviderTest { @Test void endToEnd() { String yaml = - "file_format: 0.4\n" + "file_format: 1.0-rc.1\n" + "propagator:\n" + " composite:\n" + " - xray:\n" diff --git a/declarative-config-bridge/README.md b/declarative-config-bridge/README.md new file mode 100644 index 000000000..8af40618b --- /dev/null +++ b/declarative-config-bridge/README.md @@ -0,0 +1,91 @@ +# 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 { + + @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 getType() { + return SpanProcessor.class; + } +} +``` + +## Component owners + +- [Gregor Zeitlinger](https://github.com/zeitlinger), Grafana +- [Jay DeLuca](https://github.com/jaydeluca), Grafana + +Learn more about component owners in [component_owners.yml](../.github/component_owners.yml). diff --git a/declarative-config-bridge/build.gradle.kts b/declarative-config-bridge/build.gradle.kts new file mode 100644 index 000000000..8e20094b4 --- /dev/null +++ b/declarative-config-bridge/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") +} + +description = "OpenTelemetry extension that provides a bridge for declarative configuration." +otelJava.moduleName.set("io.opentelemetry.contrib.sdk.config.bridge") + +dependencies { + // We use `compileOnly` dependency because during runtime all necessary classes are provided by + // javaagent itself. + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator") + + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") + testImplementation("org.mockito:mockito-inline") +} diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/contrib/sdk/config/bridge/DeclarativeConfigPropertiesBridge.java b/declarative-config-bridge/src/main/java/io/opentelemetry/contrib/sdk/config/bridge/DeclarativeConfigPropertiesBridge.java new file mode 100644 index 000000000..c5384097c --- /dev/null +++ b/declarative-config-bridge/src/main/java/io/opentelemetry/contrib/sdk/config/bridge/DeclarativeConfigPropertiesBridge.java @@ -0,0 +1,188 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sdk.config.bridge; + +import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty; + +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import javax.annotation.Nullable; + +/** + * A {@link ConfigProperties} which resolves properties based on {@link + * DeclarativeConfigProperties}. + * + *

Only properties starting with "otel.instrumentation." are resolved. Others return null (or + * default value if provided). + * + *

To resolve: + * + *

+ * + *

For example, given the following YAML, asking for {@code + * ConfigProperties#getString("otel.instrumentation.common.string_key")} yields "value": + * + *

+ *   instrumentation:
+ *     java:
+ *       common:
+ *         string_key: value
+ * 
+ */ +final class DeclarativeConfigPropertiesBridge implements ConfigProperties { + + private static final String OTEL_INSTRUMENTATION_PREFIX = "otel.instrumentation."; + + private final DeclarativeConfigProperties baseNode; + + // lookup order matters - we choose the first match + private final Map mappings; + private final Map overrideValues; + + DeclarativeConfigPropertiesBridge( + DeclarativeConfigProperties baseNode, + Map mappings, + Map overrideValues) { + this.baseNode = Objects.requireNonNull(baseNode); + this.mappings = mappings; + this.overrideValues = overrideValues; + } + + @Nullable + @Override + public String getString(String propertyName) { + return getPropertyValue(propertyName, String.class, DeclarativeConfigProperties::getString); + } + + @Nullable + @Override + public Boolean getBoolean(String propertyName) { + return getPropertyValue(propertyName, Boolean.class, DeclarativeConfigProperties::getBoolean); + } + + @Nullable + @Override + public Integer getInt(String propertyName) { + return getPropertyValue(propertyName, Integer.class, DeclarativeConfigProperties::getInt); + } + + @Nullable + @Override + public Long getLong(String propertyName) { + return getPropertyValue(propertyName, Long.class, DeclarativeConfigProperties::getLong); + } + + @Nullable + @Override + public Double getDouble(String propertyName) { + return getPropertyValue(propertyName, Double.class, DeclarativeConfigProperties::getDouble); + } + + @Nullable + @Override + public Duration getDuration(String propertyName) { + Long millis = getPropertyValue(propertyName, Long.class, DeclarativeConfigProperties::getLong); + if (millis == null) { + return null; + } + return Duration.ofMillis(millis); + } + + @SuppressWarnings("unchecked") + @Override + public List getList(String propertyName) { + List propertyValue = + getPropertyValue( + propertyName, + List.class, + (properties, lastPart) -> properties.getScalarList(lastPart, String.class)); + return propertyValue == null ? Collections.emptyList() : propertyValue; + } + + @SuppressWarnings("unchecked") + @Override + public Map getMap(String propertyName) { + DeclarativeConfigProperties propertyValue = + getPropertyValue( + propertyName, + DeclarativeConfigProperties.class, + DeclarativeConfigProperties::getStructured); + if (propertyValue == null) { + return Collections.emptyMap(); + } + Map 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 getPropertyValue( + String property, + Class clazz, + BiFunction extractor) { + T override = clazz.cast(overrideValues.get(property)); + if (override != null) { + return override; + } + + String[] segments = getSegments(translateProperty(property)); + if (segments.length == 0) { + return null; + } + + // Extract the value by walking to the N-1 entry + DeclarativeConfigProperties target = baseNode; + 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); + } + + static String[] getSegments(String property) { + if (property.startsWith(OTEL_INSTRUMENTATION_PREFIX)) { + property = property.substring(OTEL_INSTRUMENTATION_PREFIX.length()); + } + // Split the remainder of the property on "." + return property.replace('-', '_').split("\\."); + } + + private String translateProperty(String property) { + for (Map.Entry entry : mappings.entrySet()) { + if (property.startsWith(entry.getKey())) { + return entry.getValue() + property.substring(entry.getKey().length()); + } + } + return property; + } +} diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/contrib/sdk/config/bridge/DeclarativeConfigPropertiesBridgeBuilder.java b/declarative-config-bridge/src/main/java/io/opentelemetry/contrib/sdk/config/bridge/DeclarativeConfigPropertiesBridgeBuilder.java new file mode 100644 index 000000000..7f00bdd5f --- /dev/null +++ b/declarative-config-bridge/src/main/java/io/opentelemetry/contrib/sdk/config/bridge/DeclarativeConfigPropertiesBridgeBuilder.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sdk.config.bridge; + +import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.incubator.config.ConfigProvider; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * A builder for {@link DeclarativeConfigPropertiesBridge} that allows adding translations and fixed + * values for properties. + */ +public class DeclarativeConfigPropertiesBridgeBuilder { + /** + * order is important here, so we use LinkedHashMap - see {@link #addMapping(String, String)} for + * more details + */ + private final Map mappings = new LinkedHashMap<>(); + + private final Map overrideValues = new HashMap<>(); + + public DeclarativeConfigPropertiesBridgeBuilder() {} + + /** + * Adds a mapping from a property prefix to a YAML path. + * + *

For example, if the property prefix is "otel.javaagent" and the YAML path is "agent", then + * any property starting with "otel.javaagent." will be resolved against the "agent" node in the + * instrumentation/java section of the YAML configuration. + * + * @param propertyPrefix the prefix of the property to translate + * @param yamlPath the YAML path to resolve the property against + */ + @CanIgnoreReturnValue + public DeclarativeConfigPropertiesBridgeBuilder addMapping( + String propertyPrefix, String yamlPath) { + mappings.put(propertyPrefix, yamlPath); + return this; + } + + /** + * Adds a fixed override value for a property. + * + * @param propertyName the name of the property to override + * @param value the value to return when the property is requested + */ + @CanIgnoreReturnValue + public DeclarativeConfigPropertiesBridgeBuilder addOverride(String propertyName, Object value) { + overrideValues.put(propertyName, value); + return this; + } + + /** Build {@link ConfigProperties} from the {@code autoConfiguredOpenTelemetrySdk}. */ + public ConfigProperties build(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { + ConfigProperties sdkConfigProperties = + AutoConfigureUtil.getConfig(autoConfiguredOpenTelemetrySdk); + if (sdkConfigProperties != null) { + return sdkConfigProperties; + } + ConfigProvider configProvider = + AutoConfigureUtil.getConfigProvider(autoConfiguredOpenTelemetrySdk); + if (configProvider != null) { + return buildFromInstrumentationConfig(configProvider.getInstrumentationConfig()); + } + // Should never happen + throw new IllegalStateException( + "AutoConfiguredOpenTelemetrySdk does not have ConfigProperties or DeclarativeConfigProperties. This is likely a programming error in opentelemetry-java"); + } + + /** + * Build {@link DeclarativeConfigPropertiesBridge} from the {@link DeclarativeConfigProperties}. + * + * @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}. + * + * @param instrumentationConfig the instrumentation configuration to build from + * @return a new instance of {@link ConfigProperties} + */ + public ConfigProperties buildFromInstrumentationConfig( + @Nullable DeclarativeConfigProperties instrumentationConfig) { + return build( + instrumentationConfig == null ? null : instrumentationConfig.getStructured("java")); + } +} diff --git a/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridgeBuilderTest.java b/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridgeBuilderTest.java new file mode 100644 index 000000000..a353a307f --- /dev/null +++ b/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridgeBuilderTest.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.incubator.config.ConfigProvider; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.contrib.sdk.config.bridge.DeclarativeConfigPropertiesBridgeBuilder; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +@SuppressWarnings("DoNotMockAutoValue") +class DeclarativeConfigPropertiesBridgeBuilderTest { + @Test + void shouldUseConfigPropertiesForAutoConfiguration() { + ConfigProperties configPropertiesMock = mock(ConfigProperties.class); + AutoConfiguredOpenTelemetrySdk sdkMock = mock(AutoConfiguredOpenTelemetrySdk.class); + try (MockedStatic autoConfigureUtilMock = + Mockito.mockStatic(AutoConfigureUtil.class)) { + autoConfigureUtilMock + .when(() -> AutoConfigureUtil.getConfig(sdkMock)) + .thenReturn(configPropertiesMock); + + ConfigProperties configProperties = + new DeclarativeConfigPropertiesBridgeBuilder().build(sdkMock); + + assertThat(configProperties).isSameAs(configPropertiesMock); + } + } + + @Test + void shouldUseConfigProviderForDeclarativeConfiguration() { + String propertyName = "testProperty"; + String expectedValue = "the value"; + DeclarativeConfigProperties javaNodeMock = mock(DeclarativeConfigProperties.class); + when(javaNodeMock.getString(propertyName)).thenReturn(expectedValue); + + DeclarativeConfigProperties instrumentationConfigMock = mock(DeclarativeConfigProperties.class); + when(instrumentationConfigMock.getStructured(eq("java"))).thenReturn(javaNodeMock); + + ConfigProvider configProviderMock = mock(ConfigProvider.class); + when(configProviderMock.getInstrumentationConfig()).thenReturn(instrumentationConfigMock); + + AutoConfiguredOpenTelemetrySdk sdkMock = mock(AutoConfiguredOpenTelemetrySdk.class); + + try (MockedStatic autoConfigureUtilMock = + Mockito.mockStatic(AutoConfigureUtil.class)) { + autoConfigureUtilMock.when(() -> AutoConfigureUtil.getConfig(sdkMock)).thenReturn(null); + autoConfigureUtilMock + .when(() -> AutoConfigureUtil.getConfigProvider(sdkMock)) + .thenReturn(configProviderMock); + + ConfigProperties configProperties = + new DeclarativeConfigPropertiesBridgeBuilder().build(sdkMock); + + assertThat(configProperties.getString(propertyName)).isEqualTo(expectedValue); + } + } + + @Test + void shouldUseConfigProviderForDeclarativeConfiguration_noInstrumentationConfig() { + AutoConfiguredOpenTelemetrySdk sdkMock = mock(AutoConfiguredOpenTelemetrySdk.class); + ConfigProvider configProviderMock = mock(ConfigProvider.class); + when(configProviderMock.getInstrumentationConfig()).thenReturn(null); + + try (MockedStatic autoConfigureUtilMock = + Mockito.mockStatic(AutoConfigureUtil.class)) { + autoConfigureUtilMock.when(() -> AutoConfigureUtil.getConfig(sdkMock)).thenReturn(null); + autoConfigureUtilMock + .when(() -> AutoConfigureUtil.getConfigProvider(sdkMock)) + .thenReturn(configProviderMock); + + ConfigProperties configProperties = + new DeclarativeConfigPropertiesBridgeBuilder().build(sdkMock); + + assertThat(configProperties.getString("testProperty")).isNull(); + } + } +} diff --git a/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridgeTest.java b/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridgeTest.java new file mode 100644 index 000000000..ac08e221b --- /dev/null +++ b/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridgeTest.java @@ -0,0 +1,145 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.contrib.sdk.config.bridge.DeclarativeConfigPropertiesBridgeBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration; +import io.opentelemetry.sdk.extension.incubator.fileconfig.SdkConfigProvider; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.InstrumentationModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DeclarativeConfigPropertiesBridgeTest { + + private ConfigProperties bridge; + private ConfigProperties emptyBridge; + + @BeforeEach + void setup() { + bridge = create(new DeclarativeConfigPropertiesBridgeBuilder()); + + OpenTelemetryConfigurationModel emptyModel = + new OpenTelemetryConfigurationModel() + .withAdditionalProperty("instrumentation/development", new InstrumentationModel()); + SdkConfigProvider emptyConfigProvider = SdkConfigProvider.create(emptyModel); + emptyBridge = + new DeclarativeConfigPropertiesBridgeBuilder() + .buildFromInstrumentationConfig( + Objects.requireNonNull(emptyConfigProvider.getInstrumentationConfig())); + } + + private static ConfigProperties create(DeclarativeConfigPropertiesBridgeBuilder builder) { + OpenTelemetryConfigurationModel model = + DeclarativeConfiguration.parse( + DeclarativeConfigPropertiesBridgeTest.class + .getClassLoader() + .getResourceAsStream("config.yaml")); + return builder.buildFromInstrumentationConfig( + SdkConfigProvider.create(model).getInstrumentationConfig()); + } + + @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.runtime-telemetry.enabled")).isFalse(); + + // check all the types + Map 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); + } + + @Test + void vendor() { + // verify vendor specific property names are preserved in unchanged form (prefix is not stripped + // as for otel.instrumentation.*) + assertThat(bridge.getBoolean("acme.full_name.preserved")).isTrue(); + } + + @Test + void vendorTranslation() { + ConfigProperties propertiesBridge = + create(new DeclarativeConfigPropertiesBridgeBuilder().addMapping("acme", "acme.full_name")); + assertThat(propertiesBridge.getBoolean("acme.preserved")).isTrue(); + } + + @Test + void agentTranslation() { + ConfigProperties bridge = + create( + new DeclarativeConfigPropertiesBridgeBuilder() + .addMapping("otel.javaagent", "agent") + .addOverride("otel.javaagent.debug", true) + .addOverride("otel.javaagent.logging", "application")); + + assertThat(bridge.getBoolean("otel.javaagent.debug")).isTrue(); + assertThat(bridge.getBoolean("otel.javaagent.experimental.indy")).isTrue(); + assertThat(bridge.getString("otel.javaagent.logging")).isEqualTo("application"); + } +} diff --git a/declarative-config-bridge/src/test/resources/config.yaml b/declarative-config-bridge/src/test/resources/config.yaml new file mode 100644 index 000000000..6c8d7fddc --- /dev/null +++ b/declarative-config-bridge/src/test/resources/config.yaml @@ -0,0 +1,27 @@ +file_format: 1.0-rc.1 +instrumentation/development: + java: + acme: + full_name: + preserved: true + agent: + experimental: + indy: true + common: + default: + enabled: false + runtime_telemetry: + enabled: false + example_instrumentation: + string_key: value + bool_key: true + int_key: 1 + double_key: 1.1 + list_key: + - value1 + - value2 + - true + map_key: + string_key1: value1 + string_key2: value2 + bool_key: true diff --git a/inferred-spans/build.gradle.kts b/inferred-spans/build.gradle.kts index 98d5e33a3..bdf94a906 100644 --- a/inferred-spans/build.gradle.kts +++ b/inferred-spans/build.gradle.kts @@ -9,10 +9,13 @@ description = "OpenTelemetry Java profiling based inferred spans module" otelJava.moduleName.set("io.opentelemetry.contrib.inferredspans") dependencies { + implementation(project(":declarative-config-bridge")) + annotationProcessor("com.google.auto.service:auto-service") compileOnly("com.google.auto.service:auto-service-annotations") compileOnly("io.opentelemetry:opentelemetry-sdk") - compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator") compileOnly("io.opentelemetry.semconv:opentelemetry-semconv") implementation("com.lmax:disruptor") implementation("org.jctools:jctools-core") @@ -25,6 +28,7 @@ dependencies { testImplementation("io.opentelemetry.semconv:opentelemetry-semconv") testImplementation("io.opentelemetry:opentelemetry-sdk") testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") testImplementation("io.opentelemetry:opentelemetry-api-incubator") testImplementation("io.opentelemetry:opentelemetry-exporter-logging") diff --git a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfig.java b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfig.java index 9c8118ec5..ff71dfba5 100644 --- a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfig.java +++ b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfig.java @@ -5,69 +5,24 @@ package io.opentelemetry.contrib.inferredspans; +import static io.opentelemetry.contrib.inferredspans.InferredSpansConfig.ENABLED_OPTION; + import com.google.auto.service.AutoService; -import io.opentelemetry.api.trace.SpanBuilder; -import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import java.time.Duration; -import java.util.Arrays; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.logging.Logger; -import java.util.stream.Collectors; -import javax.annotation.Nullable; @AutoService(AutoConfigurationCustomizerProvider.class) public class InferredSpansAutoConfig implements AutoConfigurationCustomizerProvider { private static final Logger log = Logger.getLogger(InferredSpansAutoConfig.class.getName()); - static final String ENABLED_OPTION = "otel.inferred.spans.enabled"; - static final String LOGGING_OPTION = "otel.inferred.spans.logging.enabled"; - static final String DIAGNOSTIC_FILES_OPTION = "otel.inferred.spans.backup.diagnostic.files"; - static final String SAFEMODE_OPTION = "otel.inferred.spans.safe.mode"; - static final String POSTPROCESSING_OPTION = "otel.inferred.spans.post.processing.enabled"; - static final String SAMPLING_INTERVAL_OPTION = "otel.inferred.spans.sampling.interval"; - static final String MIN_DURATION_OPTION = "otel.inferred.spans.min.duration"; - static final String INCLUDED_CLASSES_OPTION = "otel.inferred.spans.included.classes"; - static final String EXCLUDED_CLASSES_OPTION = "otel.inferred.spans.excluded.classes"; - static final String INTERVAL_OPTION = "otel.inferred.spans.interval"; - static final String DURATION_OPTION = "otel.inferred.spans.duration"; - static final String LIB_DIRECTORY_OPTION = "otel.inferred.spans.lib.directory"; - static final String PARENT_OVERRIDE_HANDLER_OPTION = - "otel.inferred.spans.parent.override.handler"; - @Override public void customize(AutoConfigurationCustomizer config) { config.addTracerProviderCustomizer( (providerBuilder, properties) -> { if (properties.getBoolean(ENABLED_OPTION, false)) { - InferredSpansProcessorBuilder builder = InferredSpansProcessor.builder(); - - PropertiesApplier applier = new PropertiesApplier(properties); - - applier.applyBool(LOGGING_OPTION, builder::profilerLoggingEnabled); - applier.applyBool(DIAGNOSTIC_FILES_OPTION, builder::backupDiagnosticFiles); - applier.applyInt(SAFEMODE_OPTION, builder::asyncProfilerSafeMode); - applier.applyBool(POSTPROCESSING_OPTION, builder::postProcessingEnabled); - applier.applyDuration(SAMPLING_INTERVAL_OPTION, builder::samplingInterval); - applier.applyDuration(MIN_DURATION_OPTION, builder::inferredSpansMinDuration); - applier.applyWildcards(INCLUDED_CLASSES_OPTION, builder::includedClasses); - applier.applyWildcards(EXCLUDED_CLASSES_OPTION, builder::excludedClasses); - applier.applyDuration(INTERVAL_OPTION, builder::profilerInterval); - applier.applyDuration(DURATION_OPTION, builder::profilingDuration); - applier.applyString(LIB_DIRECTORY_OPTION, builder::profilerLibDirectory); - - String parentOverrideHandlerName = properties.getString(PARENT_OVERRIDE_HANDLER_OPTION); - if (parentOverrideHandlerName != null && !parentOverrideHandlerName.isEmpty()) { - builder.parentOverrideHandler( - constructParentOverrideHandler(parentOverrideHandlerName)); - } - - providerBuilder.addSpanProcessor(builder.build()); + providerBuilder.addSpanProcessor(InferredSpansConfig.create(properties)); } else { log.finest( "Not enabling inferred spans processor because " + ENABLED_OPTION + " is not set"); @@ -75,59 +30,4 @@ public void customize(AutoConfigurationCustomizer config) { return providerBuilder; }); } - - @SuppressWarnings("unchecked") - private static BiConsumer constructParentOverrideHandler(String name) { - try { - Class clazz = Class.forName(name); - return (BiConsumer) clazz.getConstructor().newInstance(); - } catch (Exception e) { - throw new IllegalArgumentException("Could not construct parent override handler", e); - } - } - - private static class PropertiesApplier { - - private final ConfigProperties properties; - - PropertiesApplier(ConfigProperties properties) { - this.properties = properties; - } - - void applyBool(String configKey, Consumer funcToApply) { - applyValue(properties.getBoolean(configKey), funcToApply); - } - - void applyInt(String configKey, Consumer funcToApply) { - applyValue(properties.getInt(configKey), funcToApply); - } - - void applyDuration(String configKey, Consumer funcToApply) { - applyValue(properties.getDuration(configKey), funcToApply); - } - - void applyString(String configKey, Consumer funcToApply) { - applyValue(properties.getString(configKey), funcToApply); - } - - void applyWildcards(String configKey, Consumer> funcToApply) { - String wildcardListString = properties.getString(configKey); - if (wildcardListString != null && !wildcardListString.isEmpty()) { - List values = - Arrays.stream(wildcardListString.split(",")) - .filter(str -> !str.isEmpty()) - .map(WildcardMatcher::valueOf) - .collect(Collectors.toList()); - if (!values.isEmpty()) { - funcToApply.accept(values); - } - } - } - - private static void applyValue(@Nullable T value, Consumer funcToApply) { - if (value != null) { - funcToApply.accept(value); - } - } - } } diff --git a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansComponentProvider.java b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansComponentProvider.java new file mode 100644 index 000000000..287b067e2 --- /dev/null +++ b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansComponentProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.inferredspans; + +import com.google.auto.service.AutoService; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.contrib.sdk.config.bridge.DeclarativeConfigPropertiesBridgeBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.trace.SpanProcessor; + +@SuppressWarnings("rawtypes") +@AutoService(ComponentProvider.class) +public class InferredSpansComponentProvider implements ComponentProvider { + + @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 getType() { + return SpanProcessor.class; + } +} diff --git a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansConfig.java b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansConfig.java new file mode 100644 index 000000000..73f9738e3 --- /dev/null +++ b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansConfig.java @@ -0,0 +1,119 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.inferredspans; + +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.trace.SpanProcessor; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +class InferredSpansConfig { + + private InferredSpansConfig() {} + + static final String ENABLED_OPTION = "otel.inferred.spans.enabled"; + static final String LOGGING_OPTION = "otel.inferred.spans.logging.enabled"; + static final String DIAGNOSTIC_FILES_OPTION = "otel.inferred.spans.backup.diagnostic.files"; + static final String SAFEMODE_OPTION = "otel.inferred.spans.safe.mode"; + static final String POSTPROCESSING_OPTION = "otel.inferred.spans.post.processing.enabled"; + static final String SAMPLING_INTERVAL_OPTION = "otel.inferred.spans.sampling.interval"; + static final String MIN_DURATION_OPTION = "otel.inferred.spans.min.duration"; + static final String INCLUDED_CLASSES_OPTION = "otel.inferred.spans.included.classes"; + static final String EXCLUDED_CLASSES_OPTION = "otel.inferred.spans.excluded.classes"; + static final String INTERVAL_OPTION = "otel.inferred.spans.interval"; + static final String DURATION_OPTION = "otel.inferred.spans.duration"; + static final String LIB_DIRECTORY_OPTION = "otel.inferred.spans.lib.directory"; + static final String PARENT_OVERRIDE_HANDLER_OPTION = + "otel.inferred.spans.parent.override.handler"; + + static SpanProcessor create(ConfigProperties properties) { + InferredSpansProcessorBuilder builder = InferredSpansProcessor.builder(); + + PropertiesApplier applier = new PropertiesApplier(properties); + + applier.applyBool(ENABLED_OPTION, builder::profilerEnabled); + applier.applyBool(LOGGING_OPTION, builder::profilerLoggingEnabled); + applier.applyBool(DIAGNOSTIC_FILES_OPTION, builder::backupDiagnosticFiles); + applier.applyInt(SAFEMODE_OPTION, builder::asyncProfilerSafeMode); + applier.applyBool(POSTPROCESSING_OPTION, builder::postProcessingEnabled); + applier.applyDuration(SAMPLING_INTERVAL_OPTION, builder::samplingInterval); + applier.applyDuration(MIN_DURATION_OPTION, builder::inferredSpansMinDuration); + applier.applyWildcards(INCLUDED_CLASSES_OPTION, builder::includedClasses); + applier.applyWildcards(EXCLUDED_CLASSES_OPTION, builder::excludedClasses); + applier.applyDuration(INTERVAL_OPTION, builder::profilerInterval); + applier.applyDuration(DURATION_OPTION, builder::profilingDuration); + applier.applyString(LIB_DIRECTORY_OPTION, builder::profilerLibDirectory); + + String parentOverrideHandlerName = properties.getString(PARENT_OVERRIDE_HANDLER_OPTION); + if (parentOverrideHandlerName != null && !parentOverrideHandlerName.isEmpty()) { + builder.parentOverrideHandler(constructParentOverrideHandler(parentOverrideHandlerName)); + } + + return builder.build(); + } + + @SuppressWarnings("unchecked") + private static BiConsumer constructParentOverrideHandler(String name) { + try { + Class clazz = Class.forName(name); + return (BiConsumer) clazz.getConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException("Could not construct parent override handler", e); + } + } + + private static class PropertiesApplier { + + private final ConfigProperties properties; + + PropertiesApplier(ConfigProperties properties) { + this.properties = properties; + } + + void applyBool(String configKey, Consumer funcToApply) { + applyValue(properties.getBoolean(configKey), funcToApply); + } + + void applyInt(String configKey, Consumer funcToApply) { + applyValue(properties.getInt(configKey), funcToApply); + } + + void applyDuration(String configKey, Consumer funcToApply) { + applyValue(properties.getDuration(configKey), funcToApply); + } + + void applyString(String configKey, Consumer funcToApply) { + applyValue(properties.getString(configKey), funcToApply); + } + + void applyWildcards(String configKey, Consumer> funcToApply) { + String wildcardListString = properties.getString(configKey); + if (wildcardListString != null && !wildcardListString.isEmpty()) { + List values = + Arrays.stream(wildcardListString.split(",")) + .filter(str -> !str.isEmpty()) + .map(WildcardMatcher::valueOf) + .collect(Collectors.toList()); + if (!values.isEmpty()) { + funcToApply.accept(values); + } + } + } + + private static void applyValue(@Nullable T value, Consumer funcToApply) { + if (value != null) { + funcToApply.accept(value); + } + } + } +} diff --git a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansProcessor.java b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansProcessor.java index 22f59ba53..b96163b48 100644 --- a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansProcessor.java +++ b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansProcessor.java @@ -49,7 +49,8 @@ public class InferredSpansProcessor implements SpanProcessor { boolean startScheduledProfiling, @Nullable File activationEventsFile, @Nullable File jfrFile) { - profiler = new SamplingProfiler(config, clock, this::getTracer, activationEventsFile, jfrFile); + profiler = + new SamplingProfiler(config, clock, this::getTracer, activationEventsFile, jfrFile, null); if (startScheduledProfiling) { profiler.start(); } diff --git a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansProcessorBuilder.java b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansProcessorBuilder.java index b464f0f42..33ec9e237 100644 --- a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansProcessorBuilder.java +++ b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansProcessorBuilder.java @@ -19,6 +19,7 @@ @SuppressWarnings("CanIgnoreReturnValueSuggester") public class InferredSpansProcessorBuilder { + private boolean enabled = true; private boolean profilerLoggingEnabled = true; private boolean backupDiagnosticFiles = false; private int asyncProfilerSafeMode = 0; @@ -60,6 +61,7 @@ public class InferredSpansProcessorBuilder { public InferredSpansProcessor build() { InferredSpansConfiguration config = new InferredSpansConfiguration( + enabled, profilerLoggingEnabled, backupDiagnosticFiles, asyncProfilerSafeMode, @@ -76,6 +78,11 @@ public InferredSpansProcessor build() { config, clock, startScheduledProfiling, activationEventsFile, jfrFile); } + public InferredSpansProcessorBuilder profilerEnabled(boolean profilerEnabled) { + this.enabled = profilerEnabled; + return this; + } + /** * By default, async profiler prints warning messages about missing JVM symbols to standard * output. Set this option to {@code false} to suppress such messages. diff --git a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/InferredSpansConfiguration.java b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/InferredSpansConfiguration.java index 5091a36a5..111eceaaf 100644 --- a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/InferredSpansConfiguration.java +++ b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/InferredSpansConfiguration.java @@ -15,6 +15,7 @@ public class InferredSpansConfiguration { + private final boolean enabled; private final boolean profilerLoggingEnabled; private final boolean backupDiagnosticFiles; private final int asyncProfilerSafeMode; @@ -30,6 +31,7 @@ public class InferredSpansConfiguration { @SuppressWarnings("TooManyParameters") public InferredSpansConfiguration( + boolean enabled, boolean profilerLoggingEnabled, boolean backupDiagnosticFiles, int asyncProfilerSafeMode, @@ -42,6 +44,7 @@ public InferredSpansConfiguration( Duration profilingDuration, @Nullable String profilerLibDirectory, BiConsumer parentOverrideHandler) { + this.enabled = enabled; this.profilerLoggingEnabled = profilerLoggingEnabled; this.backupDiagnosticFiles = backupDiagnosticFiles; this.asyncProfilerSafeMode = asyncProfilerSafeMode; @@ -56,6 +59,10 @@ public InferredSpansConfiguration( this.parentOverrideHandler = parentOverrideHandler; } + public boolean isEnabled() { + return enabled; + } + public boolean isProfilingLoggingEnabled() { return profilerLoggingEnabled; } diff --git a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/SamplingProfiler.java b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/SamplingProfiler.java index 61e5f75a7..1dc67e71c 100644 --- a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/SamplingProfiler.java +++ b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/SamplingProfiler.java @@ -149,6 +149,7 @@ public class SamplingProfiler implements Runnable { private final ProfilingActivationListener activationListener; private final Supplier tracerProvider; + @Nullable private final File tempDir; private final AsyncProfiler profiler; @@ -168,9 +169,11 @@ public SamplingProfiler( SpanAnchoredClock nanoClock, Supplier tracerProvider, @Nullable File activationEventsFile, - @Nullable File jfrFile) { + @Nullable File jfrFile, + @Nullable File tempDir) { this.config = config; this.tracerProvider = tracerProvider; + this.tempDir = tempDir; this.scheduler = Executors.newSingleThreadScheduledExecutor( r -> { @@ -250,12 +253,13 @@ boolean isProfilingActiveOnThread(Thread thread) { private synchronized void createFilesIfRequired() throws IOException { if (jfrFile == null || !jfrFile.exists()) { - jfrFile = File.createTempFile("otel-inferred-traces-", ".jfr"); + jfrFile = File.createTempFile("otel-inferred-traces-", ".jfr", tempDir); jfrFile.deleteOnExit(); canDeleteJfrFile = true; } if (activationEventsFile == null || !activationEventsFile.exists()) { - activationEventsFile = File.createTempFile("otel-inferred-activation-events-", ".bin"); + activationEventsFile = + File.createTempFile("otel-inferred-activation-events-", ".bin", tempDir); activationEventsFile.deleteOnExit(); canDeleteActivationEventsFile = true; } @@ -350,6 +354,10 @@ public boolean onDeactivation(Span deactivatedSpan, @Nullable Span previouslyAct @Override @SuppressWarnings("FutureReturnValueIgnored") public void run() { + if (!config.isEnabled()) { + logger.fine("Profiling is disabled, not starting profiling session"); + return; + } // lazily create temporary files try { diff --git a/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfigTest.java b/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfigTest.java index 76a6333ba..29c6b0961 100644 --- a/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfigTest.java +++ b/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfigTest.java @@ -56,20 +56,20 @@ public void checkAllOptions(@TempDir Path tmpDir) { String libDir = tmpDir.resolve("foo").resolve("bar").toString(); try (AutoConfigTestProperties props = new AutoConfigTestProperties() - .put(InferredSpansAutoConfig.ENABLED_OPTION, "true") - .put(InferredSpansAutoConfig.LOGGING_OPTION, "false") - .put(InferredSpansAutoConfig.DIAGNOSTIC_FILES_OPTION, "true") - .put(InferredSpansAutoConfig.SAFEMODE_OPTION, "16") - .put(InferredSpansAutoConfig.POSTPROCESSING_OPTION, "false") - .put(InferredSpansAutoConfig.SAMPLING_INTERVAL_OPTION, "7ms") - .put(InferredSpansAutoConfig.MIN_DURATION_OPTION, "2ms") - .put(InferredSpansAutoConfig.INCLUDED_CLASSES_OPTION, "foo*23,bar.baz") - .put(InferredSpansAutoConfig.EXCLUDED_CLASSES_OPTION, "blub,test*.test2") - .put(InferredSpansAutoConfig.INTERVAL_OPTION, "2s") - .put(InferredSpansAutoConfig.DURATION_OPTION, "3s") - .put(InferredSpansAutoConfig.LIB_DIRECTORY_OPTION, libDir) + .put(InferredSpansConfig.ENABLED_OPTION, "true") + .put(InferredSpansConfig.LOGGING_OPTION, "false") + .put(InferredSpansConfig.DIAGNOSTIC_FILES_OPTION, "true") + .put(InferredSpansConfig.SAFEMODE_OPTION, "16") + .put(InferredSpansConfig.POSTPROCESSING_OPTION, "false") + .put(InferredSpansConfig.SAMPLING_INTERVAL_OPTION, "7ms") + .put(InferredSpansConfig.MIN_DURATION_OPTION, "2ms") + .put(InferredSpansConfig.INCLUDED_CLASSES_OPTION, "foo*23,bar.baz") + .put(InferredSpansConfig.EXCLUDED_CLASSES_OPTION, "blub,test*.test2") + .put(InferredSpansConfig.INTERVAL_OPTION, "2s") + .put(InferredSpansConfig.DURATION_OPTION, "3s") + .put(InferredSpansConfig.LIB_DIRECTORY_OPTION, libDir) .put( - InferredSpansAutoConfig.PARENT_OVERRIDE_HANDLER_OPTION, + InferredSpansConfig.PARENT_OVERRIDE_HANDLER_OPTION, NoOpParentOverrideHandler.class.getName())) { OpenTelemetry otel = GlobalOpenTelemetry.get(); @@ -112,10 +112,10 @@ public void checkDisabledbyDefault() { public void checkProfilerWorking() { try (AutoConfigTestProperties props = new AutoConfigTestProperties() - .put(InferredSpansAutoConfig.ENABLED_OPTION, "true") - .put(InferredSpansAutoConfig.DURATION_OPTION, "500ms") - .put(InferredSpansAutoConfig.INTERVAL_OPTION, "500ms") - .put(InferredSpansAutoConfig.SAMPLING_INTERVAL_OPTION, "5ms")) { + .put(InferredSpansConfig.ENABLED_OPTION, "true") + .put(InferredSpansConfig.DURATION_OPTION, "500ms") + .put(InferredSpansConfig.INTERVAL_OPTION, "500ms") + .put(InferredSpansConfig.SAMPLING_INTERVAL_OPTION, "5ms")) { OpenTelemetry otel = GlobalOpenTelemetry.get(); List processors = OtelReflectionUtils.getSpanProcessors(otel); assertThat(processors).filteredOn(proc -> proc instanceof InferredSpansProcessor).hasSize(1); diff --git a/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/InferredSpansCustomizerProviderTest.java b/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/InferredSpansCustomizerProviderTest.java new file mode 100644 index 000000000..45f61b1f8 --- /dev/null +++ b/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/InferredSpansCustomizerProviderTest.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.inferredspans; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.condition.OS.WINDOWS; + +import io.opentelemetry.contrib.inferredspans.internal.ProfilingActivationListener; +import io.opentelemetry.contrib.inferredspans.internal.util.OtelReflectionUtils; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; + +@DisabledOnOs(WINDOWS) // Uses async-profiler, which is not supported on Windows +class InferredSpansCustomizerProviderTest { + + @BeforeEach + @AfterEach + public void resetGlobalOtel() { + ProfilingActivationListener.ensureInitialized(); + OtelReflectionUtils.shutdownAndResetGlobalOtel(); + } + + @Test + void declarativeConfig() { + String yaml = + "file_format: 1.0-rc.1\n" + + "tracer_provider:\n" + + " processors:\n" + + " - inferred_spans:\n" + + " enabled: false\n" + + " backup:\n" + + " diagnostic:\n" + + " files: true\n"; + + OpenTelemetrySdk sdk = + DeclarativeConfiguration.parseAndCreate( + new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8))); + + assertThat(sdk) + .extracting("tracerProvider") + .extracting("delegate") + .extracting("sharedState") + .extracting("activeSpanProcessor") + .extracting("profiler") + .extracting("config") + .extracting("backupDiagnosticFiles") + .isEqualTo(true); + } +} diff --git a/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/internal/SamplingProfilerTest.java b/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/internal/SamplingProfilerTest.java index b97ce8729..1e9d97807 100644 --- a/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/internal/SamplingProfilerTest.java +++ b/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/internal/SamplingProfilerTest.java @@ -32,12 +32,12 @@ import java.util.stream.Stream; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledForJreRange; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; // async-profiler doesn't work on Windows @DisabledOnOs(OS.WINDOWS) @@ -46,11 +46,7 @@ class SamplingProfilerTest { private ProfilerTestSetup setup; - @BeforeEach - void setup() { - // avoids any test failure to make other tests to fail - getProfilerTempFiles().forEach(SamplingProfilerTest::silentDeleteFile); - } + @TempDir private Path tempDir; @AfterEach void tearDown() { @@ -58,7 +54,6 @@ void tearDown() { setup.close(); setup = null; } - getProfilerTempFiles().forEach(SamplingProfilerTest::silentDeleteFile); } @Test @@ -117,8 +112,8 @@ void shouldNotDeleteProvidedFiles() throws Exception { defaultConfig = ProfilerTestSetup.extractProfilerImpl(profiler1).getConfig(); } - Path tempFile1 = Files.createTempFile("otel-inferred-provided", "test.bin"); - Path tempFile2 = Files.createTempFile("otel-inferred-provided", "test.jfr"); + Path tempFile1 = Files.createTempFile(tempDir, "otel-inferred-provided", "test.bin"); + Path tempFile2 = Files.createTempFile(tempDir, "otel-inferred-provided", "test.jfr"); try (OpenTelemetrySdk sdk = OpenTelemetrySdk.builder().build()) { @@ -128,7 +123,8 @@ void shouldNotDeleteProvidedFiles() throws Exception { new FixedClock(), () -> sdk.getTracer("my-tracer"), tempFile1.toFile(), - tempFile2.toFile()); + tempFile2.toFile(), + tempDir.toFile()); otherProfiler.start(); awaitProfilerStarted(otherProfiler); @@ -341,12 +337,4 @@ private static void awaitProfilerStarted(SamplingProfiler profiler) { .timeout(Duration.ofSeconds(6)) .until(() -> profiler.getProfilingSessions() > 1); } - - private static void silentDeleteFile(Path f) { - try { - Files.delete(f); - } catch (IOException e) { - throw new IllegalStateException(e); - } - } } diff --git a/processors/src/test/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanBridgeComponentProviderTest.java b/processors/src/test/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanBridgeComponentProviderTest.java index 55746e61d..1e503a51c 100644 --- a/processors/src/test/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanBridgeComponentProviderTest.java +++ b/processors/src/test/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanBridgeComponentProviderTest.java @@ -18,7 +18,7 @@ class EventToSpanBridgeComponentProviderTest { @Test void endToEnd() { String yaml = - "file_format: 0.4\n" + "file_format: 1.0-rc.1\n" + "logger_provider:\n" + " processors:\n" + " - event_to_span_event_bridge:\n"; diff --git a/samplers/src/test/java/internal/RuleBasedRoutingSamplerComponentProviderTest.java b/samplers/src/test/java/internal/RuleBasedRoutingSamplerComponentProviderTest.java index 02611c9d5..c39603352 100644 --- a/samplers/src/test/java/internal/RuleBasedRoutingSamplerComponentProviderTest.java +++ b/samplers/src/test/java/internal/RuleBasedRoutingSamplerComponentProviderTest.java @@ -38,7 +38,7 @@ class RuleBasedRoutingSamplerComponentProviderTest { @Test void endToEnd() { String yaml = - "file_format: 0.4\n" + "file_format: 1.0-rc.1\n" + "tracer_provider:\n" + " sampler:\n" + " parent_based:\n" diff --git a/settings.gradle.kts b/settings.gradle.kts index ba79c4693..700c0fb60 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,6 +43,7 @@ include(":baggage-processor") include(":compressors:compressor-zstd") include(":cloudfoundry-resources") include(":consistent-sampling") +include(":declarative-config-bridge") include(":dependencyManagement") include(":disk-buffering") include(":example")