diff --git a/all/src/test/java/io/opentelemetry/all/NoSharedInternalCodeTest.java b/all/src/test/java/io/opentelemetry/all/NoSharedInternalCodeTest.java index 669d1019a55..afd6de742a4 100644 --- a/all/src/test/java/io/opentelemetry/all/NoSharedInternalCodeTest.java +++ b/all/src/test/java/io/opentelemetry/all/NoSharedInternalCodeTest.java @@ -39,6 +39,7 @@ class NoSharedInternalCodeTest { "opentelemetry-exporter-prometheus", "opentelemetry-extension-trace-propagators", "opentelemetry-opencensus-shim", + "opentelemetry-sdk", "opentelemetry-sdk-logs", "opentelemetry-sdk-metrics", "opentelemetry-sdk-profiles", 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..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 @@ -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,9 +32,12 @@ 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) { + OpenTelemetrySdk openTelemetrySdk, + SdkConfigProvider configProvider, + @Nullable SdkResourceProvider resourceProvider) { super( openTelemetrySdk.getSdkTracerProvider(), openTelemetrySdk.getSdkMeterProvider(), @@ -33,11 +45,13 @@ private ExtendedOpenTelemetrySdk( openTelemetrySdk.getPropagators()); this.openTelemetrySdk = openTelemetrySdk; this.configProvider = new ObfuscatedConfigProvider(configProvider); + this.resourceProvider = resourceProvider; } public static ExtendedOpenTelemetrySdk create( OpenTelemetrySdk openTelemetrySdk, SdkConfigProvider sdkConfigProvider) { - return new ExtendedOpenTelemetrySdk(openTelemetrySdk, sdkConfigProvider); + return new ExtendedOpenTelemetrySdk( + openTelemetrySdk, sdkConfigProvider, resolveSdkResourceProvider(openTelemetrySdk)); } @Override @@ -50,6 +64,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 +104,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..67044d70a2e --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetector.java @@ -0,0 +1,36 @@ +/* + * 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 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..7cea19e273e --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/ResourceDetector.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** 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 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(); +} 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..13d016cff07 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProvider.java @@ -0,0 +1,72 @@ +/* + * 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 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 + * guarantees are made. + */ +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; + + 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}. */ + public static SdkResourceProviderBuilder builder() { + return new SdkResourceProviderBuilder(); + } + + /** Returns an {@link SdkResourceProvider} backed by a single constant {@link Resource}. */ + public static SdkResourceProvider create(Resource resource) { + requireNonNull(resource, "resource"); + return builder().addConstantResource(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..0f193c1f146 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderBuilder.java @@ -0,0 +1,66 @@ +/* + * 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.List; +import javax.annotation.Nullable; + +/** + * 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 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 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) { + requireNonNull(resource, "resource"); + detectors.add(new ConstantResourceDetector(name, resource)); + return this; + } + + /** + * Returns a new {@link SdkResourceProvider}. + * + * @throws IllegalStateException if no resource has been added via {@link + * #addConstantResource(Resource)}. + */ + public SdkResourceProvider build() { + if (detectors.isEmpty()) { + throw new IllegalStateException( + "At least one Resource must be added via addConstantResource before calling build()."); + } + 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 new file mode 100644 index 00000000000..c54dfe39a5d --- /dev/null +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/ConstantResourceDetectorTest.java @@ -0,0 +1,35 @@ +/* + * 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 io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.resources.Resource; +import org.junit.jupiter.api.Test; + +class ConstantResourceDetectorTest { + + private static final Resource RESOURCE = + Resource.create(Attributes.of(AttributeKey.stringKey("k"), "v")); + + @Test + void constant_unnamed() { + ResourceDetector detector = new ConstantResourceDetector(null, RESOURCE); + + assertThat(detector.getName()).isNull(); + assertThat(detector.getResource()).isEqualTo(RESOURCE); + } + + @Test + void constant_named() { + ResourceDetector detector = new ConstantResourceDetector("my-detector", RESOURCE); + + assertThat(detector.getName()).isEqualTo("my-detector"); + assertThat(detector.getResource()).isEqualTo(RESOURCE); + } +} 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..02f8891e3ee --- /dev/null +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/SdkResourceProviderTest.java @@ -0,0 +1,160 @@ +/* + * 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.Arrays; +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_singleConstantResource() { + SdkResourceProvider provider = + SdkResourceProvider.builder().addConstantResource(RESOURCE_A).build(); + + assertThat(provider.getResource()).isEqualTo(RESOURCE_A); + } + + @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(first) + .addConstantResource(second) + .build(); + + 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 + 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("resource"); + assertThatThrownBy(() -> SdkResourceProvider.builder().addConstantResource(null, "name")) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("resource"); + } + + @Test + void builder_build_withoutAddConstantResource_throws() { + assertThatThrownBy(() -> SdkResourceProvider.builder().build()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("At least one Resource must be added via addConstantResource"); + } + + @Test + void getResource_allDetectorsResolvedOnceAtCreation() { + CountingDetector first = new CountingDetector(RESOURCE_A); + CountingDetector second = new CountingDetector(RESOURCE_B); + SdkResourceProvider provider = + SdkResourceProvider.createFromDetectors(Arrays.asList(first, second)); + + 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(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; + + CountingDetector(Resource resource) { + this.resource = resource; + } + + @Override + public String getName() { + return "counting"; + } + + @Override + public Resource getResource() { + invocations.incrementAndGet(); + return resource; + } + } +} 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..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 @@ -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 = @@ -51,10 +55,17 @@ 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; } @@ -62,11 +73,38 @@ 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; + } + + /** + * 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)}. + * 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; } @@ -207,8 +245,10 @@ public SdkLoggerProviderBuilder setMeterProvider(Supplier meterPr * @return an instance configured with the provided options */ public SdkLoggerProvider build() { + 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..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 @@ -6,11 +6,18 @@ 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 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 { @@ -24,8 +31,84 @@ 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); + } + + @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.setSdkResourceProvider(SdkResourceProvider.create(resource))) + .isInstanceOf(IllegalStateException.class) + .hasMessage( + "Cannot call setSdkResourceProvider after setResource or addResource has been called."); + assertThat(SdkLoggerProviderUtil.getSdkResourceProvider(builder.build()).getResource()) + .isEqualTo(addResource ? Resource.getDefault().merge(resource) : resource); + } + + @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( + () -> { + if (addResource) { + builder.addResource(resource); + } else { + builder.setResource(resource); + } + }) + .isInstanceOf(IllegalStateException.class) + .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/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..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 @@ -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<>(); @@ -63,10 +67,20 @@ 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; } @@ -74,11 +88,38 @@ 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; + } + + /** + * Sets the {@link SdkResourceProvider} used to resolve the {@link Resource} attached to all + * metrics. + * + *

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; } @@ -201,12 +242,14 @@ SdkMeterProviderBuilder addMeterConfiguratorCondition( /** Returns an {@link SdkMeterProvider} built with the configuration of this builder. */ public SdkMeterProvider build() { + 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..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 @@ -6,12 +6,19 @@ 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 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 { @@ -43,4 +50,84 @@ 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); + } + + @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); + } + + @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) + .hasMessage( + "Cannot call " + + (addResource ? "addResource" : "setResource") + + " after setSdkResourceProvider has been called."); + assertThat(SdkMeterProviderUtil.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/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..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 @@ -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 = @@ -78,10 +82,17 @@ 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; } @@ -89,11 +100,38 @@ 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; + } + + /** + * Sets the {@link SdkResourceProvider} used to resolve the {@link Resource} attached to all + * spans. + * + *

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; } @@ -253,10 +291,12 @@ public SdkTracerProviderBuilder setMeterProvider(Supplier meterPr * @return The instance. */ public SdkTracerProvider build() { + 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..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 @@ -6,13 +6,20 @@ 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 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 { @@ -26,9 +33,85 @@ 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); + } + + @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.setSdkResourceProvider(SdkResourceProvider.create(resource))) + .isInstanceOf(IllegalStateException.class) + .hasMessage( + "Cannot call setSdkResourceProvider after setResource or addResource has been called."); + assertThat(SdkTracerProviderUtil.getSdkResourceProvider(builder.build()).getResource()) + .isEqualTo(addResource ? Resource.getDefault().merge(resource) : resource); + } + + @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( + () -> { + if (addResource) { + builder.addResource(resource); + } else { + builder.setResource(resource); + } + }) + .isInstanceOf(IllegalStateException.class) + .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 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