From e4d4d2fe81773c651b52406c36ba27f1cce5bd4b Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Fri, 25 Sep 2026 16:26:31 -0500 Subject: [PATCH 1/3] Add experimental SdkResourceProvider implementation --- .../all/NoSharedInternalCodeTest.java | 1 + sdk/all/build.gradle.kts | 17 +++ .../internal/ExtendedOpenTelemetrySdk.java | 46 +++++++ .../ExtendedOpenTelemetrySdkTest.java | 108 ++++++++++++++++ .../internal/ConstantResourceDetector.java | 41 +++++++ .../resources/internal/ResourceDetector.java | 58 +++++++++ .../internal/SdkResourceProvider.java | 70 +++++++++++ .../internal/SdkResourceProviderBuilder.java | 50 ++++++++ .../internal/ResourceDetectorTest.java | 55 +++++++++ .../internal/SdkResourceProviderTest.java | 116 ++++++++++++++++++ .../sdk/logs/LoggerSharedState.java | 9 +- .../sdk/logs/SdkLoggerProvider.java | 19 ++- .../sdk/logs/SdkLoggerProviderBuilder.java | 30 ++++- .../logs/internal/SdkLoggerProviderUtil.java | 28 +++++ .../sdk/logs/LoggerSharedStateTest.java | 3 +- .../logs/SdkLoggerProviderBuilderTest.java | 57 ++++++++- .../sdk/metrics/SdkMeterProvider.java | 18 ++- .../sdk/metrics/SdkMeterProviderBuilder.java | 30 ++++- .../internal/SdkMeterProviderUtil.java | 28 +++++ .../state/MeterProviderSharedState.java | 14 ++- .../sdk/metrics/InstrumentBuilderTest.java | 3 +- .../metrics/SdkMeterProviderBuilderTest.java | 61 +++++++++ .../sdk/trace/SdkTracerProvider.java | 17 ++- .../sdk/trace/SdkTracerProviderBuilder.java | 30 ++++- .../sdk/trace/TracerSharedState.java | 9 +- .../trace/internal/SdkTracerProviderUtil.java | 28 +++++ .../trace/SdkTracerProviderBuilderTest.java | 57 ++++++++- .../sdk/trace/SdkTracerProviderTest.java | 10 +- 28 files changed, 977 insertions(+), 36 deletions(-) create mode 100644 sdk/all/src/testIncubating/java/io/opentelemetry/sdk/internal/ExtendedOpenTelemetrySdkTest.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetector.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ResourceDetector.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProvider.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderBuilder.java create mode 100644 sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ResourceDetectorTest.java create mode 100644 sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderTest.java diff --git a/all/src/test/java/io/opentelemetry/all/NoSharedInternalCodeTest.java b/all/src/test/java/io/opentelemetry/all/NoSharedInternalCodeTest.java index 764061b4834..1b1371321a0 100644 --- a/all/src/test/java/io/opentelemetry/all/NoSharedInternalCodeTest.java +++ b/all/src/test/java/io/opentelemetry/all/NoSharedInternalCodeTest.java @@ -40,6 +40,7 @@ class NoSharedInternalCodeTest { "opentelemetry-exporter-prometheus", "opentelemetry-extension-trace-propagators", "opentelemetry-opencensus-shim", + "opentelemetry-sdk", "opentelemetry-sdk-common", "opentelemetry-sdk-logs", "opentelemetry-sdk-metrics", diff --git a/sdk/all/build.gradle.kts b/sdk/all/build.gradle.kts index 44d638799fe..5ef583575dc 100644 --- a/sdk/all/build.gradle.kts +++ b/sdk/all/build.gradle.kts @@ -30,3 +30,20 @@ dependencies { jmh(project(":sdk:testing")) jmh(project(":api:incubator")) } + +testing { + suites { + register("testIncubating") { + dependencies { + implementation(project(":api:incubator")) + implementation(project(":sdk:testing")) + } + } + } +} + +tasks { + check { + dependsOn(testing.suites) + } +} diff --git a/sdk/all/src/main/java/io/opentelemetry/sdk/internal/ExtendedOpenTelemetrySdk.java b/sdk/all/src/main/java/io/opentelemetry/sdk/internal/ExtendedOpenTelemetrySdk.java index dc89b116108..1b5160a3f77 100644 --- a/sdk/all/src/main/java/io/opentelemetry/sdk/internal/ExtendedOpenTelemetrySdk.java +++ b/sdk/all/src/main/java/io/opentelemetry/sdk/internal/ExtendedOpenTelemetrySdk.java @@ -9,6 +9,15 @@ import io.opentelemetry.api.incubator.config.ConfigProvider; import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.internal.SdkLoggerProviderUtil; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.internal.SdkTracerProviderUtil; +import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; /** @@ -23,6 +32,7 @@ public final class ExtendedOpenTelemetrySdk extends OpenTelemetrySdk private final OpenTelemetrySdk openTelemetrySdk; private final ObfuscatedConfigProvider configProvider; + @Nullable private final SdkResourceProvider resourceProvider; private ExtendedOpenTelemetrySdk( OpenTelemetrySdk openTelemetrySdk, SdkConfigProvider configProvider) { @@ -33,6 +43,7 @@ private ExtendedOpenTelemetrySdk( openTelemetrySdk.getPropagators()); this.openTelemetrySdk = openTelemetrySdk; this.configProvider = new ObfuscatedConfigProvider(configProvider); + this.resourceProvider = resolveSdkResourceProvider(openTelemetrySdk); } public static ExtendedOpenTelemetrySdk create( @@ -50,6 +61,39 @@ public SdkConfigProvider getSdkConfigProvider() { return configProvider.unobfuscate(); } + /** + * Returns the {@link SdkResourceProvider} shared by the SDK's tracer, meter, and logger + * providers. + * + * @throws IllegalStateException if the three signal providers were not configured with equivalent + * resources. + */ + public SdkResourceProvider getSdkResourceProvider() { + if (resourceProvider == null) { + throw new IllegalStateException( + "SdkTracerProvider, SdkMeterProvider, and SdkLoggerProvider are configured with" + + " different resources. Configure all three signal providers with the same" + + " SdkResourceProvider (or the same Resource) to resolve this."); + } + return resourceProvider; + } + + @Nullable + private static SdkResourceProvider resolveSdkResourceProvider(OpenTelemetrySdk sdk) { + SdkTracerProvider tracerProvider = sdk.getSdkTracerProvider(); + SdkMeterProvider meterProvider = sdk.getSdkMeterProvider(); + SdkLoggerProvider loggerProvider = sdk.getSdkLoggerProvider(); + SdkResourceProvider tracerRp = SdkTracerProviderUtil.getSdkResourceProvider(tracerProvider); + SdkResourceProvider meterRp = SdkMeterProviderUtil.getSdkResourceProvider(meterProvider); + SdkResourceProvider loggerRp = SdkLoggerProviderUtil.getSdkResourceProvider(loggerProvider); + Resource tracerResource = tracerRp.getResource(); + if (tracerResource.equals(meterRp.getResource()) + && tracerResource.equals(loggerRp.getResource())) { + return tracerRp; + } + return null; + } + @Override public String toString() { return "ExtendedOpenTelemetrySdk{" @@ -57,6 +101,8 @@ public String toString() { + openTelemetrySdk + ", configProvider=" + configProvider.unobfuscate() + + ", resourceProvider=" + + resourceProvider + "}"; } diff --git a/sdk/all/src/testIncubating/java/io/opentelemetry/sdk/internal/ExtendedOpenTelemetrySdkTest.java b/sdk/all/src/testIncubating/java/io/opentelemetry/sdk/internal/ExtendedOpenTelemetrySdkTest.java new file mode 100644 index 00000000000..eaf620933be --- /dev/null +++ b/sdk/all/src/testIncubating/java/io/opentelemetry/sdk/internal/ExtendedOpenTelemetrySdkTest.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; +import io.opentelemetry.sdk.logs.internal.SdkLoggerProviderUtil; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.internal.SdkTracerProviderUtil; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +class ExtendedOpenTelemetrySdkTest { + + private static final Resource RESOURCE_A = + Resource.create(Attributes.of(AttributeKey.stringKey("a"), "1")); + private static final Resource RESOURCE_B = + Resource.create(Attributes.of(AttributeKey.stringKey("b"), "2")); + + @Test + void getSdkResourceProvider_defaultResources_returnsAutoWrapped() { + ExtendedOpenTelemetrySdk sdk = buildExtended(null, null, null, null, null, null); + + assertThat(sdk.getSdkResourceProvider().getResource()).isEqualTo(Resource.getDefault()); + } + + @Test + void getSdkResourceProvider_sameSdkResourceProviderOnAll_returnsIt() { + SdkResourceProvider resourceProvider = SdkResourceProvider.create(RESOURCE_A); + ExtendedOpenTelemetrySdk sdk = + buildExtended(null, resourceProvider, null, resourceProvider, null, resourceProvider); + + assertThat(sdk.getSdkResourceProvider()).isSameAs(resourceProvider); + } + + @Test + void getSdkResourceProvider_equivalentAutoWrappedResources_returnsFirst() { + // Distinct RP instances resolving to the same Resource: composite check is equal-by-Resource + // so this must succeed. + ExtendedOpenTelemetrySdk sdk = + buildExtended(RESOURCE_A, null, RESOURCE_A, null, RESOURCE_A, null); + + assertThat(sdk.getSdkResourceProvider().getResource()).isEqualTo(RESOURCE_A); + } + + @Test + void getSdkResourceProvider_mismatchedResources_throws() { + ExtendedOpenTelemetrySdk sdk = + buildExtended(RESOURCE_A, null, RESOURCE_B, null, RESOURCE_A, null); + + assertThatThrownBy(sdk::getSdkResourceProvider) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("configured with different"); + } + + private static ExtendedOpenTelemetrySdk buildExtended( + @Nullable Resource tracerResource, + @Nullable SdkResourceProvider tracerRp, + @Nullable Resource meterResource, + @Nullable SdkResourceProvider meterRp, + @Nullable Resource loggerResource, + @Nullable SdkResourceProvider loggerRp) { + SdkTracerProviderBuilder tracerBuilder = SdkTracerProvider.builder(); + if (tracerResource != null) { + tracerBuilder.setResource(tracerResource); + } + if (tracerRp != null) { + SdkTracerProviderUtil.setSdkResourceProvider(tracerBuilder, tracerRp); + } + SdkMeterProviderBuilder meterBuilder = + SdkMeterProvider.builder().registerMetricReader(InMemoryMetricReader.create()); + if (meterResource != null) { + meterBuilder.setResource(meterResource); + } + if (meterRp != null) { + SdkMeterProviderUtil.setSdkResourceProvider(meterBuilder, meterRp); + } + SdkLoggerProviderBuilder loggerBuilder = SdkLoggerProvider.builder(); + if (loggerResource != null) { + loggerBuilder.setResource(loggerResource); + } + if (loggerRp != null) { + SdkLoggerProviderUtil.setSdkResourceProvider(loggerBuilder, loggerRp); + } + return (ExtendedOpenTelemetrySdk) + OpenTelemetrySdk.builder() + .setTracerProvider(tracerBuilder.build()) + .setMeterProvider(meterBuilder.build()) + .setLoggerProvider(loggerBuilder.build()) + .build(); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetector.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetector.java new file mode 100644 index 00000000000..680312709ed --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetector.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import io.opentelemetry.sdk.resources.Resource; +import javax.annotation.Nullable; + +final class ConstantResourceDetector implements ResourceDetector { + + @Nullable private final String name; + private final Resource resource; + + ConstantResourceDetector(@Nullable String name, Resource resource) { + this.name = name; + this.resource = resource; + } + + @Nullable + @Override + public String getName() { + return name; + } + + @Override + public Resource getResource() { + return resource; + } + + @Override + public boolean shouldReinvoke() { + return false; + } + + @Override + public String toString() { + return "ConstantResourceDetector{name=" + name + ", resource=" + resource + "}"; + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ResourceDetector.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ResourceDetector.java new file mode 100644 index 00000000000..35eaf5f08b7 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ResourceDetector.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.sdk.resources.Resource; +import javax.annotation.Nullable; + +/** + * A source of a {@link Resource} to be merged by an {@link SdkResourceProvider}. + * + *

This class is internal and experimental. Its APIs are unstable and can change at any time. Its + * APIs (or a version of them) may be promoted to the public stable API in the future, but no + * guarantees are made. + */ +public interface ResourceDetector { + + /** Returns a {@link ResourceDetector} that always returns {@code resource} and has no name. */ + static ResourceDetector constant(Resource resource) { + requireNonNull(resource, "resource"); + return new ConstantResourceDetector(null, resource); + } + + /** Returns a {@link ResourceDetector} named {@code name} that always returns {@code resource}. */ + static ResourceDetector constant(Resource resource, String name) { + requireNonNull(resource, "resource"); + requireNonNull(name, "name"); + return new ConstantResourceDetector(name, resource); + } + + /** + * Returns the name of this detector, or {@code null} if unnamed. Names are informational (e.g. + * for logging / debugging) and are not required to be unique. + */ + @Nullable + String getName(); + + /** Returns the {@link Resource} detected by this detector. */ + Resource getResource(); + + /** + * Returns whether the enclosing {@link SdkResourceProvider} must re-invoke this detector on every + * call to {@link SdkResourceProvider#getResource()}. + * + *

When {@code true}, {@link #getResource()} may be called on every {@link + * SdkResourceProvider#getResource()}. When {@code false}, this detector's result may be resolved + * once at {@link SdkResourceProvider} construction and cached. + * + *

This flag is part of the target API but has no effect on runtime behavior in the current + * iteration - all detectors are cached at construction. TTL-based cache invalidation using this + * flag is planned for a future iteration. + */ + boolean shouldReinvoke(); +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProvider.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProvider.java new file mode 100644 index 00000000000..17cf3e3585e --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProvider.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Provides the {@link Resource} used by the SDK by merging the results of a configured, ordered + * list of {@link ResourceDetector}s. + * + *

All detectors are invoked once at construction and the merged {@link Resource} is cached for + * the lifetime of this provider. {@link ResourceDetector#shouldReinvoke()} is part of the target + * API shape but has no effect on runtime behavior in this iteration. A future iteration will add + * TTL-based cache invalidation that re-invokes only {@code shouldReinvoke=true} detectors when the + * cache expires. Until then, {@code getResource()} is a single field read on the hot path. + * + *

This class is internal and experimental. Its APIs are unstable and can change at any time. Its + * APIs (or a version of them) may be promoted to the public stable API in the future, but no + * guarantees are made. + */ +public final class SdkResourceProvider { + + private final List detectors; + private final Resource cachedResource; + + SdkResourceProvider(List detectors) { + this.detectors = Collections.unmodifiableList(new ArrayList<>(detectors)); + this.cachedResource = merge(this.detectors); + } + + /** Returns a new {@link SdkResourceProviderBuilder}. */ + public static SdkResourceProviderBuilder builder() { + return new SdkResourceProviderBuilder(); + } + + /** + * Returns an {@link SdkResourceProvider} backed by a single {@linkplain + * ResourceDetector#constant(Resource) constant detector} wrapping the given {@link Resource}. + */ + public static SdkResourceProvider create(Resource resource) { + requireNonNull(resource, "resource"); + return builder().addDetector(ResourceDetector.constant(resource)).build(); + } + + /** Returns the merged {@link Resource}. */ + public Resource getResource() { + return cachedResource; + } + + private static Resource merge(List detectors) { + Resource resource = Resource.empty(); + for (ResourceDetector detector : detectors) { + resource = resource.merge(detector.getResource()); + } + return resource; + } + + @Override + public String toString() { + return "SdkResourceProvider{detectors=" + detectors + "}"; + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderBuilder.java new file mode 100644 index 00000000000..0e054da6d86 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderBuilder.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * Builder for {@link SdkResourceProvider}. + * + *

This class is internal and experimental. Its APIs are unstable and can change at any time. Its + * APIs (or a version of them) may be promoted to the public stable API in the future, but no + * guarantees are made. + */ +public final class SdkResourceProviderBuilder { + + private final List detectors = new ArrayList<>(); + + SdkResourceProviderBuilder() {} + + /** + * Adds a {@link ResourceDetector}. Detectors are merged in the order they are added, with + * later-added attributes winning on conflict (per {@link + * io.opentelemetry.sdk.resources.Resource#merge}). + */ + public SdkResourceProviderBuilder addDetector(ResourceDetector detector) { + requireNonNull(detector, "detector"); + detectors.add(detector); + return this; + } + + /** + * Returns a new {@link SdkResourceProvider}. + * + * @throws IllegalStateException if no detector has been added via {@link + * #addDetector(ResourceDetector)}. + */ + public SdkResourceProvider build() { + if (detectors.isEmpty()) { + throw new IllegalStateException( + "At least one ResourceDetector must be added via addDetector before calling build()."); + } + return new SdkResourceProvider(detectors); + } +} diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ResourceDetectorTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ResourceDetectorTest.java new file mode 100644 index 00000000000..7b5535f4b9f --- /dev/null +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ResourceDetectorTest.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.resources.Resource; +import org.junit.jupiter.api.Test; + +class ResourceDetectorTest { + + private static final Resource RESOURCE = + Resource.create(Attributes.of(AttributeKey.stringKey("k"), "v")); + + @Test + void constant_unnamed() { + ResourceDetector detector = ResourceDetector.constant(RESOURCE); + + assertThat(detector.getName()).isNull(); + assertThat(detector.getResource()).isEqualTo(RESOURCE); + assertThat(detector.shouldReinvoke()).isFalse(); + } + + @Test + void constant_named() { + ResourceDetector detector = ResourceDetector.constant(RESOURCE, "my-detector"); + + assertThat(detector.getName()).isEqualTo("my-detector"); + assertThat(detector.getResource()).isEqualTo(RESOURCE); + assertThat(detector.shouldReinvoke()).isFalse(); + } + + @Test + void constant_nullResource_throws() { + assertThatThrownBy(() -> ResourceDetector.constant(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("resource"); + assertThatThrownBy(() -> ResourceDetector.constant(null, "name")) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("resource"); + } + + @Test + void constant_nullName_throws() { + assertThatThrownBy(() -> ResourceDetector.constant(RESOURCE, null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("name"); + } +} diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderTest.java new file mode 100644 index 00000000000..530e880a8ff --- /dev/null +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderTest.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.resources.Resource; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class SdkResourceProviderTest { + + private static final Resource RESOURCE_A = + Resource.create(Attributes.of(AttributeKey.stringKey("a"), "1")); + private static final Resource RESOURCE_B = + Resource.create(Attributes.of(AttributeKey.stringKey("b"), "2")); + + @Test + void create_wrapsResourceInConstantDetector() { + SdkResourceProvider provider = SdkResourceProvider.create(RESOURCE_A); + + assertThat(provider.getResource()).isEqualTo(RESOURCE_A); + } + + @Test + void create_nullResource_throws() { + assertThatThrownBy(() -> SdkResourceProvider.create(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("resource"); + } + + @Test + void builder_singleDetector() { + SdkResourceProvider provider = + SdkResourceProvider.builder().addDetector(ResourceDetector.constant(RESOURCE_A)).build(); + + assertThat(provider.getResource()).isEqualTo(RESOURCE_A); + } + + @Test + void builder_multipleDetectors_mergedInOrder() { + SdkResourceProvider provider = + SdkResourceProvider.builder() + .addDetector(ResourceDetector.constant(RESOURCE_A)) + .addDetector(ResourceDetector.constant(RESOURCE_B)) + .build(); + + assertThat(provider.getResource()).isEqualTo(RESOURCE_A.merge(RESOURCE_B)); + } + + @Test + void builder_addDetector_nullDetector_throws() { + assertThatThrownBy(() -> SdkResourceProvider.builder().addDetector(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("detector"); + } + + @Test + void builder_build_withoutAddDetector_throws() { + assertThatThrownBy(() -> SdkResourceProvider.builder().build()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("At least one ResourceDetector"); + } + + @Test + void getResource_allDetectorsResolvedOnceAtConstruction_regardlessOfShouldReinvoke() { + // shouldReinvoke has no runtime effect in this iteration; all detectors are resolved once at + // construction and the merged Resource is cached for the lifetime of the provider. + CountingDetector constant = new CountingDetector(RESOURCE_A, /* shouldReinvoke= */ false); + CountingDetector reinvoke = new CountingDetector(RESOURCE_B, /* shouldReinvoke= */ true); + SdkResourceProvider provider = + SdkResourceProvider.builder().addDetector(constant).addDetector(reinvoke).build(); + + assertThat(constant.invocations.get()).isEqualTo(1); + assertThat(reinvoke.invocations.get()).isEqualTo(1); + + assertThat(provider.getResource()).isEqualTo(RESOURCE_A.merge(RESOURCE_B)); + assertThat(provider.getResource()).isEqualTo(RESOURCE_A.merge(RESOURCE_B)); + + assertThat(constant.invocations.get()).isEqualTo(1); + assertThat(reinvoke.invocations.get()).isEqualTo(1); + } + + private static final class CountingDetector implements ResourceDetector { + final AtomicInteger invocations = new AtomicInteger(); + private final Resource resource; + private final boolean shouldReinvoke; + + CountingDetector(Resource resource, boolean shouldReinvoke) { + this.resource = resource; + this.shouldReinvoke = shouldReinvoke; + } + + @Override + public String getName() { + return "counting"; + } + + @Override + public Resource getResource() { + invocations.incrementAndGet(); + return resource; + } + + @Override + public boolean shouldReinvoke() { + return shouldReinvoke; + } + } +} diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java index 4c4a64ea30f..95a1c6e8181 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java @@ -9,6 +9,7 @@ import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import java.util.function.Supplier; import javax.annotation.Nullable; @@ -18,7 +19,7 @@ */ final class LoggerSharedState { private final Object lock = new Object(); - private final Resource resource; + private final SdkResourceProvider resourceProvider; private final Supplier logLimitsSupplier; private final LogRecordProcessor logRecordProcessor; private final Clock clock; @@ -27,13 +28,13 @@ final class LoggerSharedState { @Nullable private volatile CompletableResultCode shutdownResult = null; LoggerSharedState( - Resource resource, + SdkResourceProvider resourceProvider, Supplier logLimitsSupplier, LogRecordProcessor logRecordProcessor, Clock clock, ExceptionAttributeResolver exceptionAttributeResolver, SdkLoggerInstrumentation loggerInstrumentation) { - this.resource = resource; + this.resourceProvider = resourceProvider; this.logLimitsSupplier = logLimitsSupplier; this.logRecordProcessor = logRecordProcessor; this.clock = clock; @@ -42,7 +43,7 @@ final class LoggerSharedState { } Resource getResource() { - return resource; + return resourceProvider.getResource(); } LogLimits getLogLimits() { diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java index db658e55bd8..099686c8c0a 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java @@ -17,7 +17,9 @@ import io.opentelemetry.sdk.common.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.common.internal.ScopeConfigurator; import io.opentelemetry.sdk.logs.internal.LoggerConfig; +import io.opentelemetry.sdk.logs.internal.SdkLoggerProviderUtil; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import java.io.Closeable; import java.util.List; import java.util.concurrent.TimeUnit; @@ -37,6 +39,7 @@ public final class SdkLoggerProvider implements LoggerProvider, Closeable { java.util.logging.Logger.getLogger(SdkLoggerProvider.class.getName()); private final LoggerSharedState sharedState; + private final SdkResourceProvider resourceProvider; private final ComponentRegistry loggerComponentRegistry; private final boolean isNoopLogRecordProcessor; @@ -54,17 +57,18 @@ public static SdkLoggerProviderBuilder builder() { } SdkLoggerProvider( - Resource resource, + SdkResourceProvider resourceProvider, Supplier logLimitsSupplier, List processors, Clock clock, ScopeConfigurator loggerConfigurator, ExceptionAttributeResolver exceptionAttributeResolver, Supplier meterProvider) { + this.resourceProvider = resourceProvider; LogRecordProcessor logRecordProcessor = LogRecordProcessor.composite(processors); this.sharedState = new LoggerSharedState( - resource, + resourceProvider, logLimitsSupplier, logRecordProcessor, clock, @@ -81,6 +85,17 @@ public static SdkLoggerProviderBuilder builder() { this.isNoopLogRecordProcessor = logRecordProcessor instanceof NoopLogRecordProcessor; } + /** + * Returns the {@link SdkResourceProvider} used to resolve the {@link Resource} attached to log + * records. + * + *

This method is experimental so not public. You may reflectively call it using {@link + * SdkLoggerProviderUtil#getSdkResourceProvider(SdkLoggerProvider)}. + */ + SdkResourceProvider getSdkResourceProvider() { + return resourceProvider; + } + private LoggerConfig getLoggerConfig(InstrumentationScopeInfo instrumentationScopeInfo) { LoggerConfig loggerConfig = loggerConfigurator.apply(instrumentationScopeInfo); return loggerConfig == null ? LoggerConfig.defaultConfig() : loggerConfig; diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java index ccd13cc5e7f..a6e54ab29e9 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java @@ -20,11 +20,13 @@ import io.opentelemetry.sdk.logs.internal.LoggerConfig; import io.opentelemetry.sdk.logs.internal.SdkLoggerProviderUtil; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.function.Predicate; import java.util.function.Supplier; +import javax.annotation.Nullable; /** * Builder class for {@link SdkLoggerProvider} instances. @@ -35,6 +37,8 @@ public final class SdkLoggerProviderBuilder { private final List logRecordProcessors = new ArrayList<>(); private Resource resource = Resource.getDefault(); + private boolean resourceExplicitlySet = false; + @Nullable private SdkResourceProvider resourceProvider; private Supplier logLimitsSupplier = LogLimits::getDefault; private Clock clock = Clock.getDefault(); private ScopeConfiguratorBuilder loggerConfiguratorBuilder = @@ -55,6 +59,7 @@ public final class SdkLoggerProviderBuilder { public SdkLoggerProviderBuilder setResource(Resource resource) { requireNonNull(resource, "resource"); this.resource = resource; + this.resourceExplicitlySet = true; return this; } @@ -67,6 +72,23 @@ public SdkLoggerProviderBuilder setResource(Resource resource) { public SdkLoggerProviderBuilder addResource(Resource resource) { Objects.requireNonNull(resource, "resource"); this.resource = this.resource.merge(resource); + this.resourceExplicitlySet = true; + return this; + } + + /** + * Sets the {@link SdkResourceProvider} used to resolve the {@link Resource} attached to all log + * records. + * + *

Mutually exclusive with {@link #setResource(Resource)} and {@link #addResource(Resource)}; + * {@link #build()} throws {@link IllegalStateException} if both are configured. + * + *

This method is experimental so not public. You may reflectively call it using {@link + * SdkLoggerProviderUtil#setSdkResourceProvider(SdkLoggerProviderBuilder, SdkResourceProvider)}. + */ + SdkLoggerProviderBuilder setSdkResourceProvider(SdkResourceProvider resourceProvider) { + requireNonNull(resourceProvider, "resourceProvider"); + this.resourceProvider = resourceProvider; return this; } @@ -207,8 +229,14 @@ public SdkLoggerProviderBuilder setMeterProvider(Supplier meterPr * @return an instance configured with the provided options */ public SdkLoggerProvider build() { + if (resourceProvider != null && resourceExplicitlySet) { + throw new IllegalStateException( + "setSdkResourceProvider is mutually exclusive with setResource / addResource."); + } + SdkResourceProvider effectiveSdkResourceProvider = + resourceProvider != null ? resourceProvider : SdkResourceProvider.create(resource); return new SdkLoggerProvider( - resource, + effectiveSdkResourceProvider, logLimitsSupplier, logRecordProcessors, clock, diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java index 9d9c911ab5d..243c3bb7ca1 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java @@ -10,6 +10,7 @@ import io.opentelemetry.sdk.common.internal.ScopeConfigurator; import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.function.Predicate; @@ -25,6 +26,33 @@ public final class SdkLoggerProviderUtil { private SdkLoggerProviderUtil() {} + /** Reflectively set the {@link SdkResourceProvider} on the {@link SdkLoggerProviderBuilder}. */ + public static void setSdkResourceProvider( + SdkLoggerProviderBuilder sdkLoggerProviderBuilder, SdkResourceProvider resourceProvider) { + try { + Method method = + SdkLoggerProviderBuilder.class.getDeclaredMethod( + "setSdkResourceProvider", SdkResourceProvider.class); + method.setAccessible(true); + method.invoke(sdkLoggerProviderBuilder, resourceProvider); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling setSdkResourceProvider on SdkLoggerProviderBuilder", e); + } + } + + /** Reflectively get the {@link SdkResourceProvider} from the {@link SdkLoggerProvider}. */ + public static SdkResourceProvider getSdkResourceProvider(SdkLoggerProvider sdkLoggerProvider) { + try { + Method method = SdkLoggerProvider.class.getDeclaredMethod("getSdkResourceProvider"); + method.setAccessible(true); + return (SdkResourceProvider) method.invoke(sdkLoggerProvider); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling getSdkResourceProvider on SdkLoggerProvider", e); + } + } + /** Reflectively set the {@link ScopeConfigurator} to the {@link SdkLoggerProvider}. */ public static void setLoggerConfigurator( SdkLoggerProvider sdkLoggerProvider, ScopeConfigurator scopeConfigurator) { diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java index 1bd98dab57d..3a6067a9498 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java @@ -15,6 +15,7 @@ import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import org.junit.jupiter.api.Test; class LoggerSharedStateTest { @@ -26,7 +27,7 @@ void shutdown() { when(logRecordProcessor.shutdown()).thenReturn(code); LoggerSharedState state = new LoggerSharedState( - Resource.empty(), + SdkResourceProvider.create(Resource.empty()), LogLimits::getDefault, logRecordProcessor, Clock.getDefault(), diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilderTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilderTest.java index 5c49d35e5ca..5a274c163e4 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilderTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilderTest.java @@ -6,10 +6,13 @@ package io.opentelemetry.sdk.logs; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.logs.internal.SdkLoggerProviderUtil; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import org.junit.jupiter.api.Test; public class SdkLoggerProviderBuilderTest { @@ -24,8 +27,56 @@ void addResource() { SdkLoggerProvider sdkLoggerProvider = SdkLoggerProvider.builder().addResource(customResource).build(); - assertThat(sdkLoggerProvider) - .extracting("sharedState") - .hasFieldOrPropertyWithValue("resource", Resource.getDefault().merge(customResource)); + assertThat(SdkLoggerProviderUtil.getSdkResourceProvider(sdkLoggerProvider).getResource()) + .isEqualTo(Resource.getDefault().merge(customResource)); + } + + @Test + void resourceProvider_defaultsToAutoWrappedStaticResource() { + SdkLoggerProvider provider = SdkLoggerProvider.builder().build(); + + assertThat(SdkLoggerProviderUtil.getSdkResourceProvider(provider).getResource()) + .isEqualTo(Resource.getDefault()); + } + + @Test + void setSdkResourceProvider_usedForResource() { + Resource customResource = + Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); + SdkResourceProvider resourceProvider = SdkResourceProvider.create(customResource); + + SdkLoggerProviderBuilder builder = SdkLoggerProvider.builder(); + SdkLoggerProviderUtil.setSdkResourceProvider(builder, resourceProvider); + SdkLoggerProvider provider = builder.build(); + + assertThat(SdkLoggerProviderUtil.getSdkResourceProvider(provider)).isSameAs(resourceProvider); + assertThat(SdkLoggerProviderUtil.getSdkResourceProvider(provider).getResource()) + .isEqualTo(customResource); + } + + @Test + void setSdkResourceProvider_mutuallyExclusiveWithSetResource() { + Resource customResource = + Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); + SdkLoggerProviderBuilder builder = SdkLoggerProvider.builder().setResource(customResource); + SdkLoggerProviderUtil.setSdkResourceProvider( + builder, SdkResourceProvider.create(customResource)); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("mutually exclusive"); + } + + @Test + void setSdkResourceProvider_mutuallyExclusiveWithAddResource() { + Resource customResource = + Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); + SdkLoggerProviderBuilder builder = SdkLoggerProvider.builder().addResource(customResource); + SdkLoggerProviderUtil.setSdkResourceProvider( + builder, SdkResourceProvider.create(customResource)); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("mutually exclusive"); } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java index 1ec856f8c05..3851fbe4ed8 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java @@ -28,6 +28,7 @@ import io.opentelemetry.sdk.metrics.internal.view.RegisteredView; import io.opentelemetry.sdk.metrics.internal.view.ViewRegistry; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import java.io.Closeable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -54,6 +55,7 @@ public final class SdkMeterProvider implements MeterProvider, Closeable { private final List registeredReaders; private final List metricProducers; private final MeterProviderSharedState sharedState; + private final SdkResourceProvider resourceProvider; private final ComponentRegistry registry; private final AtomicBoolean isClosed = new AtomicBoolean(false); @@ -69,9 +71,10 @@ public static SdkMeterProviderBuilder builder() { IdentityHashMap metricReaders, List metricProducers, Clock clock, - Resource resource, + SdkResourceProvider resourceProvider, ExemplarFilterInternal exemplarFilter, ScopeConfigurator meterConfigurator) { + this.resourceProvider = resourceProvider; this.registeredViews = registeredViews; this.registeredReaders = metricReaders.entrySet().stream() @@ -82,7 +85,7 @@ public static SdkMeterProviderBuilder builder() { ViewRegistry.create(entry.getKey(), entry.getValue(), registeredViews))) .collect(toList()); this.metricProducers = metricProducers; - this.sharedState = MeterProviderSharedState.create(clock, resource, exemplarFilter); + this.sharedState = MeterProviderSharedState.create(clock, resourceProvider, exemplarFilter); this.registry = new ComponentRegistry<>( instrumentationLibraryInfo -> @@ -108,6 +111,17 @@ private MeterConfig getMeterConfig(InstrumentationScopeInfo instrumentationScope return meterConfig == null ? MeterConfig.defaultConfig() : meterConfig; } + /** + * Returns the {@link SdkResourceProvider} used to resolve the {@link Resource} attached to + * metrics. + * + *

This method is experimental so not public. You may reflectively call it using {@link + * SdkMeterProviderUtil#getSdkResourceProvider(SdkMeterProvider)}. + */ + SdkResourceProvider getSdkResourceProvider() { + return resourceProvider; + } + void setMeterConfigurator(ScopeConfigurator meterConfigurator) { this.meterConfigurator = meterConfigurator; this.registry diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java index e13b45ceba1..c9c19bdaa33 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java @@ -19,11 +19,13 @@ import io.opentelemetry.sdk.metrics.internal.view.AttributesFilters; import io.opentelemetry.sdk.metrics.internal.view.RegisteredView; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; import java.util.function.Predicate; +import javax.annotation.Nullable; /** * Builder class for the {@link SdkMeterProvider}. @@ -42,6 +44,8 @@ public final class SdkMeterProviderBuilder { private Clock clock = Clock.getDefault(); private Resource resource = Resource.getDefault(); + private boolean resourceExplicitlySet = false; + @Nullable private SdkResourceProvider resourceProvider; private final IdentityHashMap metricReaders = new IdentityHashMap<>(); private final List metricProducers = new ArrayList<>(); @@ -67,6 +71,7 @@ public SdkMeterProviderBuilder setClock(Clock clock) { public SdkMeterProviderBuilder setResource(Resource resource) { Objects.requireNonNull(resource, "resource"); this.resource = resource; + this.resourceExplicitlySet = true; return this; } @@ -79,6 +84,23 @@ public SdkMeterProviderBuilder setResource(Resource resource) { public SdkMeterProviderBuilder addResource(Resource resource) { Objects.requireNonNull(resource, "resource"); this.resource = this.resource.merge(resource); + this.resourceExplicitlySet = true; + return this; + } + + /** + * Sets the {@link SdkResourceProvider} used to resolve the {@link Resource} attached to all + * metrics. + * + *

Mutually exclusive with {@link #setResource(Resource)} and {@link #addResource(Resource)}; + * {@link #build()} throws {@link IllegalStateException} if both are configured. + * + *

This method is experimental so not public. You may reflectively call it using {@link + * SdkMeterProviderUtil#setSdkResourceProvider(SdkMeterProviderBuilder, SdkResourceProvider)}. + */ + SdkMeterProviderBuilder setSdkResourceProvider(SdkResourceProvider resourceProvider) { + Objects.requireNonNull(resourceProvider, "resourceProvider"); + this.resourceProvider = resourceProvider; return this; } @@ -201,12 +223,18 @@ SdkMeterProviderBuilder addMeterConfiguratorCondition( /** Returns an {@link SdkMeterProvider} built with the configuration of this builder. */ public SdkMeterProvider build() { + if (resourceProvider != null && resourceExplicitlySet) { + throw new IllegalStateException( + "setSdkResourceProvider is mutually exclusive with setResource / addResource."); + } + SdkResourceProvider effectiveSdkResourceProvider = + resourceProvider != null ? resourceProvider : SdkResourceProvider.create(resource); return new SdkMeterProvider( registeredViews, metricReaders, metricProducers, clock, - resource, + effectiveSdkResourceProvider, exemplarFilter, meterConfiguratorBuilder.build()); } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java index 2507779136d..6ac78dcbec4 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java @@ -9,6 +9,7 @@ import io.opentelemetry.sdk.common.internal.ScopeConfigurator; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.function.Predicate; @@ -25,6 +26,33 @@ public final class SdkMeterProviderUtil { private SdkMeterProviderUtil() {} + /** Reflectively set the {@link SdkResourceProvider} on the {@link SdkMeterProviderBuilder}. */ + public static void setSdkResourceProvider( + SdkMeterProviderBuilder sdkMeterProviderBuilder, SdkResourceProvider resourceProvider) { + try { + Method method = + SdkMeterProviderBuilder.class.getDeclaredMethod( + "setSdkResourceProvider", SdkResourceProvider.class); + method.setAccessible(true); + method.invoke(sdkMeterProviderBuilder, resourceProvider); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling setSdkResourceProvider on SdkMeterProviderBuilder", e); + } + } + + /** Reflectively get the {@link SdkResourceProvider} from the {@link SdkMeterProvider}. */ + public static SdkResourceProvider getSdkResourceProvider(SdkMeterProvider sdkMeterProvider) { + try { + Method method = SdkMeterProvider.class.getDeclaredMethod("getSdkResourceProvider"); + method.setAccessible(true); + return (SdkResourceProvider) method.invoke(sdkMeterProvider); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling getSdkResourceProvider on SdkMeterProvider", e); + } + } + /** Reflectively set the {@link ScopeConfigurator} to the {@link SdkMeterProvider}. */ public static void setMeterConfigurator( SdkMeterProvider sdkMeterProvider, ScopeConfigurator scopeConfigurator) { diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterProviderSharedState.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterProviderSharedState.java index 46dace490df..a40222392fa 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterProviderSharedState.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterProviderSharedState.java @@ -10,6 +10,7 @@ import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilterInternal; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import javax.annotation.concurrent.Immutable; /** @@ -23,10 +24,8 @@ public abstract class MeterProviderSharedState { public static MeterProviderSharedState create( - Clock clock, Resource resource, ExemplarFilterInternal exemplarFilter) { - MeterProviderSharedState sharedState = - new AutoValue_MeterProviderSharedState(clock, resource, exemplarFilter); - return sharedState; + Clock clock, SdkResourceProvider resourceProvider, ExemplarFilterInternal exemplarFilter) { + return new AutoValue_MeterProviderSharedState(clock, resourceProvider, exemplarFilter); } MeterProviderSharedState() {} @@ -34,8 +33,13 @@ public static MeterProviderSharedState create( /** Returns the {@link Clock} used for measurements. */ public abstract Clock getClock(); + /** Returns the {@link SdkResourceProvider} used to resolve the attached {@link Resource}. */ + public abstract SdkResourceProvider getSdkResourceProvider(); + /** Returns the {@link Resource} to attach telemetry to. */ - public abstract Resource getResource(); + public Resource getResource() { + return getSdkResourceProvider().getResource(); + } /** Returns the {@link ExemplarFilterInternal} for remembering measurements. */ public abstract ExemplarFilterInternal getExemplarFilter(); diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java index 14ce8e8609e..972c9667052 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java @@ -13,6 +13,7 @@ import io.opentelemetry.sdk.metrics.internal.descriptor.Advice; import io.opentelemetry.sdk.metrics.internal.state.MeterProviderSharedState; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import io.opentelemetry.sdk.testing.time.TestClock; import java.util.Collections; import org.junit.jupiter.api.Test; @@ -22,7 +23,7 @@ class InstrumentBuilderTest { public static final MeterProviderSharedState PROVIDER_SHARED_STATE = MeterProviderSharedState.create( TestClock.create(), - Resource.getDefault(), + SdkResourceProvider.create(Resource.getDefault()), asExemplarFilterInternal(ExemplarFilter.alwaysOff())); static final InstrumentationScopeInfo SCOPE = InstrumentationScopeInfo.create("scope-name"); public static final SdkMeter SDK_METER = diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java index bb15dc070d1..65009b3d2eb 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java @@ -6,10 +6,13 @@ package io.opentelemetry.sdk.metrics; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; import org.junit.jupiter.api.Test; @@ -43,4 +46,62 @@ void addResource() { .extracting("sharedState") .hasFieldOrPropertyWithValue("resource", Resource.getDefault().merge(customResource)); } + + @Test + void resourceProvider_defaultsToAutoWrappedStaticResource() { + SdkMeterProvider provider = + SdkMeterProvider.builder().registerMetricReader(InMemoryMetricReader.create()).build(); + + assertThat(SdkMeterProviderUtil.getSdkResourceProvider(provider).getResource()) + .isEqualTo(Resource.getDefault()); + } + + @Test + void setSdkResourceProvider_usedForResource() { + Resource customResource = + Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); + SdkResourceProvider resourceProvider = SdkResourceProvider.create(customResource); + + SdkMeterProviderBuilder builder = + SdkMeterProvider.builder().registerMetricReader(InMemoryMetricReader.create()); + SdkMeterProviderUtil.setSdkResourceProvider(builder, resourceProvider); + SdkMeterProvider provider = builder.build(); + + assertThat(SdkMeterProviderUtil.getSdkResourceProvider(provider)).isSameAs(resourceProvider); + assertThat(provider) + .extracting("sharedState") + .hasFieldOrPropertyWithValue("resource", customResource); + } + + @Test + void setSdkResourceProvider_mutuallyExclusiveWithSetResource() { + Resource customResource = + Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); + SdkMeterProviderBuilder builder = + SdkMeterProvider.builder() + .registerMetricReader(InMemoryMetricReader.create()) + .setResource(customResource); + SdkMeterProviderUtil.setSdkResourceProvider( + builder, SdkResourceProvider.create(customResource)); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("mutually exclusive"); + } + + @Test + void setSdkResourceProvider_mutuallyExclusiveWithAddResource() { + Resource customResource = + Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); + SdkMeterProviderBuilder builder = + SdkMeterProvider.builder() + .registerMetricReader(InMemoryMetricReader.create()) + .addResource(customResource); + SdkMeterProviderUtil.setSdkResourceProvider( + builder, SdkResourceProvider.create(customResource)); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("mutually exclusive"); + } } diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java index 84d9230bc6e..6e2e57b43db 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java @@ -16,6 +16,7 @@ import io.opentelemetry.sdk.common.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.common.internal.ScopeConfigurator; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import io.opentelemetry.sdk.trace.internal.SdkTracerProviderUtil; import io.opentelemetry.sdk.trace.internal.TracerConfig; import io.opentelemetry.sdk.trace.samplers.Sampler; @@ -32,6 +33,7 @@ public final class SdkTracerProvider implements TracerProvider, Closeable { private static final Logger logger = Logger.getLogger(SdkTracerProvider.class.getName()); static final String DEFAULT_TRACER_NAME = ""; private final TracerSharedState sharedState; + private final SdkResourceProvider resourceProvider; private final ComponentRegistry tracerSdkComponentRegistry; // deliberately not volatile because of performance concerns // - which means its eventually consistent @@ -50,18 +52,19 @@ public static SdkTracerProviderBuilder builder() { SdkTracerProvider( Clock clock, IdGenerator idsGenerator, - Resource resource, + SdkResourceProvider resourceProvider, Supplier spanLimitsSupplier, Sampler sampler, List spanProcessors, ScopeConfigurator tracerConfigurator, ExceptionAttributeResolver exceptionAttributeResolver, Supplier meterProvider) { + this.resourceProvider = resourceProvider; this.sharedState = new TracerSharedState( clock, idsGenerator, - resource, + resourceProvider, spanLimitsSupplier, sampler, spanProcessors, @@ -107,6 +110,16 @@ public TracerBuilder tracerBuilder(@Nullable String instrumentationScopeName) { return new SdkTracerBuilder(tracerSdkComponentRegistry, instrumentationScopeName); } + /** + * Returns the {@link SdkResourceProvider} used to resolve the {@link Resource} attached to spans. + * + *

This method is experimental so not public. You may reflectively call it using {@link + * SdkTracerProviderUtil#getSdkResourceProvider(SdkTracerProvider)}. + */ + SdkResourceProvider getSdkResourceProvider() { + return resourceProvider; + } + /** Returns the {@link SpanLimits} that are currently applied to created spans. */ public SpanLimits getSpanLimits() { return sharedState.getSpanLimits(); diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java index b0e30b13c52..57a6cfde851 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java @@ -15,6 +15,7 @@ import io.opentelemetry.sdk.common.internal.ScopeConfigurator; import io.opentelemetry.sdk.common.internal.ScopeConfiguratorBuilder; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import io.opentelemetry.sdk.trace.internal.SdkTracerProviderUtil; import io.opentelemetry.sdk.trace.internal.TracerConfig; import io.opentelemetry.sdk.trace.samplers.Sampler; @@ -23,6 +24,7 @@ import java.util.Objects; import java.util.function.Predicate; import java.util.function.Supplier; +import javax.annotation.Nullable; /** Builder of {@link SdkTracerProvider}. */ public final class SdkTracerProviderBuilder { @@ -33,6 +35,8 @@ public final class SdkTracerProviderBuilder { private Clock clock = Clock.getDefault(); private IdGenerator idsGenerator = IdGenerator.random(); private Resource resource = Resource.getDefault(); + private boolean resourceExplicitlySet = false; + @Nullable private SdkResourceProvider resourceProvider; private Supplier spanLimitsSupplier = SpanLimits::getDefault; private Sampler sampler = DEFAULT_SAMPLER; private ScopeConfiguratorBuilder tracerConfiguratorBuilder = @@ -82,6 +86,7 @@ public SdkTracerProviderBuilder setIdGenerator(IdGenerator idGenerator) { public SdkTracerProviderBuilder setResource(Resource resource) { requireNonNull(resource, "resource"); this.resource = resource; + this.resourceExplicitlySet = true; return this; } @@ -94,6 +99,23 @@ public SdkTracerProviderBuilder setResource(Resource resource) { public SdkTracerProviderBuilder addResource(Resource resource) { Objects.requireNonNull(resource, "resource"); this.resource = this.resource.merge(resource); + this.resourceExplicitlySet = true; + return this; + } + + /** + * Sets the {@link SdkResourceProvider} used to resolve the {@link Resource} attached to all + * spans. + * + *

Mutually exclusive with {@link #setResource(Resource)} and {@link #addResource(Resource)}; + * {@link #build()} throws {@link IllegalStateException} if both are configured. + * + *

This method is experimental so not public. You may reflectively call it using {@link + * SdkTracerProviderUtil#setSdkResourceProvider(SdkTracerProviderBuilder, SdkResourceProvider)}. + */ + SdkTracerProviderBuilder setSdkResourceProvider(SdkResourceProvider resourceProvider) { + requireNonNull(resourceProvider, "resourceProvider"); + this.resourceProvider = resourceProvider; return this; } @@ -253,10 +275,16 @@ public SdkTracerProviderBuilder setMeterProvider(Supplier meterPr * @return The instance. */ public SdkTracerProvider build() { + if (resourceProvider != null && resourceExplicitlySet) { + throw new IllegalStateException( + "setSdkResourceProvider is mutually exclusive with setResource / addResource."); + } + SdkResourceProvider effectiveSdkResourceProvider = + resourceProvider != null ? resourceProvider : SdkResourceProvider.create(resource); return new SdkTracerProvider( clock, idsGenerator, - resource, + effectiveSdkResourceProvider, spanLimitsSupplier, sampler, spanProcessors, diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java index 6c2e32a34dd..977ac35df70 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java @@ -9,6 +9,7 @@ import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import io.opentelemetry.sdk.trace.samplers.Sampler; import java.util.Arrays; import java.util.List; @@ -23,7 +24,7 @@ final class TracerSharedState { private final IdGenerator idGenerator; // tracks whether it is safe to skip id validation on ids from the above generator private final boolean idGeneratorSafeToSkipIdValidation; - private final Resource resource; + private final SdkResourceProvider resourceProvider; private final Supplier spanLimitsSupplier; private final Sampler sampler; @@ -37,7 +38,7 @@ final class TracerSharedState { TracerSharedState( Clock clock, IdGenerator idGenerator, - Resource resource, + SdkResourceProvider resourceProvider, Supplier spanLimitsSupplier, Sampler sampler, List spanProcessors, @@ -46,7 +47,7 @@ final class TracerSharedState { this.clock = clock; this.idGenerator = idGenerator; this.idGeneratorSafeToSkipIdValidation = idGenerator instanceof RandomIdGenerator; - this.resource = resource; + this.resourceProvider = resourceProvider; this.spanLimitsSupplier = spanLimitsSupplier; this.sampler = sampler; this.activeSpanProcessor = SpanProcessor.composite(spanProcessors); @@ -71,7 +72,7 @@ boolean isIdGeneratorSafeToSkipIdValidation() { } Resource getResource() { - return resource; + return resourceProvider.getResource(); } /** Returns the current {@link SpanLimits}. */ diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java index b9a5a357f98..108008f0386 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java @@ -8,6 +8,7 @@ import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.common.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.common.internal.ScopeConfigurator; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; import java.lang.reflect.InvocationTargetException; @@ -74,6 +75,33 @@ public static void addTracerConfiguratorCondition( } } + /** Reflectively set the {@link SdkResourceProvider} on the {@link SdkTracerProviderBuilder}. */ + public static void setSdkResourceProvider( + SdkTracerProviderBuilder sdkTracerProviderBuilder, SdkResourceProvider resourceProvider) { + try { + Method method = + SdkTracerProviderBuilder.class.getDeclaredMethod( + "setSdkResourceProvider", SdkResourceProvider.class); + method.setAccessible(true); + method.invoke(sdkTracerProviderBuilder, resourceProvider); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling setSdkResourceProvider on SdkTracerProviderBuilder", e); + } + } + + /** Reflectively get the {@link SdkResourceProvider} from the {@link SdkTracerProvider}. */ + public static SdkResourceProvider getSdkResourceProvider(SdkTracerProvider sdkTracerProvider) { + try { + Method method = SdkTracerProvider.class.getDeclaredMethod("getSdkResourceProvider"); + method.setAccessible(true); + return (SdkResourceProvider) method.invoke(sdkTracerProvider); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling getSdkResourceProvider on SdkTracerProvider", e); + } + } + /** Reflectively set exception attribute resolver to the {@link SdkTracerProviderBuilder}. */ public static void setExceptionAttributeResolver( SdkTracerProviderBuilder sdkTracerProviderBuilder, diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilderTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilderTest.java index 5972a914cef..ec5042340f2 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilderTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilderTest.java @@ -6,11 +6,14 @@ package io.opentelemetry.sdk.trace; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; +import io.opentelemetry.sdk.trace.internal.SdkTracerProviderUtil; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; @@ -26,9 +29,57 @@ void addResource() { SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder().addResource(customResource).build(); - assertThat(sdkTracerProvider) - .extracting("sharedState") - .hasFieldOrPropertyWithValue("resource", Resource.getDefault().merge(customResource)); + assertThat(SdkTracerProviderUtil.getSdkResourceProvider(sdkTracerProvider).getResource()) + .isEqualTo(Resource.getDefault().merge(customResource)); + } + + @Test + void resourceProvider_defaultsToAutoWrappedStaticResource() { + SdkTracerProvider provider = SdkTracerProvider.builder().build(); + + assertThat(SdkTracerProviderUtil.getSdkResourceProvider(provider).getResource()) + .isEqualTo(Resource.getDefault()); + } + + @Test + void setSdkResourceProvider_usedForResource() { + Resource customResource = + Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); + SdkResourceProvider resourceProvider = SdkResourceProvider.create(customResource); + + SdkTracerProviderBuilder builder = SdkTracerProvider.builder(); + SdkTracerProviderUtil.setSdkResourceProvider(builder, resourceProvider); + SdkTracerProvider provider = builder.build(); + + assertThat(SdkTracerProviderUtil.getSdkResourceProvider(provider)).isSameAs(resourceProvider); + assertThat(SdkTracerProviderUtil.getSdkResourceProvider(provider).getResource()) + .isEqualTo(customResource); + } + + @Test + void setSdkResourceProvider_mutuallyExclusiveWithSetResource() { + Resource customResource = + Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); + SdkTracerProviderBuilder builder = SdkTracerProvider.builder().setResource(customResource); + SdkTracerProviderUtil.setSdkResourceProvider( + builder, SdkResourceProvider.create(customResource)); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("mutually exclusive"); + } + + @Test + void setSdkResourceProvider_mutuallyExclusiveWithAddResource() { + Resource customResource = + Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); + SdkTracerProviderBuilder builder = SdkTracerProvider.builder().addResource(customResource); + SdkTracerProviderUtil.setSdkResourceProvider( + builder, SdkResourceProvider.create(customResource)); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("mutually exclusive"); } @Test diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java index 00b12c1b005..9b46a8e827a 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java @@ -65,9 +65,8 @@ void builder_defaultResource() { .build(); assertThat(tracerProvider).isNotNull(); - assertThat(tracerProvider) - .extracting("sharedState") - .hasFieldOrPropertyWithValue("resource", resourceWithDefaults); + assertThat(SdkTracerProviderUtil.getSdkResourceProvider(tracerProvider).getResource()) + .isEqualTo(resourceWithDefaults); } @Test @@ -102,9 +101,8 @@ void builder_serviceNameProvided() { .build(); assertThat(tracerProvider).isNotNull(); - assertThat(tracerProvider) - .extracting("sharedState") - .hasFieldOrPropertyWithValue("resource", resource); + assertThat(SdkTracerProviderUtil.getSdkResourceProvider(tracerProvider).getResource()) + .isEqualTo(resource); } @Test From b82c1bd1476d2e716d19950786b5af24aae353cf Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:27:02 -0500 Subject: [PATCH 2/3] Make ResourceDetector package private, add method to add constant resource to SdkResourceProviderBuilder --- .../resources/internal/ResourceDetector.java | 25 +---------- .../internal/SdkResourceProvider.java | 7 +-- .../internal/SdkResourceProviderBuilder.java | 33 ++++++++++---- ...java => ConstantResourceDetectorTest.java} | 24 ++-------- .../internal/SdkResourceProviderTest.java | 45 ++++++++++++++----- 5 files changed, 64 insertions(+), 70 deletions(-) rename sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/{ResourceDetectorTest.java => ConstantResourceDetectorTest.java} (52%) diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ResourceDetector.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ResourceDetector.java index 35eaf5f08b7..4d5ca262e9f 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ResourceDetector.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ResourceDetector.java @@ -5,32 +5,11 @@ package io.opentelemetry.sdk.resources.internal; -import static java.util.Objects.requireNonNull; - import io.opentelemetry.sdk.resources.Resource; import javax.annotation.Nullable; -/** - * A source of a {@link Resource} to be merged by an {@link SdkResourceProvider}. - * - *

This class is internal and experimental. Its APIs are unstable and can change at any time. Its - * APIs (or a version of them) may be promoted to the public stable API in the future, but no - * guarantees are made. - */ -public interface ResourceDetector { - - /** Returns a {@link ResourceDetector} that always returns {@code resource} and has no name. */ - static ResourceDetector constant(Resource resource) { - requireNonNull(resource, "resource"); - return new ConstantResourceDetector(null, resource); - } - - /** Returns a {@link ResourceDetector} named {@code name} that always returns {@code resource}. */ - static ResourceDetector constant(Resource resource, String name) { - requireNonNull(resource, "resource"); - requireNonNull(name, "name"); - return new ConstantResourceDetector(name, resource); - } +/** A source of a {@link Resource} to be merged by an {@link SdkResourceProvider}. */ +interface ResourceDetector { /** * Returns the name of this detector, or {@code null} if unnamed. Names are informational (e.g. diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProvider.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProvider.java index 17cf3e3585e..a0720b447b5 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProvider.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProvider.java @@ -41,13 +41,10 @@ public static SdkResourceProviderBuilder builder() { return new SdkResourceProviderBuilder(); } - /** - * Returns an {@link SdkResourceProvider} backed by a single {@linkplain - * ResourceDetector#constant(Resource) constant detector} wrapping the given {@link Resource}. - */ + /** Returns an {@link SdkResourceProvider} backed by a single constant {@link Resource}. */ public static SdkResourceProvider create(Resource resource) { requireNonNull(resource, "resource"); - return builder().addDetector(ResourceDetector.constant(resource)).build(); + return builder().addConstantResource(resource).build(); } /** Returns the merged {@link Resource}. */ diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderBuilder.java index 0e054da6d86..891810a3d25 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderBuilder.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderBuilder.java @@ -7,8 +7,10 @@ import static java.util.Objects.requireNonNull; +import io.opentelemetry.sdk.resources.Resource; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nullable; /** * Builder for {@link SdkResourceProvider}. @@ -24,26 +26,39 @@ public final class SdkResourceProviderBuilder { SdkResourceProviderBuilder() {} /** - * Adds a {@link ResourceDetector}. Detectors are merged in the order they are added, with - * later-added attributes winning on conflict (per {@link - * io.opentelemetry.sdk.resources.Resource#merge}). + * Adds a constant {@link Resource} with no name. + * + * @param resource the resource to add + * @return this builder + */ + public SdkResourceProviderBuilder addConstantResource(Resource resource) { + return addConstantResource(resource, null); + } + + /** + * Adds a constant {@link Resource}. Resources are merged in the order they are added, with + * later-added attributes winning on conflict (per {@link Resource#merge}). + * + * @param resource the resource to add + * @param name an informational name for logging / debugging, or {@code null} if unnamed + * @return this builder */ - public SdkResourceProviderBuilder addDetector(ResourceDetector detector) { - requireNonNull(detector, "detector"); - detectors.add(detector); + public SdkResourceProviderBuilder addConstantResource(Resource resource, @Nullable String name) { + requireNonNull(resource, "resource"); + detectors.add(new ConstantResourceDetector(name, resource)); return this; } /** * Returns a new {@link SdkResourceProvider}. * - * @throws IllegalStateException if no detector has been added via {@link - * #addDetector(ResourceDetector)}. + * @throws IllegalStateException if no resource has been added via {@link + * #addConstantResource(Resource)}. */ public SdkResourceProvider build() { if (detectors.isEmpty()) { throw new IllegalStateException( - "At least one ResourceDetector must be added via addDetector before calling build()."); + "At least one Resource must be added via addConstantResource before calling build()."); } return new SdkResourceProvider(detectors); } diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ResourceDetectorTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetectorTest.java similarity index 52% rename from sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ResourceDetectorTest.java rename to sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetectorTest.java index 7b5535f4b9f..681256ba39a 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ResourceDetectorTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetectorTest.java @@ -6,21 +6,20 @@ package io.opentelemetry.sdk.resources.internal; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.resources.Resource; import org.junit.jupiter.api.Test; -class ResourceDetectorTest { +class ConstantResourceDetectorTest { private static final Resource RESOURCE = Resource.create(Attributes.of(AttributeKey.stringKey("k"), "v")); @Test void constant_unnamed() { - ResourceDetector detector = ResourceDetector.constant(RESOURCE); + ResourceDetector detector = new ConstantResourceDetector(null, RESOURCE); assertThat(detector.getName()).isNull(); assertThat(detector.getResource()).isEqualTo(RESOURCE); @@ -29,27 +28,10 @@ void constant_unnamed() { @Test void constant_named() { - ResourceDetector detector = ResourceDetector.constant(RESOURCE, "my-detector"); + ResourceDetector detector = new ConstantResourceDetector("my-detector", RESOURCE); assertThat(detector.getName()).isEqualTo("my-detector"); assertThat(detector.getResource()).isEqualTo(RESOURCE); assertThat(detector.shouldReinvoke()).isFalse(); } - - @Test - void constant_nullResource_throws() { - assertThatThrownBy(() -> ResourceDetector.constant(null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("resource"); - assertThatThrownBy(() -> ResourceDetector.constant(null, "name")) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("resource"); - } - - @Test - void constant_nullName_throws() { - assertThatThrownBy(() -> ResourceDetector.constant(RESOURCE, null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("name"); - } } diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderTest.java index 530e880a8ff..778595f8b5a 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderTest.java @@ -11,6 +11,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; @@ -36,36 +37,57 @@ void create_nullResource_throws() { } @Test - void builder_singleDetector() { + void builder_singleConstantResource() { SdkResourceProvider provider = - SdkResourceProvider.builder().addDetector(ResourceDetector.constant(RESOURCE_A)).build(); + SdkResourceProvider.builder().addConstantResource(RESOURCE_A).build(); assertThat(provider.getResource()).isEqualTo(RESOURCE_A); } @Test - void builder_multipleDetectors_mergedInOrder() { + void builder_multipleConstantResources_mergedInOrder() { SdkResourceProvider provider = SdkResourceProvider.builder() - .addDetector(ResourceDetector.constant(RESOURCE_A)) - .addDetector(ResourceDetector.constant(RESOURCE_B)) + .addConstantResource(RESOURCE_A) + .addConstantResource(RESOURCE_B) .build(); assertThat(provider.getResource()).isEqualTo(RESOURCE_A.merge(RESOURCE_B)); } @Test - void builder_addDetector_nullDetector_throws() { - assertThatThrownBy(() -> SdkResourceProvider.builder().addDetector(null)) + void builder_addConstantResource_named() { + SdkResourceProvider provider = + SdkResourceProvider.builder().addConstantResource(RESOURCE_A, "my-detector").build(); + + assertThat(provider.getResource()).isEqualTo(RESOURCE_A); + assertThat(provider.toString()).contains("ConstantResourceDetector{name=my-detector"); + } + + @Test + void builder_addConstantResource_nullName() { + SdkResourceProvider provider = + SdkResourceProvider.builder().addConstantResource(RESOURCE_A, null).build(); + + assertThat(provider.getResource()).isEqualTo(RESOURCE_A); + assertThat(provider.toString()).contains("ConstantResourceDetector{name=null"); + } + + @Test + void builder_addConstantResource_nullResource_throws() { + assertThatThrownBy(() -> SdkResourceProvider.builder().addConstantResource(null)) .isInstanceOf(NullPointerException.class) - .hasMessageContaining("detector"); + .hasMessageContaining("resource"); + assertThatThrownBy(() -> SdkResourceProvider.builder().addConstantResource(null, "name")) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("resource"); } @Test - void builder_build_withoutAddDetector_throws() { + void builder_build_withoutAddConstantResource_throws() { assertThatThrownBy(() -> SdkResourceProvider.builder().build()) .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("At least one ResourceDetector"); + .hasMessageContaining("At least one Resource must be added via addConstantResource"); } @Test @@ -74,8 +96,7 @@ void getResource_allDetectorsResolvedOnceAtConstruction_regardlessOfShouldReinvo // construction and the merged Resource is cached for the lifetime of the provider. CountingDetector constant = new CountingDetector(RESOURCE_A, /* shouldReinvoke= */ false); CountingDetector reinvoke = new CountingDetector(RESOURCE_B, /* shouldReinvoke= */ true); - SdkResourceProvider provider = - SdkResourceProvider.builder().addDetector(constant).addDetector(reinvoke).build(); + SdkResourceProvider provider = new SdkResourceProvider(Arrays.asList(constant, reinvoke)); assertThat(constant.invocations.get()).isEqualTo(1); assertThat(reinvoke.invocations.get()).isEqualTo(1); From 537723e599e09aabb35c3f41120517d64fecf7c2 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:35:15 -0500 Subject: [PATCH 3/3] PR feedback --- .../internal/ExtendedOpenTelemetrySdk.java | 9 ++- .../internal/ConstantResourceDetector.java | 5 -- .../resources/internal/ResourceDetector.java | 22 ++---- .../internal/SdkResourceProvider.java | 21 ++++-- .../internal/SdkResourceProviderBuilder.java | 5 +- .../ConstantResourceDetectorTest.java | 2 - .../internal/SdkResourceProviderTest.java | 65 ++++++++++------ .../sdk/logs/SdkLoggerProviderBuilder.java | 24 ++++-- .../logs/SdkLoggerProviderBuilderTest.java | 68 ++++++++++++----- .../sdk/metrics/SdkMeterProviderBuilder.java | 29 ++++++-- .../metrics/SdkMeterProviderBuilderTest.java | 74 +++++++++++++------ .../sdk/trace/SdkTracerProviderBuilder.java | 24 ++++-- .../trace/SdkTracerProviderBuilderTest.java | 68 ++++++++++++----- 13 files changed, 280 insertions(+), 136 deletions(-) diff --git a/sdk/all/src/main/java/io/opentelemetry/sdk/internal/ExtendedOpenTelemetrySdk.java b/sdk/all/src/main/java/io/opentelemetry/sdk/internal/ExtendedOpenTelemetrySdk.java index 1b5160a3f77..6981d9e85a0 100644 --- a/sdk/all/src/main/java/io/opentelemetry/sdk/internal/ExtendedOpenTelemetrySdk.java +++ b/sdk/all/src/main/java/io/opentelemetry/sdk/internal/ExtendedOpenTelemetrySdk.java @@ -35,7 +35,9 @@ public final class ExtendedOpenTelemetrySdk extends OpenTelemetrySdk @Nullable private final SdkResourceProvider resourceProvider; private ExtendedOpenTelemetrySdk( - OpenTelemetrySdk openTelemetrySdk, SdkConfigProvider configProvider) { + OpenTelemetrySdk openTelemetrySdk, + SdkConfigProvider configProvider, + @Nullable SdkResourceProvider resourceProvider) { super( openTelemetrySdk.getSdkTracerProvider(), openTelemetrySdk.getSdkMeterProvider(), @@ -43,12 +45,13 @@ private ExtendedOpenTelemetrySdk( openTelemetrySdk.getPropagators()); this.openTelemetrySdk = openTelemetrySdk; this.configProvider = new ObfuscatedConfigProvider(configProvider); - this.resourceProvider = resolveSdkResourceProvider(openTelemetrySdk); + this.resourceProvider = resourceProvider; } public static ExtendedOpenTelemetrySdk create( OpenTelemetrySdk openTelemetrySdk, SdkConfigProvider sdkConfigProvider) { - return new ExtendedOpenTelemetrySdk(openTelemetrySdk, sdkConfigProvider); + return new ExtendedOpenTelemetrySdk( + openTelemetrySdk, sdkConfigProvider, resolveSdkResourceProvider(openTelemetrySdk)); } @Override diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetector.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetector.java index 680312709ed..67044d70a2e 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetector.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetector.java @@ -29,11 +29,6 @@ public Resource getResource() { return resource; } - @Override - public boolean shouldReinvoke() { - return false; - } - @Override public String toString() { return "ConstantResourceDetector{name=" + name + ", resource=" + resource + "}"; diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ResourceDetector.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ResourceDetector.java index 4d5ca262e9f..7cea19e273e 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ResourceDetector.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ResourceDetector.java @@ -11,27 +11,17 @@ /** A source of a {@link Resource} to be merged by an {@link SdkResourceProvider}. */ interface ResourceDetector { + // TODO(jack-berg): Add shouldReinvoke() or equivalent to distinguish static detectors from + // detectors whose entity + // descriptive attributes can change at runtime. + /** - * Returns the name of this detector, or {@code null} if unnamed. Names are informational (e.g. - * for logging / debugging) and are not required to be unique. + * Returns a diagnostic label for logging / debugging, or {@code null} if unnamed. This is not a + * detector lookup identifier and need not be unique. */ @Nullable String getName(); /** Returns the {@link Resource} detected by this detector. */ Resource getResource(); - - /** - * Returns whether the enclosing {@link SdkResourceProvider} must re-invoke this detector on every - * call to {@link SdkResourceProvider#getResource()}. - * - *

When {@code true}, {@link #getResource()} may be called on every {@link - * SdkResourceProvider#getResource()}. When {@code false}, this detector's result may be resolved - * once at {@link SdkResourceProvider} construction and cached. - * - *

This flag is part of the target API but has no effect on runtime behavior in the current - * iteration - all detectors are cached at construction. TTL-based cache invalidation using this - * flag is planned for a future iteration. - */ - boolean shouldReinvoke(); } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProvider.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProvider.java index a0720b447b5..13d016cff07 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProvider.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProvider.java @@ -16,11 +16,8 @@ * Provides the {@link Resource} used by the SDK by merging the results of a configured, ordered * list of {@link ResourceDetector}s. * - *

All detectors are invoked once at construction and the merged {@link Resource} is cached for - * the lifetime of this provider. {@link ResourceDetector#shouldReinvoke()} is part of the target - * API shape but has no effect on runtime behavior in this iteration. A future iteration will add - * TTL-based cache invalidation that re-invokes only {@code shouldReinvoke=true} detectors when the - * cache expires. Until then, {@code getResource()} is a single field read on the hot path. + *

All detectors are invoked once when the provider is built. The merged {@link Resource} is + * cached for the lifetime of this provider. * *

This class is internal and experimental. Its APIs are unstable and can change at any time. Its * APIs (or a version of them) may be promoted to the public stable API in the future, but no @@ -29,11 +26,19 @@ public final class SdkResourceProvider { private final List detectors; + // TODO: Add TTL-based cache refresh that re-invokes only refreshable detectors while keeping + // hot-path resource reads cheap. private final Resource cachedResource; - SdkResourceProvider(List detectors) { - this.detectors = Collections.unmodifiableList(new ArrayList<>(detectors)); - this.cachedResource = merge(this.detectors); + private SdkResourceProvider(List detectors, Resource cachedResource) { + this.detectors = detectors; + this.cachedResource = cachedResource; + } + + static SdkResourceProvider createFromDetectors(List detectors) { + List detectorSnapshot = + Collections.unmodifiableList(new ArrayList<>(detectors)); + return new SdkResourceProvider(detectorSnapshot, merge(detectorSnapshot)); } /** Returns a new {@link SdkResourceProviderBuilder}. */ diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderBuilder.java index 891810a3d25..0f193c1f146 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderBuilder.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderBuilder.java @@ -40,7 +40,8 @@ public SdkResourceProviderBuilder addConstantResource(Resource resource) { * later-added attributes winning on conflict (per {@link Resource#merge}). * * @param resource the resource to add - * @param name an informational name for logging / debugging, or {@code null} if unnamed + * @param name a diagnostic label for logging / debugging, or {@code null} if unnamed. This is not + * a detector lookup identifier and need not be unique. * @return this builder */ public SdkResourceProviderBuilder addConstantResource(Resource resource, @Nullable String name) { @@ -60,6 +61,6 @@ public SdkResourceProvider build() { throw new IllegalStateException( "At least one Resource must be added via addConstantResource before calling build()."); } - return new SdkResourceProvider(detectors); + return SdkResourceProvider.createFromDetectors(detectors); } } diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetectorTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetectorTest.java index 681256ba39a..c54dfe39a5d 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetectorTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetectorTest.java @@ -23,7 +23,6 @@ void constant_unnamed() { assertThat(detector.getName()).isNull(); assertThat(detector.getResource()).isEqualTo(RESOURCE); - assertThat(detector.shouldReinvoke()).isFalse(); } @Test @@ -32,6 +31,5 @@ void constant_named() { assertThat(detector.getName()).isEqualTo("my-detector"); assertThat(detector.getResource()).isEqualTo(RESOURCE); - assertThat(detector.shouldReinvoke()).isFalse(); } } diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderTest.java index 778595f8b5a..02f8891e3ee 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderTest.java @@ -46,13 +46,44 @@ void builder_singleConstantResource() { @Test void builder_multipleConstantResources_mergedInOrder() { + AttributeKey sharedKey = AttributeKey.stringKey("shared"); + Resource first = RESOURCE_A.toBuilder().put(sharedKey, "first").build(); + Resource second = RESOURCE_B.toBuilder().put(sharedKey, "second").build(); SdkResourceProvider provider = SdkResourceProvider.builder() - .addConstantResource(RESOURCE_A) - .addConstantResource(RESOURCE_B) + .addConstantResource(first) + .addConstantResource(second) .build(); - assertThat(provider.getResource()).isEqualTo(RESOURCE_A.merge(RESOURCE_B)); + assertThat(provider.getResource()).isEqualTo(first.merge(second)); + assertThat(provider.getResource().getAttribute(sharedKey)).isEqualTo("second"); + } + + @Test + void builder_conflictingSchemaUrls_usesSequentialMergeSemantics() { + Resource first = Resource.create(RESOURCE_A.getAttributes(), "https://example.com/schema/1"); + Resource second = Resource.create(RESOURCE_B.getAttributes(), "https://example.com/schema/2"); + Resource third = Resource.create(Attributes.empty(), "https://example.com/schema/3"); + SdkResourceProviderBuilder builder = + SdkResourceProvider.builder().addConstantResource(first).addConstantResource(second); + + assertThat(builder.build().getResource().getSchemaUrl()).isNull(); + + SdkResourceProvider provider = builder.addConstantResource(third).build(); + + assertThat(provider.getResource()).isEqualTo(first.merge(second).merge(third)); + assertThat(provider.getResource().getSchemaUrl()).isEqualTo(third.getSchemaUrl()); + } + + @Test + void builder_reuse_doesNotChangePreviousProvider() { + SdkResourceProviderBuilder builder = + SdkResourceProvider.builder().addConstantResource(RESOURCE_A); + SdkResourceProvider first = builder.build(); + SdkResourceProvider second = builder.addConstantResource(RESOURCE_B).build(); + + assertThat(first.getResource()).isEqualTo(RESOURCE_A); + assertThat(second.getResource()).isEqualTo(RESOURCE_A.merge(RESOURCE_B)); } @Test @@ -91,31 +122,28 @@ void builder_build_withoutAddConstantResource_throws() { } @Test - void getResource_allDetectorsResolvedOnceAtConstruction_regardlessOfShouldReinvoke() { - // shouldReinvoke has no runtime effect in this iteration; all detectors are resolved once at - // construction and the merged Resource is cached for the lifetime of the provider. - CountingDetector constant = new CountingDetector(RESOURCE_A, /* shouldReinvoke= */ false); - CountingDetector reinvoke = new CountingDetector(RESOURCE_B, /* shouldReinvoke= */ true); - SdkResourceProvider provider = new SdkResourceProvider(Arrays.asList(constant, reinvoke)); + void getResource_allDetectorsResolvedOnceAtCreation() { + CountingDetector first = new CountingDetector(RESOURCE_A); + CountingDetector second = new CountingDetector(RESOURCE_B); + SdkResourceProvider provider = + SdkResourceProvider.createFromDetectors(Arrays.asList(first, second)); - assertThat(constant.invocations.get()).isEqualTo(1); - assertThat(reinvoke.invocations.get()).isEqualTo(1); + assertThat(first.invocations.get()).isEqualTo(1); + assertThat(second.invocations.get()).isEqualTo(1); assertThat(provider.getResource()).isEqualTo(RESOURCE_A.merge(RESOURCE_B)); assertThat(provider.getResource()).isEqualTo(RESOURCE_A.merge(RESOURCE_B)); - assertThat(constant.invocations.get()).isEqualTo(1); - assertThat(reinvoke.invocations.get()).isEqualTo(1); + assertThat(first.invocations.get()).isEqualTo(1); + assertThat(second.invocations.get()).isEqualTo(1); } private static final class CountingDetector implements ResourceDetector { final AtomicInteger invocations = new AtomicInteger(); private final Resource resource; - private final boolean shouldReinvoke; - CountingDetector(Resource resource, boolean shouldReinvoke) { + CountingDetector(Resource resource) { this.resource = resource; - this.shouldReinvoke = shouldReinvoke; } @Override @@ -128,10 +156,5 @@ public Resource getResource() { invocations.incrementAndGet(); return resource; } - - @Override - public boolean shouldReinvoke() { - return shouldReinvoke; - } } } diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java index a6e54ab29e9..f56ff75c234 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java @@ -55,9 +55,15 @@ public final class SdkLoggerProviderBuilder { * * @param resource the resource * @return this + * @throws IllegalStateException if {@link #setSdkResourceProvider(SdkResourceProvider)} was + * already called */ public SdkLoggerProviderBuilder setResource(Resource resource) { requireNonNull(resource, "resource"); + if (resourceProvider != null) { + throw new IllegalStateException( + "Cannot call setResource after setSdkResourceProvider has been called."); + } this.resource = resource; this.resourceExplicitlySet = true; return this; @@ -67,10 +73,16 @@ public SdkLoggerProviderBuilder setResource(Resource resource) { * Merge a {@link Resource} with the current. * * @param resource {@link Resource} to merge with current. + * @throws IllegalStateException if {@link #setSdkResourceProvider(SdkResourceProvider)} was + * already called * @since 1.29.0 */ public SdkLoggerProviderBuilder addResource(Resource resource) { Objects.requireNonNull(resource, "resource"); + if (resourceProvider != null) { + throw new IllegalStateException( + "Cannot call addResource after setSdkResourceProvider has been called."); + } this.resource = this.resource.merge(resource); this.resourceExplicitlySet = true; return this; @@ -80,14 +92,18 @@ public SdkLoggerProviderBuilder addResource(Resource resource) { * Sets the {@link SdkResourceProvider} used to resolve the {@link Resource} attached to all log * records. * - *

Mutually exclusive with {@link #setResource(Resource)} and {@link #addResource(Resource)}; - * {@link #build()} throws {@link IllegalStateException} if both are configured. + *

Mutually exclusive with {@link #setResource(Resource)} and {@link #addResource(Resource)}. + * The conflicting setter throws {@link IllegalStateException} if both are configured. * *

This method is experimental so not public. You may reflectively call it using {@link * SdkLoggerProviderUtil#setSdkResourceProvider(SdkLoggerProviderBuilder, SdkResourceProvider)}. */ SdkLoggerProviderBuilder setSdkResourceProvider(SdkResourceProvider resourceProvider) { requireNonNull(resourceProvider, "resourceProvider"); + if (resourceExplicitlySet) { + throw new IllegalStateException( + "Cannot call setSdkResourceProvider after setResource or addResource has been called."); + } this.resourceProvider = resourceProvider; return this; } @@ -229,10 +245,6 @@ public SdkLoggerProviderBuilder setMeterProvider(Supplier meterPr * @return an instance configured with the provided options */ public SdkLoggerProvider build() { - if (resourceProvider != null && resourceExplicitlySet) { - throw new IllegalStateException( - "setSdkResourceProvider is mutually exclusive with setResource / addResource."); - } SdkResourceProvider effectiveSdkResourceProvider = resourceProvider != null ? resourceProvider : SdkResourceProvider.create(resource); return new SdkLoggerProvider( diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilderTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilderTest.java index 5a274c163e4..b013b29386a 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilderTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilderTest.java @@ -13,7 +13,11 @@ import io.opentelemetry.sdk.logs.internal.SdkLoggerProviderUtil; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class SdkLoggerProviderBuilderTest { @@ -54,29 +58,57 @@ void setSdkResourceProvider_usedForResource() { .isEqualTo(customResource); } - @Test - void setSdkResourceProvider_mutuallyExclusiveWithSetResource() { - Resource customResource = - Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); - SdkLoggerProviderBuilder builder = SdkLoggerProvider.builder().setResource(customResource); - SdkLoggerProviderUtil.setSdkResourceProvider( - builder, SdkResourceProvider.create(customResource)); + @ParameterizedTest + @MethodSource("resourceConfigurations") + void setSdkResourceProvider_afterResource_throws(boolean addResource, Resource resource) { + SdkLoggerProviderBuilder builder = SdkLoggerProvider.builder(); + if (addResource) { + builder.addResource(resource); + } else { + builder.setResource(resource); + } - assertThatThrownBy(builder::build) + assertThatThrownBy(() -> builder.setSdkResourceProvider(SdkResourceProvider.create(resource))) .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("mutually exclusive"); + .hasMessage( + "Cannot call setSdkResourceProvider after setResource or addResource has been called."); + assertThat(SdkLoggerProviderUtil.getSdkResourceProvider(builder.build()).getResource()) + .isEqualTo(addResource ? Resource.getDefault().merge(resource) : resource); } - @Test - void setSdkResourceProvider_mutuallyExclusiveWithAddResource() { - Resource customResource = - Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); - SdkLoggerProviderBuilder builder = SdkLoggerProvider.builder().addResource(customResource); - SdkLoggerProviderUtil.setSdkResourceProvider( - builder, SdkResourceProvider.create(customResource)); + @ParameterizedTest + @MethodSource("resourceConfigurations") + void resource_afterSetSdkResourceProvider_throws(boolean addResource, Resource resource) { + SdkResourceProvider resourceProvider = SdkResourceProvider.create(resource); + SdkLoggerProviderBuilder builder = SdkLoggerProvider.builder(); + SdkLoggerProviderUtil.setSdkResourceProvider(builder, resourceProvider); - assertThatThrownBy(builder::build) + assertThatThrownBy( + () -> { + if (addResource) { + builder.addResource(resource); + } else { + builder.setResource(resource); + } + }) .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("mutually exclusive"); + .hasMessage( + "Cannot call " + + (addResource ? "addResource" : "setResource") + + " after setSdkResourceProvider has been called."); + assertThat(SdkLoggerProviderUtil.getSdkResourceProvider(builder.build())) + .isSameAs(resourceProvider); + } + + private static Stream resourceConfigurations() { + Resource customResource = + Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); + return Stream.of( + Arguments.argumentSet("setResource(custom)", false, customResource), + Arguments.argumentSet("setResource(default)", false, Resource.getDefault()), + Arguments.argumentSet("setResource(empty)", false, Resource.empty()), + Arguments.argumentSet("addResource(custom)", true, customResource), + Arguments.argumentSet("addResource(default)", true, Resource.getDefault()), + Arguments.argumentSet("addResource(empty)", true, Resource.empty())); } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java index c9c19bdaa33..b79f7441aa8 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java @@ -67,9 +67,18 @@ public SdkMeterProviderBuilder setClock(Clock clock) { return this; } - /** Assign a {@link Resource} to be attached to all metrics. */ + /** + * Assign a {@link Resource} to be attached to all metrics. + * + * @throws IllegalStateException if {@link #setSdkResourceProvider(SdkResourceProvider)} was + * already called + */ public SdkMeterProviderBuilder setResource(Resource resource) { Objects.requireNonNull(resource, "resource"); + if (resourceProvider != null) { + throw new IllegalStateException( + "Cannot call setResource after setSdkResourceProvider has been called."); + } this.resource = resource; this.resourceExplicitlySet = true; return this; @@ -79,10 +88,16 @@ public SdkMeterProviderBuilder setResource(Resource resource) { * Merge a {@link Resource} with the current. * * @param resource {@link Resource} to merge with current. + * @throws IllegalStateException if {@link #setSdkResourceProvider(SdkResourceProvider)} was + * already called * @since 1.29.0 */ public SdkMeterProviderBuilder addResource(Resource resource) { Objects.requireNonNull(resource, "resource"); + if (resourceProvider != null) { + throw new IllegalStateException( + "Cannot call addResource after setSdkResourceProvider has been called."); + } this.resource = this.resource.merge(resource); this.resourceExplicitlySet = true; return this; @@ -92,14 +107,18 @@ public SdkMeterProviderBuilder addResource(Resource resource) { * Sets the {@link SdkResourceProvider} used to resolve the {@link Resource} attached to all * metrics. * - *

Mutually exclusive with {@link #setResource(Resource)} and {@link #addResource(Resource)}; - * {@link #build()} throws {@link IllegalStateException} if both are configured. + *

Mutually exclusive with {@link #setResource(Resource)} and {@link #addResource(Resource)}. + * The conflicting setter throws {@link IllegalStateException} if both are configured. * *

This method is experimental so not public. You may reflectively call it using {@link * SdkMeterProviderUtil#setSdkResourceProvider(SdkMeterProviderBuilder, SdkResourceProvider)}. */ SdkMeterProviderBuilder setSdkResourceProvider(SdkResourceProvider resourceProvider) { Objects.requireNonNull(resourceProvider, "resourceProvider"); + if (resourceExplicitlySet) { + throw new IllegalStateException( + "Cannot call setSdkResourceProvider after setResource or addResource has been called."); + } this.resourceProvider = resourceProvider; return this; } @@ -223,10 +242,6 @@ SdkMeterProviderBuilder addMeterConfiguratorCondition( /** Returns an {@link SdkMeterProvider} built with the configuration of this builder. */ public SdkMeterProvider build() { - if (resourceProvider != null && resourceExplicitlySet) { - throw new IllegalStateException( - "setSdkResourceProvider is mutually exclusive with setResource / addResource."); - } SdkResourceProvider effectiveSdkResourceProvider = resourceProvider != null ? resourceProvider : SdkResourceProvider.create(resource); return new SdkMeterProvider( diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java index 65009b3d2eb..1abc9a350a9 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java @@ -14,7 +14,11 @@ import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class SdkMeterProviderBuilderTest { @@ -73,35 +77,57 @@ void setSdkResourceProvider_usedForResource() { .hasFieldOrPropertyWithValue("resource", customResource); } - @Test - void setSdkResourceProvider_mutuallyExclusiveWithSetResource() { - Resource customResource = - Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); - SdkMeterProviderBuilder builder = - SdkMeterProvider.builder() - .registerMetricReader(InMemoryMetricReader.create()) - .setResource(customResource); - SdkMeterProviderUtil.setSdkResourceProvider( - builder, SdkResourceProvider.create(customResource)); + @ParameterizedTest + @MethodSource("resourceConfigurations") + void setSdkResourceProvider_afterResource_throws(boolean addResource, Resource resource) { + SdkMeterProviderBuilder builder = SdkMeterProvider.builder(); + if (addResource) { + builder.addResource(resource); + } else { + builder.setResource(resource); + } + + assertThatThrownBy(() -> builder.setSdkResourceProvider(SdkResourceProvider.create(resource))) + .isInstanceOf(IllegalStateException.class) + .hasMessage( + "Cannot call setSdkResourceProvider after setResource or addResource has been called."); + assertThat(SdkMeterProviderUtil.getSdkResourceProvider(builder.build()).getResource()) + .isEqualTo(addResource ? Resource.getDefault().merge(resource) : resource); + } - assertThatThrownBy(builder::build) + @ParameterizedTest + @MethodSource("resourceConfigurations") + void resource_afterSetSdkResourceProvider_throws(boolean addResource, Resource resource) { + SdkResourceProvider resourceProvider = SdkResourceProvider.create(resource); + SdkMeterProviderBuilder builder = SdkMeterProvider.builder(); + SdkMeterProviderUtil.setSdkResourceProvider(builder, resourceProvider); + + assertThatThrownBy( + () -> { + if (addResource) { + builder.addResource(resource); + } else { + builder.setResource(resource); + } + }) .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("mutually exclusive"); + .hasMessage( + "Cannot call " + + (addResource ? "addResource" : "setResource") + + " after setSdkResourceProvider has been called."); + assertThat(SdkMeterProviderUtil.getSdkResourceProvider(builder.build())) + .isSameAs(resourceProvider); } - @Test - void setSdkResourceProvider_mutuallyExclusiveWithAddResource() { + private static Stream resourceConfigurations() { Resource customResource = Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); - SdkMeterProviderBuilder builder = - SdkMeterProvider.builder() - .registerMetricReader(InMemoryMetricReader.create()) - .addResource(customResource); - SdkMeterProviderUtil.setSdkResourceProvider( - builder, SdkResourceProvider.create(customResource)); - - assertThatThrownBy(builder::build) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("mutually exclusive"); + return Stream.of( + Arguments.argumentSet("setResource(custom)", false, customResource), + Arguments.argumentSet("setResource(default)", false, Resource.getDefault()), + Arguments.argumentSet("setResource(empty)", false, Resource.empty()), + Arguments.argumentSet("addResource(custom)", true, customResource), + Arguments.argumentSet("addResource(default)", true, Resource.getDefault()), + Arguments.argumentSet("addResource(empty)", true, Resource.empty())); } } diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java index 57a6cfde851..ba737fff315 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java @@ -82,9 +82,15 @@ public SdkTracerProviderBuilder setIdGenerator(IdGenerator idGenerator) { * * @param resource A Resource implementation. * @return this + * @throws IllegalStateException if {@link #setSdkResourceProvider(SdkResourceProvider)} was + * already called */ public SdkTracerProviderBuilder setResource(Resource resource) { requireNonNull(resource, "resource"); + if (resourceProvider != null) { + throw new IllegalStateException( + "Cannot call setResource after setSdkResourceProvider has been called."); + } this.resource = resource; this.resourceExplicitlySet = true; return this; @@ -94,10 +100,16 @@ public SdkTracerProviderBuilder setResource(Resource resource) { * Merge a {@link Resource} with the current. * * @param resource {@link Resource} to merge with current. + * @throws IllegalStateException if {@link #setSdkResourceProvider(SdkResourceProvider)} was + * already called * @since 1.29.0 */ public SdkTracerProviderBuilder addResource(Resource resource) { Objects.requireNonNull(resource, "resource"); + if (resourceProvider != null) { + throw new IllegalStateException( + "Cannot call addResource after setSdkResourceProvider has been called."); + } this.resource = this.resource.merge(resource); this.resourceExplicitlySet = true; return this; @@ -107,14 +119,18 @@ public SdkTracerProviderBuilder addResource(Resource resource) { * Sets the {@link SdkResourceProvider} used to resolve the {@link Resource} attached to all * spans. * - *

Mutually exclusive with {@link #setResource(Resource)} and {@link #addResource(Resource)}; - * {@link #build()} throws {@link IllegalStateException} if both are configured. + *

Mutually exclusive with {@link #setResource(Resource)} and {@link #addResource(Resource)}. + * The conflicting setter throws {@link IllegalStateException} if both are configured. * *

This method is experimental so not public. You may reflectively call it using {@link * SdkTracerProviderUtil#setSdkResourceProvider(SdkTracerProviderBuilder, SdkResourceProvider)}. */ SdkTracerProviderBuilder setSdkResourceProvider(SdkResourceProvider resourceProvider) { requireNonNull(resourceProvider, "resourceProvider"); + if (resourceExplicitlySet) { + throw new IllegalStateException( + "Cannot call setSdkResourceProvider after setResource or addResource has been called."); + } this.resourceProvider = resourceProvider; return this; } @@ -275,10 +291,6 @@ public SdkTracerProviderBuilder setMeterProvider(Supplier meterPr * @return The instance. */ public SdkTracerProvider build() { - if (resourceProvider != null && resourceExplicitlySet) { - throw new IllegalStateException( - "setSdkResourceProvider is mutually exclusive with setResource / addResource."); - } SdkResourceProvider effectiveSdkResourceProvider = resourceProvider != null ? resourceProvider : SdkResourceProvider.create(resource); return new SdkTracerProvider( diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilderTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilderTest.java index ec5042340f2..b491d87b767 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilderTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilderTest.java @@ -14,8 +14,12 @@ import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import io.opentelemetry.sdk.trace.internal.SdkTracerProviderUtil; +import java.util.stream.Stream; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class SdkTracerProviderBuilderTest { @@ -56,30 +60,58 @@ void setSdkResourceProvider_usedForResource() { .isEqualTo(customResource); } - @Test - void setSdkResourceProvider_mutuallyExclusiveWithSetResource() { - Resource customResource = - Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); - SdkTracerProviderBuilder builder = SdkTracerProvider.builder().setResource(customResource); - SdkTracerProviderUtil.setSdkResourceProvider( - builder, SdkResourceProvider.create(customResource)); + @ParameterizedTest + @MethodSource("resourceConfigurations") + void setSdkResourceProvider_afterResource_throws(boolean addResource, Resource resource) { + SdkTracerProviderBuilder builder = SdkTracerProvider.builder(); + if (addResource) { + builder.addResource(resource); + } else { + builder.setResource(resource); + } - assertThatThrownBy(builder::build) + assertThatThrownBy(() -> builder.setSdkResourceProvider(SdkResourceProvider.create(resource))) .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("mutually exclusive"); + .hasMessage( + "Cannot call setSdkResourceProvider after setResource or addResource has been called."); + assertThat(SdkTracerProviderUtil.getSdkResourceProvider(builder.build()).getResource()) + .isEqualTo(addResource ? Resource.getDefault().merge(resource) : resource); } - @Test - void setSdkResourceProvider_mutuallyExclusiveWithAddResource() { - Resource customResource = - Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); - SdkTracerProviderBuilder builder = SdkTracerProvider.builder().addResource(customResource); - SdkTracerProviderUtil.setSdkResourceProvider( - builder, SdkResourceProvider.create(customResource)); + @ParameterizedTest + @MethodSource("resourceConfigurations") + void resource_afterSetSdkResourceProvider_throws(boolean addResource, Resource resource) { + SdkResourceProvider resourceProvider = SdkResourceProvider.create(resource); + SdkTracerProviderBuilder builder = SdkTracerProvider.builder(); + SdkTracerProviderUtil.setSdkResourceProvider(builder, resourceProvider); - assertThatThrownBy(builder::build) + assertThatThrownBy( + () -> { + if (addResource) { + builder.addResource(resource); + } else { + builder.setResource(resource); + } + }) .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("mutually exclusive"); + .hasMessage( + "Cannot call " + + (addResource ? "addResource" : "setResource") + + " after setSdkResourceProvider has been called."); + assertThat(SdkTracerProviderUtil.getSdkResourceProvider(builder.build())) + .isSameAs(resourceProvider); + } + + private static Stream resourceConfigurations() { + Resource customResource = + Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); + return Stream.of( + Arguments.argumentSet("setResource(custom)", false, customResource), + Arguments.argumentSet("setResource(default)", false, Resource.getDefault()), + Arguments.argumentSet("setResource(empty)", false, Resource.empty()), + Arguments.argumentSet("addResource(custom)", true, customResource), + Arguments.argumentSet("addResource(default)", true, Resource.getDefault()), + Arguments.argumentSet("addResource(empty)", true, Resource.empty())); } @Test