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/2] 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/2] 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);