Repository navigation
Add experimental SdkResourceProvider implementation #8865
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,13 +61,48 @@ 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."); | ||
|
breedx-splk marked this conversation as resolved.
|
||
| } | ||
| 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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this log a warning?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it throws an exception above when |
||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return "ExtendedOpenTelemetrySdk{" | ||
| + "openTelemetrySdk=" | ||
| + openTelemetrySdk | ||
| + ", configProvider=" | ||
| + configProvider.unobfuscate() | ||
| + ", resourceProvider=" | ||
| + resourceProvider | ||
| + "}"; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oooh - interesting. Do you think this needs to be one of the 'out of the box' specified detectors?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure. I did this because I had to. I think it makes sense to build SdkResourceProvider around the idea that resource attributes may change at runtime via entity descriptive attributes, but in practice all resources today (and most in the future) are statically evaluated at config time. So having syntactic sugar to reflect the common case is a must. BTW I decided (b82c1bd) that the whole notion of a ResourceDetector should be hidden from users. Given that we already have Instead, we can just have methods in |
||
|
|
||
| @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 + "}"; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /* | ||
| * 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 { | ||
|
|
||
| /** | ||
| * 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. | ||
|
Comment on lines
+15
to
+16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I got confused by this Javadoc because the spec on Resource Detector Name suggests that it should be unique and this Javadoc explicitly states otherwise. Looking closely, this is probably not the responsible component for how the resource detectors are looked up. I'm thinking adding some clarification to the Javadoc around what this method is actually intended to be used for would be good. |
||
| */ | ||
| @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()}. | ||
| * | ||
| * <p>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. | ||
| * | ||
| * <p>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(); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we're not doing anything with this today, we could punt on this. But the whole idea is sort of contrived and pointless without it. There's no need for a dedicated ResourceDetector / ResourceProvider framework when Resource / ResourceBuilder already allow you to iteratively construct a resource. But Resource/ResourceBuilder break down with entity requirements. The design in this PR tries to accomplish the core goal of making SDK resource information accessible to things like opamp, process context sharing, etc with a forward looking api.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like a bit of future-proofing, which I think is usually discouraged in public APIs since it's so easily added later when it's needed. I'd leave it out, but don't feel strongly about it. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| /* | ||
| * 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. | ||
| * | ||
| * <p>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. | ||
| * | ||
| * <p>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<ResourceDetector> detectors; | ||
| private final Resource cachedResource; | ||
|
|
||
| SdkResourceProvider(List<ResourceDetector> detectors) { | ||
| this.detectors = Collections.unmodifiableList(new ArrayList<>(detectors)); | ||
| this.cachedResource = merge(this.detectors); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because the detectors might be doing actual, real work (like going out and discovering or finding stuff)....I don't love the idea of Prefer a factory method and passing in the true dependency:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok maybe
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what you mean by escaping the constructor. This looks odd right now because all detectors are static. But in the future, we want to add detectors that needs to be periodically refreshed. |
||
| } | ||
|
|
||
| /** 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<ResourceDetector> detectors) { | ||
| Resource resource = Resource.empty(); | ||
| for (ResourceDetector detector : detectors) { | ||
| resource = resource.merge(detector.getResource()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just wanted confirm the pairwise merge functionality here. The documentation of the Resource.merge dictates that if the two resources being merged have non-null schema types, then the resulting merged resource has no schema URL So when merging 3 or more resources with all different, non-null schema types then the resulting merged resource would have the schema type of the last resource. Is this the expected behavior? |
||
| } | ||
| return resource; | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return "SdkResourceProvider{detectors=" + detectors + "}"; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: This feels like too much "work in the constructor". Prefer moving the resolve call to the
create()method and passing in the true dependency (ResourceProvider).