diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/ExtendedOpenTelemetry.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/ExtendedOpenTelemetry.java
new file mode 100644
index 00000000000..3021b24a2c4
--- /dev/null
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/ExtendedOpenTelemetry.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator;
+
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.incubator.entities.EntityProvider;
+
+/** Extension to {@link OpenTelemetry} that adds {@link EntityProvider}. */
+public interface ExtendedOpenTelemetry extends OpenTelemetry {
+ /** Returns the {@link EntityProvider} for this {@link OpenTelemetry}. */
+ default EntityProvider getEntityProvider() {
+ return EntityProvider.noop();
+ }
+}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/EntityBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/EntityBuilder.java
new file mode 100644
index 00000000000..9795ba2f724
--- /dev/null
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/EntityBuilder.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator.entities;
+
+import io.opentelemetry.api.common.Attributes;
+
+/**
+ * A builder of an Entity that allows to add identifying or descriptive {@link Attributes}, as well
+ * as type and schema_url.
+ *
+ *
Entity represents an object of interest associated with produced telemetry: traces, metrics or
+ * logs.
+ *
+ *
For example, telemetry produced using OpenTelemetry SDK is normally associated with a Service
+ * entity. Similarly, OpenTelemetry defines system metrics for a host. The Host is the entity we
+ * want to associate metrics with in this case.
+ *
+ *
Entities may be also associated with produced telemetry indirectly. For example a service that
+ * produces telemetry is also related with a process in which the service runs, so we say that the
+ * Service entity is related to the Process entity. The process normally also runs on a host, so we
+ * say that the Process entity is related to the Host entity.
+ */
+public interface EntityBuilder {
+ /**
+ * Assign an OpenTelemetry schema URL to the resulting Entity.
+ *
+ * @param schemaUrl The URL of the OpenTelemetry schema being used to create this Entity.
+ * @return this
+ */
+ EntityBuilder setSchemaUrl(String schemaUrl);
+
+ /**
+ * Modify the descriptive attributes of this Entity.
+ *
+ * @param description The {@link Attributes} which describe this Entity.
+ * @return this
+ */
+ EntityBuilder withDescription(Attributes description);
+
+ /**
+ * Modify the identifying attributes of this Entity.
+ *
+ * @param id The {@link Attributes} which identify this Entity.
+ * @return this
+ */
+ EntityBuilder withId(Attributes id);
+
+ /** Emits the current entity. */
+ void emit();
+}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/EntityProvider.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/EntityProvider.java
new file mode 100644
index 00000000000..f9854724c4b
--- /dev/null
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/EntityProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator.entities;
+
+/**
+ * A registry for interacting with {@code Resource}s. The name Provider is for consistency
+ * with other languages and it is NOT loaded using reflection.
+ */
+public interface EntityProvider {
+ /**
+ * Returns a no-op {@link EntityProvider} which only creates no-op {@link EntityBuilder}s which do
+ * not record nor are emitted.
+ */
+ static EntityProvider noop() {
+ return NoopEntityProvider.INSTANCE;
+ }
+
+ /**
+ * Removes an entity from this resource.
+ *
+ * @param entityType the type of entity to remove.
+ * @return true if entity was found and removed.
+ */
+ boolean removeEntity(String entityType);
+
+ /**
+ * Attaches an entity to the current {@code Resource}.
+ *
+ *
This will only add new entities or update description of existing entities.
+ *
+ * @param entityType The type of the entity.
+ * @return A builder that can construct an entity.
+ */
+ EntityBuilder attachOrUpdateEntity(String entityType);
+}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/NoopEntityBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/NoopEntityBuilder.java
new file mode 100644
index 00000000000..59474533a98
--- /dev/null
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/NoopEntityBuilder.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator.entities;
+
+import io.opentelemetry.api.common.Attributes;
+
+final class NoopEntityBuilder implements EntityBuilder {
+
+ static final EntityBuilder INSTANCE = new NoopEntityBuilder();
+
+ @Override
+ public EntityBuilder setSchemaUrl(String schemaUrl) {
+ return this;
+ }
+
+ @Override
+ public EntityBuilder withDescription(Attributes description) {
+ return this;
+ }
+
+ @Override
+ public EntityBuilder withId(Attributes id) {
+ return this;
+ }
+
+ @Override
+ public void emit() {}
+}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/NoopEntityProvider.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/NoopEntityProvider.java
new file mode 100644
index 00000000000..08a60093531
--- /dev/null
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/NoopEntityProvider.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator.entities;
+
+final class NoopEntityProvider implements EntityProvider {
+
+ static final EntityProvider INSTANCE = new NoopEntityProvider();
+
+ @Override
+ public boolean removeEntity(String entityType) {
+ return false;
+ }
+
+ @Override
+ public EntityBuilder attachOrUpdateEntity(String entityType) {
+ return NoopEntityBuilder.INSTANCE;
+ }
+}
diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt
index 5d251deb80c..6a88413d8c4 100644
--- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt
+++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt
@@ -1,2 +1,4 @@
Comparing source compatibility of opentelemetry-sdk-common-1.52.0-SNAPSHOT.jar against opentelemetry-sdk-common-1.51.0.jar
-No changes.
\ No newline at end of file
+*** MODIFIED CLASS: PUBLIC ABSTRACT io.opentelemetry.sdk.resources.Resource (not serializable)
+ === CLASS FILE FORMAT VERSION: 52.0 <- 52.0
+ *** MODIFIED METHOD: PUBLIC NON_ABSTRACT (<- ABSTRACT) io.opentelemetry.api.common.Attributes getAttributes()
diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshaler.java
new file mode 100644
index 00000000000..e77ab7f8308
--- /dev/null
+++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshaler.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.exporter.internal.otlp;
+
+import io.opentelemetry.api.internal.StringUtils;
+import io.opentelemetry.exporter.internal.marshal.MarshalerUtil;
+import io.opentelemetry.exporter.internal.marshal.MarshalerWithSize;
+import io.opentelemetry.exporter.internal.marshal.Serializer;
+import io.opentelemetry.proto.common.v1.internal.EntityRef;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import javax.annotation.Nullable;
+
+/**
+ * A Marshaler of {@link io.opentelemetry.sdk.resources.internal.Entity}.
+ *
+ *
This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ */
+final class EntityRefMarshaler extends MarshalerWithSize {
+ @Nullable private final byte[] schemaUrlUtf8;
+ private final byte[] typeUtf8;
+ private final byte[][] idKeysUtf8;
+ private final byte[][] descriptionKeysUtf8;
+
+ @Override
+ protected void writeTo(Serializer output) throws IOException {
+ if (schemaUrlUtf8 != null) {
+ output.writeString(EntityRef.SCHEMA_URL, schemaUrlUtf8);
+ }
+ output.writeString(EntityRef.TYPE, typeUtf8);
+ output.writeRepeatedString(EntityRef.ID_KEYS, idKeysUtf8);
+ output.writeRepeatedString(EntityRef.DESCRIPTION_KEYS, descriptionKeysUtf8);
+ }
+
+ /** Consttructs an entity reference marshaler from a full entity. */
+ static EntityRefMarshaler createForEntity(Entity e) {
+ byte[] schemaUrlUtf8 = null;
+ if (!StringUtils.isNullOrEmpty(e.getSchemaUrl())) {
+ schemaUrlUtf8 = e.getSchemaUrl().getBytes(StandardCharsets.UTF_8);
+ }
+ return new EntityRefMarshaler(
+ schemaUrlUtf8,
+ e.getType().getBytes(StandardCharsets.UTF_8),
+ e.getId().asMap().keySet().stream()
+ .map(key -> key.getKey().getBytes(StandardCharsets.UTF_8))
+ .toArray(byte[][]::new),
+ e.getDescription().asMap().keySet().stream()
+ .map(key -> key.getKey().getBytes(StandardCharsets.UTF_8))
+ .toArray(byte[][]::new));
+ }
+
+ private EntityRefMarshaler(
+ @Nullable byte[] schemaUrlUtf8,
+ byte[] typeUtf8,
+ byte[][] idKeysUtf8,
+ byte[][] descriptionKeysUtf8) {
+ super(calculateSize(schemaUrlUtf8, typeUtf8, idKeysUtf8, descriptionKeysUtf8));
+ this.schemaUrlUtf8 = schemaUrlUtf8;
+ this.typeUtf8 = typeUtf8;
+ this.idKeysUtf8 = idKeysUtf8;
+ this.descriptionKeysUtf8 = descriptionKeysUtf8;
+ }
+
+ private static int calculateSize(
+ @Nullable byte[] schemaUrlUtf8,
+ byte[] typeUtf8,
+ byte[][] idKeysUtf8,
+ byte[][] descriptionKeysUtf8) {
+ int size = 0;
+ if (schemaUrlUtf8 != null) {
+ size += MarshalerUtil.sizeBytes(EntityRef.SCHEMA_URL, schemaUrlUtf8);
+ }
+ size += MarshalerUtil.sizeBytes(EntityRef.TYPE, typeUtf8);
+ MarshalerUtil.sizeRepeatedString(EntityRef.ID_KEYS, idKeysUtf8);
+ MarshalerUtil.sizeRepeatedString(EntityRef.DESCRIPTION_KEYS, descriptionKeysUtf8);
+ return size;
+ }
+}
diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ResourceMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ResourceMarshaler.java
index b3395448a79..8be7df10099 100644
--- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ResourceMarshaler.java
+++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ResourceMarshaler.java
@@ -10,6 +10,7 @@
import io.opentelemetry.exporter.internal.marshal.MarshalerWithSize;
import io.opentelemetry.exporter.internal.marshal.Serializer;
import io.opentelemetry.proto.resource.v1.internal.Resource;
+import io.opentelemetry.sdk.resources.internal.EntityUtil;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -37,7 +38,10 @@ public static ResourceMarshaler create(io.opentelemetry.sdk.resources.Resource r
RealResourceMarshaler realMarshaler =
new RealResourceMarshaler(
- KeyValueMarshaler.createForAttributes(resource.getAttributes()));
+ KeyValueMarshaler.createForAttributes(resource.getAttributes()),
+ EntityUtil.getEntities(resource).stream()
+ .map(EntityRefMarshaler::createForEntity)
+ .toArray(MarshalerWithSize[]::new));
ByteArrayOutputStream binaryBos =
new ByteArrayOutputStream(realMarshaler.getBinarySerializedSize());
@@ -70,19 +74,26 @@ public void writeTo(Serializer output) throws IOException {
private static final class RealResourceMarshaler extends MarshalerWithSize {
private final KeyValueMarshaler[] attributes;
+ private final MarshalerWithSize[] entityRefs;
- private RealResourceMarshaler(KeyValueMarshaler[] attributes) {
- super(calculateSize(attributes));
+ private RealResourceMarshaler(KeyValueMarshaler[] attributes, MarshalerWithSize[] entityRefs) {
+ super(calculateSize(attributes, entityRefs));
this.attributes = attributes;
+ this.entityRefs = entityRefs;
}
@Override
protected void writeTo(Serializer output) throws IOException {
output.serializeRepeatedMessage(Resource.ATTRIBUTES, attributes);
+ output.serializeRepeatedMessage(Resource.ENTITY_REFS, entityRefs);
}
- private static int calculateSize(KeyValueMarshaler[] attributeMarshalers) {
- return MarshalerUtil.sizeRepeatedMessage(Resource.ATTRIBUTES, attributeMarshalers);
+ private static int calculateSize(
+ KeyValueMarshaler[] attributeMarshalers, MarshalerWithSize[] entityRefs) {
+ int size = 0;
+ size += MarshalerUtil.sizeRepeatedMessage(Resource.ATTRIBUTES, attributeMarshalers);
+ size += size += MarshalerUtil.sizeRepeatedMessage(Resource.ENTITY_REFS, entityRefs);
+ return size;
}
}
}
diff --git a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshalerTest.java b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshalerTest.java
new file mode 100644
index 00000000000..b5e8cad39f5
--- /dev/null
+++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshalerTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.exporter.internal.otlp;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Message;
+import com.google.protobuf.util.JsonFormat;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.exporter.internal.marshal.Marshaler;
+import io.opentelemetry.proto.common.v1.EntityRef;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import org.junit.jupiter.api.Test;
+
+class EntityRefMarshalerTest {
+ @Test
+ void toEntityRefs() {
+ Entity e =
+ Entity.builder("test")
+ .setSchemaUrl("test-url")
+ .withDescription(Attributes.builder().put("desc.key", "desc.value").build())
+ .withId(Attributes.builder().put("id.key", "id.value").build())
+ .build();
+ EntityRef proto = parse(EntityRef.getDefaultInstance(), EntityRefMarshaler.createForEntity(e));
+ assertThat(proto.getType()).isEqualTo("test");
+ assertThat(proto.getSchemaUrl()).isEqualTo("test-url");
+ assertThat(proto.getIdKeysList()).containsExactly("id.key");
+ assertThat(proto.getDescriptionKeysList()).containsExactly("desc.key");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static T parse(T prototype, Marshaler marshaler) {
+ byte[] serialized = toByteArray(marshaler);
+ T result;
+ try {
+ result = (T) prototype.newBuilderForType().mergeFrom(serialized).build();
+ } catch (InvalidProtocolBufferException e) {
+ throw new UncheckedIOException(e);
+ }
+ // Our marshaler should produce the exact same length of serialized output (for example, field
+ // default values are not outputted), so we check that here. The output itself may have slightly
+ // different ordering, mostly due to the way we don't output oneof values in field order all the
+ // tieme. If the lengths are equal and the resulting protos are equal, the marshaling is
+ // guaranteed to be valid.
+ assertThat(result.getSerializedSize()).isEqualTo(serialized.length);
+
+ // Compare JSON
+ String json = toJson(marshaler);
+ Message.Builder builder = prototype.newBuilderForType();
+ try {
+ JsonFormat.parser().merge(json, builder);
+ } catch (InvalidProtocolBufferException e) {
+ throw new UncheckedIOException(e);
+ }
+ assertThat(builder.build()).isEqualTo(result);
+
+ return result;
+ }
+
+ private static byte[] toByteArray(Marshaler marshaler) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try {
+ marshaler.writeBinaryTo(bos);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return bos.toByteArray();
+ }
+
+ private static String toJson(Marshaler marshaler) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try {
+ marshaler.writeJsonTo(bos);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return new String(bos.toByteArray(), StandardCharsets.UTF_8);
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ExtendedOpenTelemetrySdk.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ExtendedOpenTelemetrySdk.java
new file mode 100644
index 00000000000..d4fdb19647b
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ExtendedOpenTelemetrySdk.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator;
+
+import io.opentelemetry.api.incubator.ExtendedOpenTelemetry;
+import io.opentelemetry.api.incubator.entities.EntityProvider;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.logs.SdkLoggerProvider;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import java.io.Closeable;
+
+/** A new interface for creating OpenTelemetrySdk that supports {@link EntityProvider}. */
+public interface ExtendedOpenTelemetrySdk extends ExtendedOpenTelemetry, Closeable {
+ /**
+ * Shutdown the SDK. Calls {@link SdkTracerProvider#shutdown()}, {@link
+ * SdkMeterProvider#shutdown()}, and {@link SdkLoggerProvider#shutdown()}.
+ *
+ * @return a {@link CompletableResultCode} which completes when all providers are shutdown
+ */
+ CompletableResultCode shutdown();
+
+ /** Returns a builder for {@link ExtendedOpenTelemetrySdk}. */
+ static ExtendedOpenTelemetrySdkBuilder builder() {
+ return new ExtendedOpenTelemetrySdkBuilder();
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ExtendedOpenTelemetrySdkBuilder.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ExtendedOpenTelemetrySdkBuilder.java
new file mode 100644
index 00000000000..c1c0d805264
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ExtendedOpenTelemetrySdkBuilder.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.incubator.entities.EntityProvider;
+import io.opentelemetry.context.propagation.ContextPropagators;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.OpenTelemetrySdkBuilder;
+import io.opentelemetry.sdk.extension.incubator.entities.LatestResourceSupplier;
+import io.opentelemetry.sdk.extension.incubator.entities.SdkEntityProvider;
+import io.opentelemetry.sdk.extension.incubator.entities.SdkEntityProviderBuilder;
+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.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder;
+import io.opentelemetry.sdk.trace.internal.SdkTracerProviderUtil;
+import java.util.function.Consumer;
+
+/** A new interface for creating OpenTelemetrySdk that supports {@link EntityProvider}. */
+public final class ExtendedOpenTelemetrySdkBuilder {
+ private ContextPropagators propagators = ContextPropagators.noop();
+ private final SdkTracerProviderBuilder tracerProviderBuilder = SdkTracerProvider.builder();
+ private final SdkMeterProviderBuilder meterProviderBuilder = SdkMeterProvider.builder();
+ private final SdkLoggerProviderBuilder loggerProviderBuilder = SdkLoggerProvider.builder();
+ private final SdkEntityProviderBuilder resourceProviderBuilder = SdkEntityProvider.builder();
+
+ /** Sets the {@link ContextPropagators} to use. */
+ public ExtendedOpenTelemetrySdkBuilder setPropagators(ContextPropagators propagators) {
+ this.propagators = propagators;
+ return this;
+ }
+
+ /**
+ * Applies a consumer callback to configure the TracerProvider being built for this OpenTelemetry.
+ *
+ * @param configurator A callback fleshing out tracers.
+ * @return this
+ */
+ public ExtendedOpenTelemetrySdkBuilder withTracerProvider(
+ Consumer configurator) {
+ configurator.accept(this.tracerProviderBuilder);
+ return this;
+ }
+
+ /**
+ * Applies a consumer callback to configure the MeterProvider being built for this OpenTelemetry.
+ *
+ * @param configurator A callback fleshing out meters.
+ * @return this
+ */
+ public ExtendedOpenTelemetrySdkBuilder withMeterProvider(
+ Consumer configurator) {
+ configurator.accept(this.meterProviderBuilder);
+ return this;
+ }
+
+ /**
+ * Applies a consumer callback to configure the LoggerProvider being built for this OpenTelemetry.
+ *
+ * @param configurator A callback fleshing out meters.
+ * @return this
+ */
+ public ExtendedOpenTelemetrySdkBuilder withLoggerProvider(
+ Consumer configurator) {
+ configurator.accept(this.loggerProviderBuilder);
+ return this;
+ }
+
+ /**
+ * Returns a new {@link OpenTelemetrySdk} built with the configuration of this {@link
+ * OpenTelemetrySdkBuilder}. This SDK is not registered as the global {@link
+ * io.opentelemetry.api.OpenTelemetry}. It is recommended that you register one SDK using {@link
+ * OpenTelemetrySdkBuilder#buildAndRegisterGlobal()} for use by instrumentation that requires
+ * access to a global instance of {@link io.opentelemetry.api.OpenTelemetry}.
+ *
+ * @see GlobalOpenTelemetry
+ */
+ public ExtendedOpenTelemetrySdk build() {
+ SdkEntityProvider resourceProvider = resourceProviderBuilder.build();
+ // TODO - allow startup delay configuration
+ LatestResourceSupplier sdkResourceSupplier = new LatestResourceSupplier(200);
+ resourceProvider.onChange(sdkResourceSupplier);
+ SdkTracerProvider tracerProvider =
+ SdkTracerProviderUtil.setResourceSupplier(tracerProviderBuilder, sdkResourceSupplier)
+ .build();
+ SdkMeterProvider meterProvider =
+ SdkMeterProviderUtil.setResourceSupplier(meterProviderBuilder, sdkResourceSupplier).build();
+ SdkLoggerProvider loggerProvider =
+ SdkLoggerProviderUtil.setResourceSupplier(loggerProviderBuilder, sdkResourceSupplier)
+ .build();
+ return new ObfuscatedExtendedOpenTelemerySdk(
+ resourceProvider, tracerProvider, meterProvider, loggerProvider, propagators);
+ }
+
+ /**
+ * Returns a new {@link OpenTelemetrySdk} built with the configuration of this {@link
+ * OpenTelemetrySdkBuilder} and registers it as the global {@link
+ * io.opentelemetry.api.OpenTelemetry}. An exception will be thrown if this method is attempted to
+ * be called multiple times in the lifecycle of an application - ensure you have only one SDK for
+ * use as the global instance. If you need to configure multiple SDKs for tests, use {@link
+ * GlobalOpenTelemetry#resetForTest()} between them.
+ *
+ * @see GlobalOpenTelemetry
+ */
+ public ExtendedOpenTelemetrySdk buildAndRegisterGlobal() {
+ ExtendedOpenTelemetrySdk sdk = build();
+ GlobalOpenTelemetry.set(sdk);
+ return sdk;
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ObfuscatedExtendedOpenTelemerySdk.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ObfuscatedExtendedOpenTelemerySdk.java
new file mode 100644
index 00000000000..d868e0b26a5
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ObfuscatedExtendedOpenTelemerySdk.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator;
+
+import io.opentelemetry.api.incubator.entities.EntityBuilder;
+import io.opentelemetry.api.incubator.entities.EntityProvider;
+import io.opentelemetry.api.logs.LoggerBuilder;
+import io.opentelemetry.api.logs.LoggerProvider;
+import io.opentelemetry.api.metrics.MeterBuilder;
+import io.opentelemetry.api.metrics.MeterProvider;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.api.trace.TracerBuilder;
+import io.opentelemetry.api.trace.TracerProvider;
+import io.opentelemetry.context.propagation.ContextPropagators;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.extension.incubator.entities.SdkEntityProvider;
+import io.opentelemetry.sdk.logs.SdkLoggerProvider;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Logger;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** The SDK implementation of {@link ExtendedOpenTelemetrySdk}. */
+final class ObfuscatedExtendedOpenTelemerySdk implements ExtendedOpenTelemetrySdk {
+
+ private static final Logger LOGGER =
+ Logger.getLogger(ObfuscatedExtendedOpenTelemerySdk.class.getName());
+ private final AtomicBoolean isShutdown = new AtomicBoolean(false);
+ private final ObfuscatedTracerProvider tracerProvider;
+ private final ObfuscatedMeterProvider meterProvider;
+ private final ObfuscatedLoggerProvider loggerProvider;
+ private final ObfuscatedEntityProvider entityProvider;
+ private final ContextPropagators propagators;
+
+ ObfuscatedExtendedOpenTelemerySdk(
+ SdkEntityProvider entityProvider,
+ SdkTracerProvider tracerProvider,
+ SdkMeterProvider meterProvider,
+ SdkLoggerProvider loggerProvider,
+ ContextPropagators propagators) {
+ this.entityProvider = new ObfuscatedEntityProvider(entityProvider);
+ this.tracerProvider = new ObfuscatedTracerProvider(tracerProvider);
+ this.meterProvider = new ObfuscatedMeterProvider(meterProvider);
+ this.loggerProvider = new ObfuscatedLoggerProvider(loggerProvider);
+ this.propagators = propagators;
+ }
+
+ @Override
+ public CompletableResultCode shutdown() {
+ if (!isShutdown.compareAndSet(false, true)) {
+ LOGGER.info("Multiple shutdown calls");
+ return CompletableResultCode.ofSuccess();
+ }
+ List results = new ArrayList<>();
+ results.add(tracerProvider.unobfuscate().shutdown());
+ results.add(meterProvider.unobfuscate().shutdown());
+ results.add(loggerProvider.unobfuscate().shutdown());
+ return CompletableResultCode.ofAll(results);
+ }
+
+ @Override
+ public void close() {
+ shutdown().join(10, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public TracerProvider getTracerProvider() {
+ return tracerProvider;
+ }
+
+ @Override
+ public MeterProvider getMeterProvider() {
+ return meterProvider;
+ }
+
+ @Override
+ public LoggerProvider getLogsBridge() {
+ return loggerProvider;
+ }
+
+ @Override
+ public EntityProvider getEntityProvider() {
+ return entityProvider;
+ }
+
+ @Override
+ public ContextPropagators getPropagators() {
+ return propagators;
+ }
+
+ @Override
+ public String toString() {
+ return "ExtendedOpenTelemetrySdk{"
+ + "entityProvider="
+ + entityProvider.unobfuscate()
+ + ", tracerProvider="
+ + tracerProvider.unobfuscate()
+ + ", meterProvider="
+ + meterProvider.unobfuscate()
+ + ", loggerProvider="
+ + loggerProvider.unobfuscate()
+ + ", propagators="
+ + propagators
+ + "}";
+ }
+
+ /**
+ * This class allows the SDK to unobfuscate an obfuscated static global provider.
+ *
+ *
Static global providers are obfuscated when they are returned from the API to prevent users
+ * from casting them to their SDK specific implementation. For example, we do not want users to
+ * use patterns like {@code (SdkTracerProvider) openTelemetry.getTracerProvider()}.
+ */
+ @ThreadSafe
+ // Visible for testing
+ static class ObfuscatedTracerProvider implements TracerProvider {
+
+ private final SdkTracerProvider delegate;
+
+ ObfuscatedTracerProvider(SdkTracerProvider delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Tracer get(String instrumentationScopeName) {
+ return delegate.get(instrumentationScopeName);
+ }
+
+ @Override
+ public Tracer get(String instrumentationScopeName, String instrumentationScopeVersion) {
+ return delegate.get(instrumentationScopeName, instrumentationScopeVersion);
+ }
+
+ @Override
+ public TracerBuilder tracerBuilder(String instrumentationScopeName) {
+ return delegate.tracerBuilder(instrumentationScopeName);
+ }
+
+ public SdkTracerProvider unobfuscate() {
+ return delegate;
+ }
+ }
+
+ /**
+ * This class allows the SDK to unobfuscate an obfuscated static global provider.
+ *
+ *
Static global providers are obfuscated when they are returned from the API to prevent users
+ * from casting them to their SDK specific implementation. For example, we do not want users to
+ * use patterns like {@code (SdkMeterProvider) openTelemetry.getMeterProvider()}.
+ */
+ @ThreadSafe
+ // Visible for testing
+ static class ObfuscatedMeterProvider implements MeterProvider {
+
+ private final SdkMeterProvider delegate;
+
+ ObfuscatedMeterProvider(SdkMeterProvider delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public MeterBuilder meterBuilder(String instrumentationScopeName) {
+ return delegate.meterBuilder(instrumentationScopeName);
+ }
+
+ public SdkMeterProvider unobfuscate() {
+ return delegate;
+ }
+ }
+
+ /**
+ * This class allows the SDK to unobfuscate an obfuscated static global provider.
+ *
+ *
Static global providers are obfuscated when they are returned from the API to prevent users
+ * from casting them to their SDK specific implementation. For example, we do not want users to
+ * use patterns like {@code (SdkMeterProvider) openTelemetry.getMeterProvider()}.
+ */
+ @ThreadSafe
+ // Visible for testing
+ static class ObfuscatedLoggerProvider implements LoggerProvider {
+
+ private final SdkLoggerProvider delegate;
+
+ ObfuscatedLoggerProvider(SdkLoggerProvider delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public LoggerBuilder loggerBuilder(String instrumentationScopeName) {
+ return delegate.loggerBuilder(instrumentationScopeName);
+ }
+
+ public SdkLoggerProvider unobfuscate() {
+ return delegate;
+ }
+ }
+
+ /**
+ * This class allows the SDK to unobfuscate an obfuscated static global provider.
+ *
+ *
Static global providers are obfuscated when they are returned from the API to prevent users
+ * from casting them to their SDK specific implementation. For example, we do not want users to
+ * use patterns like {@code (SdkResourceProvider) openTelemetry.getResourceProvider()}.
+ */
+ @ThreadSafe
+ // Visible for testing
+ static class ObfuscatedEntityProvider implements EntityProvider {
+
+ private final SdkEntityProvider delegate;
+
+ ObfuscatedEntityProvider(SdkEntityProvider delegate) {
+ this.delegate = delegate;
+ }
+
+ public SdkEntityProvider unobfuscate() {
+ return delegate;
+ }
+
+ @Override
+ public boolean removeEntity(String entityType) {
+ return delegate.removeEntity(entityType);
+ }
+
+ @Override
+ public EntityBuilder attachOrUpdateEntity(String entityType) {
+ return delegate.attachOrUpdateEntity(entityType);
+ }
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/CurrentThreadExecutorService.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/CurrentThreadExecutorService.java
new file mode 100644
index 00000000000..8718ecf1804
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/CurrentThreadExecutorService.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * An executor service that runs all jobs immediately on the current thread.
+ *
+ *
We use this so SDK users can determine how to isolate {@link EntityListener}s with the default
+ * being no isolation of events.
+ */
+final class CurrentThreadExecutorService implements ExecutorService {
+ private volatile boolean shutdown = false;
+
+ @Override
+ public void execute(Runnable command) {
+ if (shutdown) {
+ throw new RejectedExecutionException("ExecutorService is shut down");
+ }
+ command.run();
+ }
+
+ @Override
+ public List> invokeAll(Collection extends Callable> tasks) {
+ if (shutdown) {
+ throw new RejectedExecutionException("ExecutorService is shut down");
+ }
+ // Execute all tasks synchronously and collect their Futures
+ return tasks.stream().map(task -> submit(task)).collect(Collectors.toList());
+ }
+
+ @Override
+ public List> invokeAll(
+ Collection extends Callable> tasks, long timeout, TimeUnit unit) {
+ return invokeAll(tasks);
+ }
+
+ @Override
+ public T invokeAny(Collection extends Callable> tasks)
+ throws InterruptedException, ExecutionException {
+ if (shutdown) {
+ throw new RejectedExecutionException("ExecutorService is shut down");
+ }
+ // Execute all tasks synchronously and return first success.
+ for (Callable task : tasks) {
+ try {
+ // We wrap the task in a `submit` call to get ExecutionExceptions.
+ return submit(task).get();
+ } catch (ExecutionException e) {
+ // Ignore this error, and try the next one.
+ }
+ }
+ throw new ExecutionException("No tasks completed successfully", null);
+ }
+
+ @Override
+ public T invokeAny(Collection extends Callable> tasks, long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException {
+ if (shutdown) {
+ throw new RejectedExecutionException("ExecutorService is shut down");
+ }
+ return invokeAny(tasks);
+ }
+
+ @Override
+ public Future submit(Callable task) {
+ if (shutdown) {
+ throw new RejectedExecutionException("ExecutorService is shut down");
+ }
+ FutureTask future = new FutureTask<>(task);
+ // Run in this thread.
+ future.run();
+ return future;
+ }
+
+ @Override
+ public Future> submit(Runnable task) {
+ if (shutdown) {
+ throw new RejectedExecutionException("ExecutorService is shut down");
+ }
+ FutureTask> future = new FutureTask<>(task, null);
+ // Run in this thread.
+ future.run();
+ return future;
+ }
+
+ @Override
+ public Future submit(Runnable task, T result) {
+ if (shutdown) {
+ throw new RejectedExecutionException("ExecutorService is shut down");
+ }
+ FutureTask future = new FutureTask<>(task, result);
+ // Run in this thread.
+ future.run();
+ return future;
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return shutdown;
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return shutdown;
+ }
+
+ @Override
+ public void shutdown() {
+ shutdown = true;
+ }
+
+ @Override
+ public List shutdownNow() {
+ shutdown = true;
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit unit) {
+ return isTerminated();
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/EntityListener.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/EntityListener.java
new file mode 100644
index 00000000000..55aece757b3
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/EntityListener.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities;
+
+import io.opentelemetry.sdk.resources.Resource;
+
+/** A listener for changes in the EntityState of this SDK. */
+public interface EntityListener {
+
+ /**
+ * Called when the EntityProvider is initialized with full resource state.
+ *
+ * @param resource The initialized state of the Resource.
+ */
+ public void onResourceInit(Resource resource);
+
+ /**
+ * Called when an entity has been added or its state has changed.
+ *
+ * @param state The current state of the entity.
+ * @param resource The current state of the Resource.
+ */
+ public void onEntityState(EntityState state, Resource resource);
+
+ /**
+ * Called when an entity has been removed.
+ *
+ * @param state The current state of the removed entity.
+ * @param resource The current state of the Resource.
+ */
+ public void onEntityDelete(EntityState state, Resource resource);
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/EntityState.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/EntityState.java
new file mode 100644
index 00000000000..07ef93bf359
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/EntityState.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities;
+
+import io.opentelemetry.api.common.Attributes;
+import javax.annotation.Nullable;
+
+/** The current state of an Entity. */
+public interface EntityState {
+ /** Returns the type of the Entity. */
+ String getType();
+
+ /** Returns the schema_url of the Entity, or null. */
+ @Nullable
+ String getSchemaUrl();
+
+ /** Returns the identity of the Entity. */
+ Attributes getId();
+
+ /** Returns the description of the Entity. */
+ Attributes getDescription();
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/LatestResourceSupplier.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/LatestResourceSupplier.java
new file mode 100644
index 00000000000..0458517c7cc
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/LatestResourceSupplier.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities;
+
+import io.opentelemetry.sdk.resources.Resource;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+/**
+ * A supplier of resource which listenes to Entity events.
+ *
+ *
This class will wait for availability via ResourceInitialized event before returning from any
+ * `getResource` call.
+ */
+public class LatestResourceSupplier implements EntityListener, Supplier {
+
+ private final AtomicReference current = new AtomicReference<>(null);
+ private final Object initializationLock = new Object();
+ private final long maxStartupDelayMs;
+
+ public LatestResourceSupplier(long maxStartupDelayMs) {
+ this.maxStartupDelayMs = maxStartupDelayMs;
+ }
+
+ @Override
+ public void onResourceInit(Resource resource) {
+ current.lazySet(resource);
+ // Here we can notify anyone waiting on initialization.
+ synchronized (initializationLock) {
+ initializationLock.notifyAll();
+ }
+ }
+
+ @Override
+ public void onEntityState(EntityState state, Resource resource) {
+ current.lazySet(resource);
+ }
+
+ @Override
+ public void onEntityDelete(EntityState state, Resource resource) {
+ current.lazySet(resource);
+ }
+
+ @Override
+ public Resource get() {
+ Resource result = this.current.get();
+ if (result == null) {
+ synchronized (initializationLock) {
+ result = this.current.get();
+ long startTime = System.currentTimeMillis();
+ boolean stillWaiting = true;
+ while (result == null || stillWaiting) {
+ long elapsedTime = System.currentTimeMillis() - startTime;
+ long remainingTime = maxStartupDelayMs - elapsedTime;
+ if (remainingTime <= 0) {
+ stillWaiting = false;
+ break;
+ }
+ try {
+ initializationLock.wait(remainingTime);
+ } catch (InterruptedException e) {
+ break;
+ }
+ }
+ if (result == null) {
+ result = Resource.getDefault();
+ }
+ }
+ }
+ return result;
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/ResourceDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/ResourceDetector.java
new file mode 100644
index 00000000000..a80bf0ddbcd
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/ResourceDetector.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities;
+
+import io.opentelemetry.api.incubator.entities.EntityProvider;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+
+/**
+ * The Resource detector in the SDK is responsible for detecting possible entities that could
+ * identify the SDK (called "associated entities"). For Example, if the SDK is running in a
+ * kubernetes pod, it may provide an Entity for that pod.
+ */
+public interface ResourceDetector {
+ /**
+ * Reports detected entities.
+ *
+ * @param provider The provider where entities are reported.
+ */
+ public CompletableResultCode report(EntityProvider provider);
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityBuilder.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityBuilder.java
new file mode 100644
index 00000000000..3cf68e2f56c
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityBuilder.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.entities.EntityBuilder;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import java.util.function.Consumer;
+
+final class SdkEntityBuilder implements EntityBuilder {
+ private final io.opentelemetry.sdk.resources.internal.EntityBuilder builder;
+ private final Consumer emitter;
+
+ SdkEntityBuilder(String entityType, Consumer emitter) {
+ this.builder = Entity.builder(entityType);
+ this.emitter = emitter;
+ }
+
+ @Override
+ public EntityBuilder setSchemaUrl(String schemaUrl) {
+ builder.setSchemaUrl(schemaUrl);
+ return this;
+ }
+
+ @Override
+ public EntityBuilder withDescription(Attributes description) {
+ builder.withDescription(description);
+ return this;
+ }
+
+ @Override
+ public EntityBuilder withId(Attributes id) {
+ builder.withId(id);
+ return this;
+ }
+
+ @Override
+ public void emit() {
+ emitter.accept(builder.build());
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityProvider.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityProvider.java
new file mode 100644
index 00000000000..4197c7e12f2
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityProvider.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities;
+
+import io.opentelemetry.api.incubator.entities.EntityBuilder;
+import io.opentelemetry.api.incubator.entities.EntityProvider;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import java.util.Collection;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/** The SDK implementation of {@link EntityProvider}. */
+public final class SdkEntityProvider implements EntityProvider {
+ private final SdkResourceSharedState state;
+
+ SdkEntityProvider(ExecutorService executorService, Collection detectors) {
+ this.state = new SdkResourceSharedState(executorService);
+ state.beginInitialization(detectors, this);
+ }
+
+ /**
+ * Creates a builder for SdkEntityProvider.
+ *
+ * @return The new builder.
+ */
+ public static SdkEntityProviderBuilder builder() {
+ return new SdkEntityProviderBuilder();
+ }
+
+ @Override
+ public String toString() {
+ return "SdkResourceProvider{}";
+ }
+
+ @Override
+ public boolean removeEntity(String entityType) {
+ return state.removeEntity(entityType);
+ }
+
+ @Override
+ public EntityBuilder attachOrUpdateEntity(String entityType) {
+ return new SdkEntityBuilder(entityType, state::addOrUpdateEntity);
+ }
+
+ public void onChange(EntityListener listener) {
+ state.addListener(listener);
+ }
+
+ /**
+ * Shutdown the provider. The resulting {@link CompletableResultCode} completes when all complete.
+ */
+ public CompletableResultCode shutdown() {
+ return state.shutdown();
+ }
+
+ /** Close the provider. Calls {@link #shutdown()} and blocks waiting for it to complete. */
+ public void close() {
+ shutdown().join(10, TimeUnit.SECONDS);
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityProviderBuilder.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityProviderBuilder.java
new file mode 100644
index 00000000000..48454d2bbe8
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityProviderBuilder.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities;
+
+import io.opentelemetry.sdk.extension.incubator.entities.detectors.ServiceDetector;
+import io.opentelemetry.sdk.extension.incubator.entities.detectors.TelemetrySdkDetector;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+/** A builder for {@link SdkEntityProvider}. */
+public final class SdkEntityProviderBuilder {
+ private final List detectors = new ArrayList<>();
+ private boolean includeDefaults = true;
+ // TODO - add configuraiton settings for this.
+ private ExecutorService executorService = new CurrentThreadExecutorService();
+
+ /**
+ * Adds a {@link ResourceDetector} that will be run when constructing this provider.
+ *
+ * @param detector The resource detector.
+ * @return this
+ */
+ public SdkEntityProviderBuilder addDetector(ResourceDetector detector) {
+ this.detectors.add(detector);
+ return this;
+ }
+
+ /**
+ * Sets the excutor service which isolates entity listeners and resource detectors.
+ *
+ * @param executorService The executor service to use for async tasks.
+ * @return this
+ */
+ SdkEntityProviderBuilder setListenerExecutorService(ExecutorService executorService) {
+ this.executorService = executorService;
+ return this;
+ }
+
+ /**
+ * Configure whether to include SDK default resoruce detection.
+ *
+ * @param include true if defaults should be used.
+ * @return this
+ */
+ public SdkEntityProviderBuilder includeDefaults(boolean include) {
+ this.includeDefaults = include;
+ return this;
+ }
+
+ public SdkEntityProvider build() {
+ // TODO - have defaults in the front?
+ if (includeDefaults) {
+ detectors.add(new ServiceDetector());
+ detectors.add(new TelemetrySdkDetector());
+ }
+ return new SdkEntityProvider(executorService, Collections.unmodifiableCollection(detectors));
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityState.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityState.java
new file mode 100644
index 00000000000..de049edd46e
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityState.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import javax.annotation.Nullable;
+
+final class SdkEntityState implements EntityState {
+ private final Entity delegate;
+
+ SdkEntityState(Entity delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public String getType() {
+ return delegate.getType();
+ }
+
+ @Override
+ @Nullable
+ public String getSchemaUrl() {
+ return delegate.getSchemaUrl();
+ }
+
+ @Override
+ public Attributes getId() {
+ return delegate.getId();
+ }
+
+ @Override
+ public Attributes getDescription() {
+ return delegate.getDescription();
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkResourceSharedState.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkResourceSharedState.java
new file mode 100644
index 00000000000..951bd50f8ef
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkResourceSharedState.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities;
+
+import io.opentelemetry.api.incubator.entities.EntityProvider;
+import io.opentelemetry.api.internal.GuardedBy;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.internal.ThrottlingLogger;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import io.opentelemetry.sdk.resources.internal.EntityUtil;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * This class does all state and listener management for a {@link Resource} constructed of {@link
+ * Entity}s.
+ */
+final class SdkResourceSharedState {
+
+ // The currently advertised Resource to other SDK providers.
+ private final AtomicReference resource = new AtomicReference<>(Resource.empty());
+ private final AtomicBoolean initialized = new AtomicBoolean(false);
+ private final Object writeLock = new Object();
+ private final List listeners = new CopyOnWriteArrayList<>();
+ private final ExecutorService listenerExecutor;
+
+ // Our internal storage of registered entities.
+ @GuardedBy("writeLock")
+ private final ArrayList entities = new ArrayList<>();
+
+ private static final ThrottlingLogger logger =
+ new ThrottlingLogger(Logger.getLogger(SdkResourceSharedState.class.getName()));
+
+ SdkResourceSharedState(ExecutorService listenerExecutor) {
+ this.listenerExecutor = listenerExecutor;
+ }
+
+ /**
+ * Begins initializing state with the given resource detectors.
+ *
+ *
This will initialize after all resource detectors have completed, or 200 ms.
+ *
+ * @param detectors The set of resource detectors that are considered initializing.
+ * @param provider The entity provider for the resource detectors.
+ */
+ @SuppressWarnings("FutureReturnValueIgnored")
+ void beginInitialization(Collection detectors, EntityProvider provider) {
+ // TODO - Should we create a different instance of entity provider for initial resource
+ // detection?
+ Collection results = new ArrayList<>(detectors.size());
+ for (ResourceDetector d : detectors) {
+ results.add(d.report(provider));
+ }
+ CompletableResultCode allDone = CompletableResultCode.ofAll(results);
+ // Ensure our blocking is done using the async model provided to use via
+ // ExecutorService.
+ listenerExecutor.submit(
+ () -> {
+ allDone.join(200, TimeUnit.MILLISECONDS);
+ this.initialize();
+ });
+ }
+
+ /**
+ * Shutdown the provider. The resulting {@link CompletableResultCode} completes when all complete.
+ */
+ CompletableResultCode shutdown() {
+ // TODO - Actually figure out how to wait for shutdown and deal with pending tasks.
+ listenerExecutor.shutdown();
+ return CompletableResultCode.ofSuccess();
+ }
+
+ /** Returns the currently active resource. */
+ public Resource getResource() {
+
+ Resource result = resource.get();
+ // We do this to make NullAway happy.
+ if (result == null) {
+ throw new IllegalStateException("SdkResource should never have null resource");
+ }
+ return result;
+ }
+
+ private static boolean hasSameSchemaUrl(Entity lhs, Entity rhs) {
+ if (lhs.getSchemaUrl() != null) {
+ return lhs.getSchemaUrl().equals(rhs.getSchemaUrl());
+ }
+ return rhs.getSchemaUrl() == null;
+ }
+
+ /**
+ * Removes an entity by type and notifies listeners.
+ *
+ * @param entityType The entity type to remove.
+ */
+ boolean removeEntity(String entityType) {
+ synchronized (writeLock) {
+ @Nullable Entity removed = null;
+ for (Entity existing : entities) {
+ if (existing.getType().equals(entityType)) {
+ removed = existing;
+ }
+ }
+ if (removed == null) {
+ return false;
+ }
+ entities.remove(removed);
+ Resource result = EntityUtil.createResource(entities);
+ resource.lazySet(result);
+ publishEntityDelete(new SdkEntityState(removed), result);
+ return true;
+ }
+ }
+
+ /**
+ * Adds an entity and notifies listeners.
+ *
+ *
Note: This will not add an entity on conflict. This will update the description if the
+ * entity already exists.
+ *
+ * @param e The entity type to add.
+ */
+ void addOrUpdateEntity(Entity e) {
+ synchronized (writeLock) {
+ @Nullable Entity conflict = null;
+ for (Entity existing : entities) {
+ if (existing.getType().equals(e.getType())) {
+ conflict = existing;
+ }
+ }
+ Entity newState = e;
+ if (conflict != null) {
+ if (hasSameSchemaUrl(conflict, e) && conflict.getId().equals(e.getId())) {
+ // We can merge descriptive attributes.
+ entities.remove(conflict);
+ io.opentelemetry.sdk.resources.internal.EntityBuilder newEntity =
+ Entity.builder(conflict.getType())
+ .withId(conflict.getId())
+ .withDescription(
+ conflict.getDescription().toBuilder().putAll(e.getDescription()).build());
+ if (conflict.getSchemaUrl() != null) {
+ newEntity.setSchemaUrl(conflict.getSchemaUrl());
+ }
+ newState = newEntity.build();
+ entities.add(newState);
+ } else {
+ logger.log(Level.INFO, "Ignoring new entity, conflicts with existing: " + e);
+ return;
+ }
+ } else {
+ entities.add(e);
+ }
+ Resource result = EntityUtil.createResource(entities);
+ resource.lazySet(result);
+ publishEntityStateChange(new SdkEntityState(newState), result);
+ }
+ }
+
+ /** Mark the resource as initialized and notify listeners. */
+ @SuppressWarnings("FutureReturnValueIgnored")
+ private void initialize() {
+ // Prevent writes so initialize events happen before updates,
+ // in the event of other issues.
+ synchronized (writeLock) {
+ Resource resource = this.resource.get();
+ if (resource == null) {
+ // Catastrophic failure, TODO - some kind of logic error message
+ return;
+ }
+ // We only do this once.
+ if (initialized.compareAndSet(false, true)) {
+ for (EntityListener listener : listeners) {
+ listenerExecutor.submit(() -> listener.onResourceInit(resource));
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ private void publishEntityStateChange(EntityState state, Resource resource) {
+ if (initialized.get()) {
+ for (EntityListener listener : listeners) {
+ // We isolate listener execution via executor, if configured.
+ // We ignore failures on futures to avoid having one listener block others.
+ listenerExecutor.submit(() -> listener.onEntityState(state, resource));
+ }
+ }
+ }
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ private void publishEntityDelete(EntityState deleted, Resource resource) {
+ if (initialized.get()) {
+ for (EntityListener listener : listeners) {
+ // We isolate listener execution via executor, if configured.
+ // We ignore failures on futures to avoid having one listener block others.
+ listenerExecutor.submit(() -> listener.onEntityDelete(deleted, resource));
+ }
+ }
+ }
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void addListener(EntityListener listener) {
+ listeners.add(listener);
+ if (initialized.get()) {
+ // We isolate listener execution via executor, if configured.
+ // We ignore failures on futures to avoid having one listener block others.
+ listenerExecutor.submit(() -> listener.onResourceInit(getResource()));
+ }
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/detectors/ServiceDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/detectors/ServiceDetector.java
new file mode 100644
index 00000000000..29e4536bbb4
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/detectors/ServiceDetector.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities.detectors;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.entities.EntityProvider;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.extension.incubator.entities.ResourceDetector;
+import java.util.UUID;
+
+/**
+ * Detection for {@code service} entity.
+ *
+ *
See: service
+ * entity
+ */
+public final class ServiceDetector implements ResourceDetector {
+ private static final String SCHEMA_URL = "https://opentelemetry.io/schemas/1.34.0";
+ private static final String ENTITY_TYPE = "service";
+ private static final AttributeKey SERVICE_NAME = AttributeKey.stringKey("service.name");
+ private static final AttributeKey SERVICE_INSTANCE_ID =
+ AttributeKey.stringKey("service.instance.id");
+ private static final UUID FALLBACK_INSTANCE_ID = UUID.randomUUID();
+
+ private static String getServiceName() {
+ return System.getenv().getOrDefault("OTEL_SERVICE_NAME", "unknown_service:java");
+ }
+
+ private static String getServiceInstanceId() {
+ // TODO - no way for users to specify a non-default.
+ return FALLBACK_INSTANCE_ID.toString();
+ }
+
+ @Override
+ public CompletableResultCode report(EntityProvider provider) {
+ // We only run on startup.
+ provider
+ .attachOrUpdateEntity(ENTITY_TYPE)
+ .setSchemaUrl(SCHEMA_URL)
+ .withId(
+ // Note: Identifying attributes MUST be provided together.
+ Attributes.builder()
+ .put(SERVICE_NAME, getServiceName())
+ .put(SERVICE_INSTANCE_ID, getServiceInstanceId())
+ .build())
+ // TODO - Need to figure out version
+ .emit();
+ return CompletableResultCode.ofSuccess();
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/detectors/TelemetrySdkDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/detectors/TelemetrySdkDetector.java
new file mode 100644
index 00000000000..458a1afddb9
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/detectors/TelemetrySdkDetector.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities.detectors;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.entities.EntityProvider;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.common.internal.OtelVersion;
+import io.opentelemetry.sdk.extension.incubator.entities.ResourceDetector;
+
+/**
+ * Detection for {@code telemetry.sdk} entity.
+ *
+ *
See: teleemtry.sdk
+ * entity
+ */
+public final class TelemetrySdkDetector implements ResourceDetector {
+ private static final String SCHEMA_URL = "https://opentelemetry.io/schemas/1.34.0";
+ private static final String ENTITY_TYPE = "telemetry.sdk";
+ private static final AttributeKey TELEMETRY_SDK_LANGUAGE =
+ AttributeKey.stringKey("telemetry.sdk.language");
+ private static final AttributeKey TELEMETRY_SDK_NAME =
+ AttributeKey.stringKey("telemetry.sdk.name");
+ private static final AttributeKey TELEMETRY_SDK_VERSION =
+ AttributeKey.stringKey("telemetry.sdk.version");
+
+ @Override
+ public CompletableResultCode report(EntityProvider provider) {
+ provider
+ .attachOrUpdateEntity(ENTITY_TYPE)
+ .setSchemaUrl(SCHEMA_URL)
+ .withId(
+ Attributes.builder()
+ .put(TELEMETRY_SDK_NAME, "opentelemetry")
+ .put(TELEMETRY_SDK_LANGUAGE, "java")
+ .build())
+ .withDescription(
+ Attributes.builder().put(TELEMETRY_SDK_VERSION, OtelVersion.VERSION).build())
+ .emit();
+ return CompletableResultCode.ofSuccess();
+ }
+}
diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/TestExtendedOpenTelemetrySdk.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/TestExtendedOpenTelemetrySdk.java
new file mode 100644
index 00000000000..80c8321ab2a
--- /dev/null
+++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/TestExtendedOpenTelemetrySdk.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.Meter;
+import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
+import org.junit.jupiter.api.Test;
+
+class TestExtendedOpenTelemetrySdk {
+ private final InMemoryMetricReader sdkMeterReader = InMemoryMetricReader.create();
+
+ @Test
+ void endToEnd() {
+ ExtendedOpenTelemetrySdk otel =
+ ExtendedOpenTelemetrySdk.builder()
+ .withMeterProvider(builder -> builder.registerMetricReader(sdkMeterReader))
+ .build();
+ // Generate our first entity.
+ otel.getEntityProvider()
+ .attachOrUpdateEntity("test")
+ .withId(Attributes.builder().put("test.id", 1).build())
+ .emit();
+ // Write a metric.
+ Meter meter = otel.getMeterProvider().get("test.scope");
+ LongCounter counter = meter.counterBuilder("testCounter").build();
+ counter.add(1, Attributes.empty());
+
+ // Verify we see the entity and the metric.
+ assertThat(sdkMeterReader.collectAllMetrics())
+ .anySatisfy(
+ metric ->
+ assertThat(metric)
+ .hasName("testCounter")
+ .hasResourceSatisfying(
+ resource ->
+ resource.hasAttributesSatisfying(
+ attributes -> assertThat(attributes).containsEntry("test.id", 1))));
+
+ // Now update the resource and check the point.
+ otel.getEntityProvider()
+ .attachOrUpdateEntity("test2")
+ .withId(Attributes.builder().put("test2.id", 1).build())
+ .emit();
+ // Verify we see the new entity and the metric.
+ assertThat(sdkMeterReader.collectAllMetrics())
+ .anySatisfy(
+ metric ->
+ assertThat(metric)
+ .hasName("testCounter")
+ .hasResourceSatisfying(
+ resource ->
+ resource.hasAttributesSatisfying(
+ attributes ->
+ assertThat(attributes)
+ .containsEntry("test.id", 1)
+ .containsEntry("test2.id", 1))));
+ }
+}
diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/entities/TestEntityProvider.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/entities/TestEntityProvider.java
new file mode 100644
index 00000000000..6df28a8ff16
--- /dev/null
+++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/entities/TestEntityProvider.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.entities.EntityProvider;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.resources.internal.EntityUtil;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+class TestEntityProvider {
+ @Test
+ void defaults_includeServiceAndSdk() {
+ LatestResourceSupplier resource = new LatestResourceSupplier(200);
+ SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(true).build();
+ provider.onChange(resource);
+
+ assertThat(resource.get().getAttributes())
+ .containsKey("service.name")
+ .containsKey("service.instance.id")
+ .containsKey("telemetry.sdk.language")
+ .containsKey("telemetry.sdk.name")
+ .containsKey("telemetry.sdk.version");
+ assertThat(resource.get().getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.34.0");
+
+ assertThat(EntityUtil.getEntities(resource.get()))
+ .satisfiesExactlyInAnyOrder(
+ e -> assertThat(e.getType()).isEqualTo("service"),
+ e -> assertThat(e.getType()).isEqualTo("telemetry.sdk"));
+ }
+
+ @Test
+ void resource_updatesDescription() {
+ LatestResourceSupplier resource = new LatestResourceSupplier(200);
+ SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build();
+ provider.onChange(resource);
+
+ provider
+ .attachOrUpdateEntity("one")
+ .setSchemaUrl("one")
+ .withId(Attributes.builder().put("one.id", 1).build())
+ .emit();
+
+ provider
+ .attachOrUpdateEntity("one")
+ .setSchemaUrl("one")
+ .withId(Attributes.builder().put("one.id", 1).build())
+ .withDescription(Attributes.builder().put("one.desc", "desc").build())
+ .emit();
+
+ assertThat(resource.get().getAttributes())
+ .hasSize(2)
+ .containsKey("one.id")
+ .containsKey("one.desc");
+ }
+
+ @Test
+ void resource_ignoresNewIds() {
+ LatestResourceSupplier resource = new LatestResourceSupplier(200);
+ SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build();
+ provider.onChange(resource);
+
+ provider
+ .attachOrUpdateEntity("one")
+ .setSchemaUrl("one")
+ .withId(Attributes.builder().put("one.id", 1).build())
+ .emit();
+
+ provider
+ .attachOrUpdateEntity("one")
+ .setSchemaUrl("one")
+ .withId(Attributes.builder().put("one.id", 2).build())
+ .withDescription(Attributes.builder().put("one.desc", "desc").build())
+ .emit();
+
+ assertThat(resource.get().getAttributes()).hasSize(1).containsKey("one.id");
+ }
+
+ @Test
+ void resource_ignoresNewSchemaUrl() {
+ SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build();
+ LatestResourceSupplier resource = new LatestResourceSupplier(200);
+ provider.onChange(resource);
+
+ provider
+ .attachOrUpdateEntity("one")
+ .setSchemaUrl("one")
+ .withId(Attributes.builder().put("one.id", 1).build())
+ .emit();
+
+ provider
+ .attachOrUpdateEntity("one")
+ .setSchemaUrl("two")
+ .withId(Attributes.builder().put("one.id", 1).build())
+ .withDescription(Attributes.builder().put("one.desc", "desc").build())
+ .emit();
+
+ assertThat(resource.get().getAttributes()).hasSize(1).containsKey("one.id");
+ }
+
+ @Test
+ void resource_addsNewEntity() {
+ SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build();
+ LatestResourceSupplier resource = new LatestResourceSupplier(200);
+ provider.onChange(resource);
+
+ provider
+ .attachOrUpdateEntity("one")
+ .setSchemaUrl("one")
+ .withId(Attributes.builder().put("one.id", 1).build())
+ .emit();
+
+ provider
+ .attachOrUpdateEntity("two")
+ .setSchemaUrl("two")
+ .withId(Attributes.builder().put("two.id", 2).build())
+ .emit();
+
+ assertThat(resource.get().getAttributes())
+ .hasSize(2)
+ .containsKey("one.id")
+ .containsKey("two.id");
+ }
+
+ @Test
+ void resource_removesEntity() {
+ SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build();
+ LatestResourceSupplier resource = new LatestResourceSupplier(200);
+ provider.onChange(resource);
+
+ provider
+ .attachOrUpdateEntity("one")
+ .setSchemaUrl("one")
+ .withId(Attributes.builder().put("one.id", 1).build())
+ .emit();
+
+ assertThat(resource.get().getAttributes()).hasSize(1).containsKey("one.id");
+
+ assertThat(provider.removeEntity("one")).isTrue();
+ assertThat(resource.get().getAttributes()).isEmpty();
+ }
+
+ @Test
+ void entityListener_notifiesOnAdd() {
+ SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build();
+
+ EntityListener listener = mock(EntityListener.class);
+ provider.onChange(listener);
+
+ provider
+ .attachOrUpdateEntity("one")
+ .setSchemaUrl("one")
+ .withId(Attributes.builder().put("one.id", 1).build())
+ .emit();
+ ArgumentCaptor entityCapture = ArgumentCaptor.forClass(EntityState.class);
+ ArgumentCaptor resourceCapture = ArgumentCaptor.forClass(Resource.class);
+ verify(listener, times(1)).onEntityState(entityCapture.capture(), resourceCapture.capture());
+ assertThat(entityCapture.getValue().getType()).isEqualTo("one");
+ assertThat(resourceCapture.getValue().getAttributes()).hasSize(1).containsKey("one.id");
+ }
+
+ @Test
+ void entityListener_notifiesOnRemove() {
+ SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build();
+ provider
+ .attachOrUpdateEntity("one")
+ .setSchemaUrl("one")
+ .withId(Attributes.builder().put("one.id", 1).build())
+ .emit();
+ EntityListener listener = mock(EntityListener.class);
+ provider.onChange(listener);
+
+ provider.removeEntity("one");
+ ArgumentCaptor entityCapture = ArgumentCaptor.forClass(EntityState.class);
+ ArgumentCaptor resourceCapture = ArgumentCaptor.forClass(Resource.class);
+ verify(listener, times(1)).onEntityDelete(entityCapture.capture(), resourceCapture.capture());
+ assertThat(entityCapture.getValue().getType()).isEqualTo("one");
+ assertThat(resourceCapture.getValue().getAttributes()).isEmpty();
+ }
+
+ @Test
+ void entityListener_initializesAfterTimeout() throws InterruptedException {
+ // Because we're using same-thread-executor, we know entity provider blocked
+ // until everything started up.
+ // Instead we fork the resource detection.
+ ExecutorService service = Executors.newSingleThreadExecutor();
+ ResourceDetector forever =
+ (EntityProvider provider) -> {
+ // This will never complete.
+ return new CompletableResultCode();
+ };
+ SdkEntityProvider provider =
+ SdkEntityProvider.builder()
+ .setListenerExecutorService(service)
+ .includeDefaults(false)
+ .addDetector(forever)
+ .build();
+ EntityListener listener = mock(EntityListener.class);
+ provider.onChange(listener);
+ // Ensure we haven't seen initialization yet (If this is flaky, remove this)
+ verify(listener, never()).onResourceInit(any());
+
+ // Wait long enough that initialization has happened.
+ Thread.sleep(500);
+ ArgumentCaptor resourceCapture = ArgumentCaptor.forClass(Resource.class);
+ verify(listener, times(1)).onResourceInit(resourceCapture.capture());
+ assertThat(resourceCapture.getValue().getAttributes()).isEmpty();
+ }
+}
diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/entities/TestLatestResourceSupplier.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/entities/TestLatestResourceSupplier.java
new file mode 100644
index 00000000000..d2d5afee418
--- /dev/null
+++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/entities/TestLatestResourceSupplier.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.entities;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
+
+import io.opentelemetry.sdk.resources.Resource;
+import org.junit.jupiter.api.Test;
+
+class TestLatestResourceSupplier {
+
+ @Test
+ void getResource_defaultsAfterTimeout() {
+ LatestResourceSupplier supplier = new LatestResourceSupplier(0);
+ // This will block. We haven't registered our listener, so
+ // we never get an initialize event. We should still get
+ // a default resource.
+ Resource resource = supplier.get();
+ assertThat(resource.getAttributes()).containsKey("service.name");
+ }
+}
diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java
index 2429ae150a2..7057d17c099 100644
--- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java
+++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java
@@ -165,7 +165,7 @@ void create_ModelCustomizer() {
DeclarativeConfigurationCreateTest.class.getClassLoader()));
assertThat(sdk.toString())
.contains(
- "resource=Resource{schemaUrl=null, attributes={"
+ "resource=Resource{schemaUrl=null, rawAttributes={"
+ "color=\"blue\", "
+ "foo=\"bar\", "
+ "service.name=\"unknown_service:java\", "
diff --git a/sdk/all/src/main/java/io/opentelemetry/sdk/IncubatingUtil.java b/sdk/all/src/main/java/io/opentelemetry/sdk/IncubatingUtil.java
new file mode 100644
index 00000000000..0ada6e9f55d
--- /dev/null
+++ b/sdk/all/src/main/java/io/opentelemetry/sdk/IncubatingUtil.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utilities for interacting with {@code io.opentelemetry:opentelemetry-api-incubator}, which is not
+ * guaranteed to be present on the classpath. For all methods, callers MUST first separately
+ * reflectively confirm that the incubator is available on the classpath.
+ */
+final class IncubatingUtil {
+ private IncubatingUtil() {}
+
+ @Nullable
+ static OpenTelemetrySdk createExtendedOpenTelemetrySdk() {
+ return null;
+ }
+}
diff --git a/sdk/all/src/test/java/io/opentelemetry/sdk/OpenTelemetrySdkTest.java b/sdk/all/src/test/java/io/opentelemetry/sdk/OpenTelemetrySdkTest.java
index 8fb5eafdf47..c6a48a0dbad 100644
--- a/sdk/all/src/test/java/io/opentelemetry/sdk/OpenTelemetrySdkTest.java
+++ b/sdk/all/src/test/java/io/opentelemetry/sdk/OpenTelemetrySdkTest.java
@@ -406,34 +406,13 @@ void stringRepresentation() {
.setPropagators(ContextPropagators.create(propagator))
.build();
+ // Test that toString delegates to underlying classes, and make sure their toString is also
+ // nice.
assertThat(sdk.toString())
- .isEqualTo(
- "OpenTelemetrySdk{"
- + "tracerProvider=SdkTracerProvider{"
- + "clock=SystemClock{}, "
- + "idGenerator=RandomIdGenerator{}, "
- + "resource=Resource{schemaUrl=null, attributes={service.name=\"otel-test\"}}, "
- + "spanLimitsSupplier=SpanLimitsValue{maxNumberOfAttributes=128, maxNumberOfEvents=128, maxNumberOfLinks=128, maxNumberOfAttributesPerEvent=128, maxNumberOfAttributesPerLink=128, maxAttributeValueLength=2147483647}, "
- + "sampler=ParentBased{root:AlwaysOnSampler,remoteParentSampled:AlwaysOnSampler,remoteParentNotSampled:AlwaysOffSampler,localParentSampled:AlwaysOnSampler,localParentNotSampled:AlwaysOffSampler}, "
- + "spanProcessor=SimpleSpanProcessor{spanExporter=MultiSpanExporter{spanExporters=[MockSpanExporter{}, MockSpanExporter{}]}, exportUnsampledSpans=false}, "
- + "tracerConfigurator=ScopeConfiguratorImpl{conditions=[]}"
- + "}, "
- + "meterProvider=SdkMeterProvider{"
- + "clock=SystemClock{}, "
- + "resource=Resource{schemaUrl=null, attributes={service.name=\"otel-test\"}}, "
- + "metricReaders=[PeriodicMetricReader{exporter=MockMetricExporter{}, intervalNanos=60000000000}], "
- + "metricProducers=[], "
- + "views=[RegisteredView{instrumentSelector=InstrumentSelector{instrumentName=instrument}, view=View{name=new-instrument, aggregation=DefaultAggregation, attributesProcessor=NoopAttributesProcessor{}, cardinalityLimit=2000}}], "
- + "meterConfigurator=ScopeConfiguratorImpl{conditions=[]}"
- + "}, "
- + "loggerProvider=SdkLoggerProvider{"
- + "clock=SystemClock{}, "
- + "resource=Resource{schemaUrl=null, attributes={service.name=\"otel-test\"}}, "
- + "logLimits=LogLimits{maxNumberOfAttributes=128, maxAttributeValueLength=2147483647}, "
- + "logRecordProcessor=SimpleLogRecordProcessor{logRecordExporter=MultiLogRecordExporter{logRecordExporters=[MockLogRecordExporter{}, MockLogRecordExporter{}]}}, "
- + "loggerConfigurator=ScopeConfiguratorImpl{conditions=[]}"
- + "}, "
- + "propagators=DefaultContextPropagators{textMapPropagator=MockTextMapPropagator{}}"
- + "}");
+ .matches("OpenTelemetrySdk\\{.*}")
+ .matches("OpenTelemetrySdk\\{tracerProvider=SdkTracerProvider\\{.*}.*}")
+ .matches("OpenTelemetrySdk\\{.*, meterProvider=SdkMeterProvider\\{.*}.*}")
+ .matches("OpenTelemetrySdk\\{.*, loggerProvider=SdkLoggerProvider\\{.*}.*}")
+ .matches("OpenTelemetrySdk\\{.*, propagators=DefaultContextPropagators\\{.*}}");
}
}
diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java
index c406ef87a5e..e72e5db90a4 100644
--- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java
+++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java
@@ -9,11 +9,13 @@
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
-import io.opentelemetry.api.internal.StringUtils;
-import io.opentelemetry.api.internal.Utils;
import io.opentelemetry.sdk.common.internal.OtelVersion;
+import io.opentelemetry.sdk.resources.internal.AttributeCheckUtil;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import io.opentelemetry.sdk.resources.internal.EntityUtil;
+import java.util.Collection;
+import java.util.Collections;
import java.util.Objects;
-import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
@@ -24,8 +26,6 @@
@Immutable
@AutoValue
public abstract class Resource {
- private static final Logger logger = Logger.getLogger(Resource.class.getName());
-
private static final AttributeKey SERVICE_NAME = AttributeKey.stringKey("service.name");
private static final AttributeKey TELEMETRY_SDK_LANGUAGE =
AttributeKey.stringKey("telemetry.sdk.language");
@@ -34,13 +34,6 @@ public abstract class Resource {
private static final AttributeKey TELEMETRY_SDK_VERSION =
AttributeKey.stringKey("telemetry.sdk.version");
- private static final int MAX_LENGTH = 255;
- private static final String ERROR_MESSAGE_INVALID_CHARS =
- " should be a ASCII string with a length greater than 0 and not exceed "
- + MAX_LENGTH
- + " characters.";
- private static final String ERROR_MESSAGE_INVALID_VALUE =
- " should be a ASCII string with a length not exceed " + MAX_LENGTH + " characters.";
private static final Resource EMPTY = create(Attributes.empty());
private static final Resource TELEMETRY_SDK;
@@ -91,7 +84,7 @@ public static Resource empty() {
* @return a {@code Resource}.
* @throws NullPointerException if {@code attributes} is null.
* @throws IllegalArgumentException if attribute key or attribute value is not a valid printable
- * ASCII string or exceed {@link #MAX_LENGTH} characters.
+ * ASCII string or exceed {@link AttributeCheckUtil#MAX_LENGTH} characters.
*/
public static Resource create(Attributes attributes) {
return create(attributes, null);
@@ -105,11 +98,27 @@ public static Resource create(Attributes attributes) {
* @return a {@code Resource}.
* @throws NullPointerException if {@code attributes} is null.
* @throws IllegalArgumentException if attribute key or attribute value is not a valid printable
- * ASCII string or exceed {@link #MAX_LENGTH} characters.
+ * ASCII string or exceed {@link AttributeCheckUtil#MAX_LENGTH} characters.
*/
public static Resource create(Attributes attributes, @Nullable String schemaUrl) {
- checkAttributes(Objects.requireNonNull(attributes, "attributes"));
- return new AutoValue_Resource(schemaUrl, attributes);
+ return create(attributes, schemaUrl, Collections.emptyList());
+ }
+
+ /**
+ * Returns a {@link Resource}.
+ *
+ * @param attributes a map of {@link Attributes} that describe the resource.
+ * @param schemaUrl The URL of the OpenTelemetry schema used to create this Resource.
+ * @param entities The set of detected {@link Entity}s that participate in this resource.
+ * @return a {@code Resource}.
+ * @throws NullPointerException if {@code attributes} is null.
+ * @throws IllegalArgumentException if attribute key or attribute value is not a valid printable
+ * ASCII string or exceed {@link AttributeCheckUtil#MAX_LENGTH} characters.
+ */
+ static Resource create(
+ Attributes attributes, @Nullable String schemaUrl, Collection entities) {
+ AttributeCheckUtil.checkAttributes(Objects.requireNonNull(attributes, "attributes"));
+ return new AutoValue_Resource(schemaUrl, attributes, entities);
}
/**
@@ -121,12 +130,38 @@ public static Resource create(Attributes attributes, @Nullable String schemaUrl)
@Nullable
public abstract String getSchemaUrl();
+ /**
+ * Returns a map of attributes that describe the resource, not associated with entites.
+ *
+ * @return a map of attributes.
+ */
+ abstract Attributes getRawAttributes();
+
+ /**
+ * Returns a collectoion of associated entities.
+ *
+ * @return a collection of entities.
+ */
+ abstract Collection getEntities();
+
/**
* Returns a map of attributes that describe the resource.
*
* @return a map of attributes.
*/
- public abstract Attributes getAttributes();
+ // @Memoized - This breaks nullaway.
+ public Attributes getAttributes() {
+ AttributesBuilder result = Attributes.builder();
+ getEntities()
+ .forEach(
+ e -> {
+ result.putAll(e.getId());
+ result.putAll(e.getDescription());
+ });
+ // In merge rules, raw comes last, so we return these last.
+ result.putAll(getRawAttributes());
+ return result.build();
+ }
/**
* Returns the value for a given resource attribute key.
@@ -146,63 +181,7 @@ public T getAttribute(AttributeKey key) {
* @return the newly merged {@code Resource}.
*/
public Resource merge(@Nullable Resource other) {
- if (other == null || other == EMPTY) {
- return this;
- }
-
- AttributesBuilder attrBuilder = Attributes.builder();
- attrBuilder.putAll(this.getAttributes());
- attrBuilder.putAll(other.getAttributes());
-
- if (other.getSchemaUrl() == null) {
- return create(attrBuilder.build(), getSchemaUrl());
- }
- if (getSchemaUrl() == null) {
- return create(attrBuilder.build(), other.getSchemaUrl());
- }
- if (!other.getSchemaUrl().equals(getSchemaUrl())) {
- logger.info(
- "Attempting to merge Resources with different schemaUrls. "
- + "The resulting Resource will have no schemaUrl assigned. Schema 1: "
- + getSchemaUrl()
- + " Schema 2: "
- + other.getSchemaUrl());
- // currently, behavior is undefined if schema URLs don't match. In the future, we may
- // apply schema transformations if possible.
- return create(attrBuilder.build(), null);
- }
- return create(attrBuilder.build(), getSchemaUrl());
- }
-
- private static void checkAttributes(Attributes attributes) {
- attributes.forEach(
- (key, value) -> {
- Utils.checkArgument(
- isValidAndNotEmpty(key), "Attribute key" + ERROR_MESSAGE_INVALID_CHARS);
- Objects.requireNonNull(value, "Attribute value" + ERROR_MESSAGE_INVALID_VALUE);
- });
- }
-
- /**
- * Determines whether the given {@code String} is a valid printable ASCII string with a length not
- * exceed {@link #MAX_LENGTH} characters.
- *
- * @param name the name to be validated.
- * @return whether the name is valid.
- */
- private static boolean isValid(String name) {
- return name.length() <= MAX_LENGTH && StringUtils.isPrintableString(name);
- }
-
- /**
- * Determines whether the given {@code String} is a valid printable ASCII string with a length
- * greater than 0 and not exceed {@link #MAX_LENGTH} characters.
- *
- * @param name the name to be validated.
- * @return whether the name is valid.
- */
- private static boolean isValidAndNotEmpty(AttributeKey> name) {
- return !name.getKey().isEmpty() && isValid(name.getKey());
+ return EntityUtil.merge(this, other);
}
/**
diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java
index 9963eeaf541..f2abb8d8b20 100644
--- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java
+++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java
@@ -8,7 +8,14 @@
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import io.opentelemetry.sdk.resources.internal.EntityUtil;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
import java.util.function.Predicate;
+import java.util.stream.Collectors;
import javax.annotation.Nullable;
/**
@@ -20,6 +27,7 @@
public class ResourceBuilder {
private final AttributesBuilder attributesBuilder = Attributes.builder();
+ private final List entities = new ArrayList<>();
@Nullable private String schemaUrl;
/**
@@ -194,6 +202,32 @@ public ResourceBuilder setSchemaUrl(String schemaUrl) {
/** Create the {@link Resource} from this. */
public Resource build() {
- return Resource.create(attributesBuilder.build(), schemaUrl);
+ // What checks should we do on "real" resource here?
+ // Derive schemaUrl from entitiy, if able.
+ if (schemaUrl == null) {
+ Set entitySchemas =
+ entities.stream().map(Entity::getSchemaUrl).collect(Collectors.toSet());
+ if (entitySchemas.size() == 1) {
+ // Updated Entities use same schema, we can preserve it.
+ schemaUrl = entitySchemas.iterator().next();
+ }
+ }
+
+ // TODO - here we deal with conflicts between entities and raw attributes.
+ // When adding an entity, we remove any raw attributes it may conflict with.
+ this.attributesBuilder.removeIf(key -> EntityUtil.hasAttributeKey(this.entities, key));
+ return Resource.create(attributesBuilder.build(), schemaUrl, entities);
+ }
+
+ /** Appends a new entity on to the end of the list of entities. */
+ ResourceBuilder add(Entity e) {
+ this.entities.add(e);
+ return this;
+ }
+
+ /** Appends a new collection of entities on to the end of the list of entities. */
+ ResourceBuilder addAll(Collection entities) {
+ this.entities.addAll(entities);
+ return this;
}
}
diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/AttributeCheckUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/AttributeCheckUtil.java
new file mode 100644
index 00000000000..966c5b58277
--- /dev/null
+++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/AttributeCheckUtil.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.resources.internal;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.internal.StringUtils;
+import io.opentelemetry.api.internal.Utils;
+import java.util.Objects;
+
+/**
+ * Helpers to check resource attributes.
+ *
+ *
This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ */
+public final class AttributeCheckUtil {
+ private AttributeCheckUtil() {}
+
+ // Note: Max length is actually configurable by specification.
+ private static final int MAX_LENGTH = 255;
+ private static final String ERROR_MESSAGE_INVALID_CHARS =
+ " should be a ASCII string with a length greater than 0 and not exceed "
+ + MAX_LENGTH
+ + " characters.";
+ private static final String ERROR_MESSAGE_INVALID_VALUE =
+ " should be a ASCII string with a length not exceed " + MAX_LENGTH + " characters.";
+
+ /** Determine if the set of attributes if valid for Resource / Entity. */
+ public static void checkAttributes(Attributes attributes) {
+ attributes.forEach(
+ (key, value) -> {
+ Utils.checkArgument(
+ isValidAndNotEmpty(key), "Attribute key" + ERROR_MESSAGE_INVALID_CHARS);
+ Objects.requireNonNull(value, "Attribute value" + ERROR_MESSAGE_INVALID_VALUE);
+ });
+ }
+
+ /**
+ * Determines whether the given {@code String} is a valid printable ASCII string with a length
+ * greater than 0 and not exceed {@link #MAX_LENGTH} characters.
+ *
+ * @param name the name to be validated.
+ * @return whether the name is valid.
+ */
+ public static boolean isValidAndNotEmpty(AttributeKey> name) {
+ return !name.getKey().isEmpty() && isValid(name.getKey());
+ }
+
+ /**
+ * Determines whether the given {@code String} is a valid printable ASCII string with a length not
+ * exceed {@link #MAX_LENGTH} characters.
+ *
+ * @param name the name to be validated.
+ * @return whether the name is valid.
+ */
+ public static boolean isValid(String name) {
+ return name.length() <= MAX_LENGTH && StringUtils.isPrintableString(name);
+ }
+}
diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/Entity.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/Entity.java
new file mode 100644
index 00000000000..40cf63e126b
--- /dev/null
+++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/Entity.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.resources.internal;
+
+import io.opentelemetry.api.common.Attributes;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Entity represents an object of interest associated with produced telemetry: traces, metrics or
+ * logs.
+ *
+ *
For example, telemetry produced using OpenTelemetry SDK is normally associated with a Service
+ * entity. Similarly, OpenTelemetry defines system metrics for a host. The Host is the entity we
+ * want to associate metrics with in this case.
+ *
+ *
Entities may be also associated with produced telemetry indirectly. For example a service that
+ * produces telemetry is also related with a process in which the service runs, so we say that the
+ * Service entity is related to the Process entity. The process normally also runs on a host, so we
+ * say that the Process entity is related to the Host entity.
+ *
+ *
This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ */
+@Immutable
+public interface Entity {
+ /**
+ * Returns the entity type string of this entity. Must not be null.
+ *
+ * @return the entity type.
+ */
+ String getType();
+
+ /**
+ * Returns a map of attributes that identify the entity.
+ *
+ * @return the entity identity.
+ */
+ Attributes getId();
+
+ /**
+ * Returns a map of attributes that describe the entity.
+ *
+ * @return the entity description.
+ */
+ Attributes getDescription();
+
+ /**
+ * Returns the URL of the OpenTelemetry schema used by this resource. May be null if this entity
+ * does not abide by schema conventions (i.e. is custom).
+ *
+ * @return An OpenTelemetry schema URL.
+ */
+ @Nullable
+ String getSchemaUrl();
+
+ /**
+ * Returns a new {@link EntityBuilder} instance populated with the data of this {@link Entity}.
+ */
+ EntityBuilder toBuilder();
+
+ /**
+ * Returns a new {@link EntityBuilder} instance for creating arbitrary {@link Entity}.
+ *
+ * @param entityType the entity type string of this entity.
+ */
+ public static EntityBuilder builder(String entityType) {
+ return SdkEntity.builder(entityType);
+ }
+}
diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityBuilder.java
new file mode 100644
index 00000000000..bb2b21c998b
--- /dev/null
+++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityBuilder.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.resources.internal;
+
+import io.opentelemetry.api.common.Attributes;
+
+/**
+ * A builder of {@link Entity} that allows to add identifying or descriptive {@link Attributes}, as
+ * well as type and schema_url.
+ *
+ *
This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ */
+public interface EntityBuilder {
+ /**
+ * Assign an OpenTelemetry schema URL to the resulting Entity.
+ *
+ * @param schemaUrl The URL of the OpenTelemetry schema being used to create this Entity.
+ * @return this
+ */
+ EntityBuilder setSchemaUrl(String schemaUrl);
+
+ /**
+ * Modify the descriptive attributes of this Entity.
+ *
+ * @param description The attributes that describe the Entity.
+ * @return this
+ */
+ EntityBuilder withDescription(Attributes description);
+
+ /**
+ * Modify the identifying attributes of this Entity.
+ *
+ * @param id The identifying attributes.
+ * @return this
+ */
+ EntityBuilder withId(Attributes id);
+
+ /** Create the {@link Entity} from this. */
+ Entity build();
+}
diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java
new file mode 100644
index 00000000000..d2379f1a4e0
--- /dev/null
+++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.resources.internal;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.resources.ResourceBuilder;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+/**
+ * Helper class for dealing with Entities.
+ *
+ *
This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ */
+public final class EntityUtil {
+ private static final Logger logger = Logger.getLogger(EntityUtil.class.getName());
+
+ private EntityUtil() {}
+
+ /**
+ * Constructs a new {@link Resource} with Entity support.
+ *
+ * @param entities The set of entities the resource needs.
+ * @return A constructed resource.
+ */
+ public static Resource createResource(Collection entities) {
+ return createResourceRaw(
+ Attributes.empty(), EntityUtil.mergeResourceSchemaUrl(entities, null, null), entities);
+ }
+
+ /**
+ * Constructs a new {@link Resource} with Entity support.
+ *
+ * @param attributes The raw attributes for the resource.
+ * @param schemaUrl The schema url for the resource.
+ * @param entities The set of entities the resource needs.
+ * @return A constructed resource.
+ */
+ static Resource createResourceRaw(
+ Attributes attributes, @Nullable String schemaUrl, Collection entities) {
+ try {
+ Method method =
+ Resource.class.getDeclaredMethod(
+ "create", Attributes.class, String.class, Collection.class);
+ if (method != null) {
+ method.setAccessible(true);
+ Object result = method.invoke(null, attributes, schemaUrl, entities);
+ if (result instanceof Resource) {
+ return (Resource) result;
+ }
+ }
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", e);
+ }
+ // Fall back to non-entity behavior?
+ logger.log(Level.WARNING, "Attempting to use entities with unsupported resource");
+ return Resource.empty();
+ }
+
+ /** Appends a new entity on to the end of the list of entities. */
+ public static ResourceBuilder addEntity(ResourceBuilder rb, Entity e) {
+ try {
+ Method method = ResourceBuilder.class.getDeclaredMethod("add", Entity.class);
+ if (method != null) {
+ method.setAccessible(true);
+ method.invoke(rb, e);
+ }
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
+ logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", ex);
+ }
+ return rb;
+ }
+
+ /** Appends a new collection of entities on to the end of the list of entities. */
+ public static ResourceBuilder addAllEntity(ResourceBuilder rb, Collection e) {
+ try {
+ Method method = ResourceBuilder.class.getDeclaredMethod("addAll", Collection.class);
+ if (method != null) {
+ method.setAccessible(true);
+ method.invoke(rb, e);
+ }
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
+ logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", ex);
+ }
+ return rb;
+ }
+
+ /**
+ * Returns a collectoion of associated entities.
+ *
+ * @return a collection of entities.
+ */
+ @SuppressWarnings("unchecked")
+ public static Collection getEntities(Resource r) {
+ try {
+ Method method = Resource.class.getDeclaredMethod("getEntities");
+ if (method != null) {
+ method.setAccessible(true);
+ return (Collection) method.invoke(r);
+ }
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", e);
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Returns a map of attributes that describe the resource, not associated with entites.
+ *
+ * @return a map of attributes.
+ */
+ public static Attributes getRawAttributes(Resource r) {
+ try {
+ Method method = Resource.class.getDeclaredMethod("getRawAttributes");
+ if (method != null) {
+ method.setAccessible(true);
+ return (Attributes) method.invoke(r);
+ }
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", e);
+ }
+ return Attributes.empty();
+ }
+
+ /** Returns true if any entity in the collection has the attribute key, in id or description. */
+ public static boolean hasAttributeKey(Collection entities, AttributeKey key) {
+ return entities.stream()
+ .anyMatch(
+ e -> e.getId().asMap().containsKey(key) || e.getDescription().asMap().containsKey(key));
+ }
+
+ /** Decides on a final SchemaURL for OTLP Resource based on entities chosen. */
+ @Nullable
+ static String mergeResourceSchemaUrl(
+ Collection entities, @Nullable String baseUrl, @Nullable String nextUrl) {
+ // Check if entities all share the same URL.
+ Set entitySchemas =
+ entities.stream().map(Entity::getSchemaUrl).collect(Collectors.toSet());
+ // If we have no entities, we preserve previous schema url behavior.
+ String result = baseUrl;
+ if (entitySchemas.size() == 1) {
+ // Updated Entities use same schema, we can preserve it.
+ result = entitySchemas.iterator().next();
+ } else if (entitySchemas.size() > 1) {
+ // Entities use different schemas, resource must treat this as no schema_url.
+ result = null;
+ }
+
+ // If schema url of merging resource is null, we use our current result.
+ if (nextUrl == null) {
+ return result;
+ }
+ // When there are no entities, we use old schema url merge behavior
+ if (result == null && entities.isEmpty()) {
+ return nextUrl;
+ }
+ if (!nextUrl.equals(result)) {
+ logger.info(
+ "Attempting to merge Resources with different schemaUrls. "
+ + "The resulting Resource will have no schemaUrl assigned. Schema 1: "
+ + baseUrl
+ + " Schema 2: "
+ + nextUrl);
+ return null;
+ }
+ return result;
+ }
+
+ /**
+ * Merges "loose" attributes on resource, removing those which conflict with the set of entities.
+ *
+ * @param base loose attributes from base resource
+ * @param additional additional attributes to add to the resource.
+ * @param entities the set of entites on the resource.
+ * @return the new set of raw attributes for Resource and the set of conflicting entities that
+ * MUST NOT be reported on OTLP resource.
+ */
+ @SuppressWarnings("unchecked")
+ static final RawAttributeMergeResult mergeRawAttributes(
+ Attributes base, Attributes additional, Collection entities) {
+ AttributesBuilder result = base.toBuilder();
+ // We know attribute conflicts were handled perviously on the resource, so
+ // This needs to account for entity merge of new entities, and remove raw
+ // attributes that would have been removed with new entities.
+ result.removeIf(key -> hasAttributeKey(entities, key));
+ // For every "raw" attribute on the other resource, we merge into the
+ // resource, but check for entity conflicts from previous entities.
+ ArrayList conflicts = new ArrayList<>();
+ if (!additional.isEmpty()) {
+ additional.forEach(
+ (key, value) -> {
+ for (Entity e : entities) {
+ if (e.getId().get(key) != null || e.getDescription().get(key) != null) {
+ // Remove the entity and push all attributes as raw,
+ // we have an override.
+ conflicts.add(e);
+ result.putAll(e.getId()).putAll(e.getDescription());
+ }
+ }
+ result.put((AttributeKey