Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions sdk/all/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,20 @@ dependencies {
jmh(project(":sdk:testing"))
jmh(project(":api:incubator"))
}

testing {
suites {
register<JvmTestSuite>("testIncubating") {
dependencies {
implementation(project(":api:incubator"))
implementation(project(":sdk:testing"))
}
}
}
}

tasks {
check {
dependsOn(testing.suites)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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) {
Expand All @@ -33,6 +43,7 @@ private ExtendedOpenTelemetrySdk(
openTelemetrySdk.getPropagators());
this.openTelemetrySdk = openTelemetrySdk;
this.configProvider = new ObfuscatedConfigProvider(configProvider);
this.resourceProvider = resolveSdkResourceProvider(openTelemetrySdk);

Copy link
Copy Markdown
Contributor

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).

}

public static ExtendedOpenTelemetrySdk create(
Expand All @@ -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.");
Comment thread
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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this log a warning?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it throws an exception above when getSdkResourceProvider() is called. That should do it huh?

}

@Override
public String toString() {
return "ExtendedOpenTelemetrySdk{"
+ "openTelemetrySdk="
+ openTelemetrySdk
+ ", configProvider="
+ configProvider.unobfuscate()
+ ", resourceProvider="
+ resourceProvider
+ "}";
}

Expand Down
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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 ResourceProvider and ComponentProvider (i.e the declarative config version), having yet another interface for providing resources is too confusing a UX.

Instead, we can just have methods in SdkResourceProviderBuilder which reflect the different styles of resources that might need to be associated with a provider:

public final class SdkResourceProviderBuilder {                                                                                                                                                                                                                                                                                                                                                                                                           
 public SdkResourceProviderBuilder addConstantResource(Resource resource);                                                                                                                                                                                                                                                                                                                                                                               
 public SdkResourceProviderBuilder addConstantResource(Resource resource, @Nullable String name);                                                                                                                                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                                                                                                                                                                                         
 public SdkResourceProvider build();                                                                                                                                                                                                                                                                                                                                                                                                                     
                                                                                                                                                                                                                                                                                                                                                                                                                                                         
 // Proposed future expansion                                                                                                                                                                                                                                                                                                                                                                                                                            
 public SdkResourceProviderBuilder addDetector(Supplier<Resource> resourceSupplier, Duration refreshInterval);                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
 public SdkResourceProviderBuilder addDetector(Supplier<Resource> resourceSupplier, Duration refreshInterval, @Nullable String name);                                                                                                                                                                                                                                                                                                                                           
}   


@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

@psx95 psx95 Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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);

@breedx-splk breedx-splk Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 escaping the constructor doing work in the constructor like this, as I think it can lead to a weird class of problem that's hard to troubleshoot, and object construction having side effects is usually unexpected behavior.

Prefer a factory method and passing in the true dependency: Resource.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok maybe escaping the constructor usually means leaking this outside the class...so that's not really happening here, but it still makes me twitchy because object construction could have side effects.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 + "}";
}
}
Loading
Loading