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/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/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java
new file mode 100644
index 00000000000..fc4e08c6f6c
--- /dev/null
+++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.autoconfigure;
+
+import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper;
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.extension.incubator.resources.EntityDetector;
+import io.opentelemetry.sdk.extension.incubator.resources.internal.ExtendedEntityUtil;
+import io.opentelemetry.sdk.resources.Resource;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+final class IncubatingEntityUtil {
+
+ private IncubatingEntityUtil() {}
+
+ @Nullable
+ static Resource configureEntityResource(
+ ConfigProperties config,
+ SpiHelper spiHelper,
+ Set enabledProviders,
+ Set disabledProviders) {
+
+ List detectors = new ArrayList<>();
+ for (EntityDetector detector : spiHelper.loadOrdered(EntityDetector.class)) {
+ String fqcn = detector.getClass().getName();
+ String shortName = detector.getName();
+ if (!enabledProviders.isEmpty()
+ && !enabledProviders.contains(fqcn)
+ && !enabledProviders.contains(shortName)) {
+ continue;
+ }
+ if (disabledProviders.contains(fqcn) || disabledProviders.contains(shortName)) {
+ continue;
+ }
+ detectors.add(detector);
+ }
+
+ if (detectors.isEmpty()) {
+ return null;
+ }
+
+ return ExtendedEntityUtil.runDetection(detectors, config);
+ }
+}
diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java
index 9f1a1c4d03c..840af3b9807 100644
--- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java
+++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java
@@ -26,6 +26,22 @@
*/
public final class ResourceConfiguration {
+ private static final boolean ENTITY_INCUBATOR_AVAILABLE;
+
+ static {
+ boolean incubatorAvailable = false;
+ try {
+ Class.forName(
+ "io.opentelemetry.sdk.extension.incubator.resources.EntityDetector",
+ false,
+ ResourceConfiguration.class.getClassLoader());
+ incubatorAvailable = true;
+ } catch (ClassNotFoundException e) {
+ // Not available
+ }
+ ENTITY_INCUBATOR_AVAILABLE = incubatorAvailable;
+ }
+
// Visible for testing
static final String DISABLED_ATTRIBUTE_KEYS = "otel.resource.disabled.keys";
static final String ENABLED_RESOURCE_PROVIDERS = "otel.java.enabled.resource.providers";
@@ -66,23 +82,35 @@ static Resource configureResource(
Set enabledProviders = new HashSet<>(config.getList(ENABLED_RESOURCE_PROVIDERS));
Set disabledProviders = new HashSet<>(config.getList(DISABLED_RESOURCE_PROVIDERS));
- for (ResourceProvider resourceProvider : spiHelper.loadOrdered(ResourceProvider.class)) {
- if (!enabledProviders.isEmpty()
- && !enabledProviders.contains(resourceProvider.getClass().getName())) {
- continue;
- }
- if (disabledProviders.contains(resourceProvider.getClass().getName())) {
- continue;
+ // If Entity experiment is enabled, we use a new flow to instantiate resources.
+ boolean entitiesEnabled = config.getBoolean("otel.experimental.entities.enabled", false);
+ if (entitiesEnabled && ENTITY_INCUBATOR_AVAILABLE) {
+ Resource entityResource =
+ IncubatingEntityUtil.configureEntityResource(
+ config, spiHelper, enabledProviders, disabledProviders);
+ if (entityResource != null) {
+ result = entityResource;
}
- if (resourceProvider instanceof ConditionalResourceProvider
- && !((ConditionalResourceProvider) resourceProvider).shouldApply(config, result)) {
- continue;
+ } else {
+
+ for (ResourceProvider resourceProvider : spiHelper.loadOrdered(ResourceProvider.class)) {
+ if (!enabledProviders.isEmpty()
+ && !enabledProviders.contains(resourceProvider.getClass().getName())) {
+ continue;
+ }
+ if (disabledProviders.contains(resourceProvider.getClass().getName())) {
+ continue;
+ }
+ if (resourceProvider instanceof ConditionalResourceProvider
+ && !((ConditionalResourceProvider) resourceProvider).shouldApply(config, result)) {
+ continue;
+ }
+ result = result.merge(resourceProvider.createResource(config));
}
- result = result.merge(resourceProvider.createResource(config));
- }
-
- result = filterAttributes(result, config);
+ // TODO(jsuereth): Should filter attributes be used with entities?
+ result = filterAttributes(result, config);
+ }
return resourceCustomizer.apply(result, config);
}
diff --git a/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java b/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java
index 64d3b68ef15..2caffc05dea 100644
--- a/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java
+++ b/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java
@@ -18,6 +18,7 @@
import io.github.netmikey.logunit.api.LogCapturer;
import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.incubator.ExtendedOpenTelemetry;
import io.opentelemetry.api.incubator.config.ConfigProvider;
import io.opentelemetry.api.incubator.config.InstrumentationConfigUtil;
@@ -34,6 +35,8 @@
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import io.opentelemetry.sdk.resources.internal.EntityUtil;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import java.io.IOException;
@@ -41,7 +44,10 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -243,4 +249,53 @@ void configFile_ConfigProvider() {
.getScalarList("request_captured_headers", String.class))
.isEqualTo(Arrays.asList("Content-Type", "Accept"));
}
+
+ @Test
+ void entitiesEnabled() {
+ ConfigProperties config =
+ DefaultConfigProperties.createFromMap(
+ Collections.singletonMap("otel.experimental.entities.enabled", "true"));
+
+ AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk =
+ AutoConfiguredOpenTelemetrySdk.builder().setConfig(config).build();
+ OpenTelemetrySdk openTelemetrySdk = autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk();
+ cleanup.addCloseable(openTelemetrySdk);
+
+ Resource resource = autoConfiguredOpenTelemetrySdk.getResource();
+
+ Collection entities = EntityUtil.getEntities(resource);
+ assertThat(entities)
+ .anyMatch(
+ e ->
+ e.getType().equals("telemetry.sdk")
+ && "opentelemetry"
+ .equals(e.getId().get(AttributeKey.stringKey("telemetry.sdk.name")))
+ && "java"
+ .equals(e.getId().get(AttributeKey.stringKey("telemetry.sdk.language"))));
+ }
+
+ @Test
+ void entitiesEnabled_WithEnabledProviders() {
+ Map props = new HashMap<>();
+ props.put("otel.experimental.entities.enabled", "true");
+ props.put("otel.service.name", "my-filtered-service");
+ props.put("otel.java.enabled.resource.providers", "service");
+ ConfigProperties config = DefaultConfigProperties.createFromMap(props);
+
+ AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk =
+ AutoConfiguredOpenTelemetrySdk.builder().setConfig(config).build();
+ OpenTelemetrySdk openTelemetrySdk = autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk();
+ cleanup.addCloseable(openTelemetrySdk);
+
+ Resource resource = autoConfiguredOpenTelemetrySdk.getResource();
+
+ Collection entities = EntityUtil.getEntities(resource);
+ assertThat(entities)
+ .anyMatch(
+ e ->
+ e.getType().equals("service")
+ && "my-filtered-service"
+ .equals(e.getId().get(AttributeKey.stringKey("service.name"))));
+ assertThat(entities).noneMatch(e -> e.getType().equals("telemetry.sdk"));
+ }
}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigContext.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigContext.java
index f6136a240f4..b246f3d1812 100644
--- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigContext.java
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigContext.java
@@ -131,6 +131,18 @@ private InternalTelemetryVersion getInternalTelemetryVersion() {
}
}
+ boolean isEntitiesEnabled() {
+ if (configProvider == null) {
+ return false;
+ }
+ // TODO - Check with Jack if this is the right thing to do for config flags,
+ // of if we want to put this in the experimental resource config space.
+ return Boolean.TRUE.equals(
+ configProvider
+ .getInstrumentationConfig("otel_sdk")
+ .getBoolean("experimental_entities_enabled"));
+ }
+
/**
* Find a registered {@link ComponentProvider} with {@link ComponentProvider#getType()} matching
* {@code type}, {@link ComponentProvider#getName()} matching {@code name}, and call {@link
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ResourceFactory.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ResourceFactory.java
index 1a46ce5af8a..5c7af1cbe46 100644
--- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ResourceFactory.java
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ResourceFactory.java
@@ -15,8 +15,11 @@
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ExperimentalResourceDetectorModel;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.IncludeExcludeModel;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ResourceModel;
+import io.opentelemetry.sdk.extension.incubator.resources.EntityDetector;
+import io.opentelemetry.sdk.extension.incubator.resources.internal.ExtendedEntityUtil;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.resources.ResourceBuilder;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
@@ -37,27 +40,54 @@ public Resource create(ResourceModel model, DeclarativeConfigContext context) {
ExperimentalResourceDetectionModel detectionModel = model.getDetectionDevelopment();
if (detectionModel != null) {
- ResourceBuilder detectedResourceBuilder = Resource.builder();
-
- List detectorModels = detectionModel.getDetectors();
- if (detectorModels != null) {
- for (ExperimentalResourceDetectorModel detectorModel : detectorModels) {
- detectedResourceBuilder.putAll(
- ResourceDetectorFactory.getInstance().create(detectorModel, context));
+ if (context.isEntitiesEnabled()) {
+ List detectorModels = detectionModel.getDetectors();
+ if (detectorModels != null) {
+ List detectors = new ArrayList<>();
+ for (ExperimentalResourceDetectorModel detectorModel : detectorModels) {
+ ConfigKeyValue detectorKeyValue =
+ FileConfigUtil.validateSingleKeyValue(context, detectorModel, "resource detector");
+ String detectorName = detectorKeyValue.getKey();
+
+ for (EntityDetector detector : context.load(EntityDetector.class)) {
+ if (detector.getName().equals(detectorName)
+ || detector.getClass().getName().equals(detectorName)) {
+ detectors.add(detector);
+ }
+ }
+ }
+ if (!detectors.isEmpty()) {
+ Resource detectedEntityResource =
+ ExtendedEntityUtil.runDetection(
+ detectors, DefaultConfigProperties.createFromMap(Collections.emptyMap()));
+ builder = detectedEntityResource.toBuilder();
+ }
+ }
+ } else {
+ ResourceBuilder detectedResourceBuilder = Resource.builder();
+
+ List detectorModels = detectionModel.getDetectors();
+ if (detectorModels != null) {
+ for (ExperimentalResourceDetectorModel detectorModel : detectorModels) {
+ detectedResourceBuilder.putAll(
+ ResourceDetectorFactory.getInstance().create(detectorModel, context));
+ }
}
- }
- IncludeExcludeModel attributesIncludeExcludeModel = detectionModel.getAttributes();
- Predicate detectorAttributeFilter =
- attributesIncludeExcludeModel == null
- ? ResourceFactory::matchAll
- : IncludeExcludeFactory.getInstance().create(attributesIncludeExcludeModel, context);
- Attributes filteredDetectedAttributes =
- detectedResourceBuilder.build().getAttributes().toBuilder()
- .removeIf(attributeKey -> !detectorAttributeFilter.test(attributeKey.getKey()))
- .build();
-
- builder.putAll(filteredDetectedAttributes);
+ IncludeExcludeModel attributesIncludeExcludeModel = detectionModel.getAttributes();
+ Predicate detectorAttributeFilter =
+ attributesIncludeExcludeModel == null
+ ? ResourceFactory::matchAll
+ : IncludeExcludeFactory.getInstance()
+ .create(attributesIncludeExcludeModel, context);
+
+ Attributes filteredDetectedAttributes =
+ detectedResourceBuilder.build().getAttributes().toBuilder()
+ .removeIf(attributeKey -> !detectorAttributeFilter.test(attributeKey.getKey()))
+ .build();
+
+ builder.putAll(filteredDetectedAttributes);
+ }
}
String attributeList = model.getAttributesList();
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/Entity.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/Entity.java
new file mode 100644
index 00000000000..b5468e14c7a
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/Entity.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.resources;
+
+import io.opentelemetry.sdk.extension.incubator.resources.internal.SdkEntityBuilder;
+
+/** An instance of an Entity. */
+public interface Entity {
+ /** Constructs a new builder for creating Entities. */
+ static EntityBuilder builder(String entityType) {
+ return new SdkEntityBuilder(entityType);
+ }
+
+ /** Converts this entity to a builder. */
+ EntityBuilder toBuilder();
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityBuilder.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityBuilder.java
new file mode 100644
index 00000000000..fc95ec47210
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityBuilder.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.resources;
+
+import io.opentelemetry.api.common.Attributes;
+
+/** A builder of {@link Entity}. */
+public interface EntityBuilder {
+
+ /** Sets the schema_url of the Entity. */
+ EntityBuilder setSchemaUrl(String schemaUrl);
+
+ /** Sets the identity of the Entity. */
+ EntityBuilder setIdentity(Attributes identity);
+
+ /** Sets the description of the Entity. */
+ EntityBuilder setDescription(Attributes description);
+
+ /** Builds an entity. */
+ Entity build();
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java
new file mode 100644
index 00000000000..d2558a64468
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.resources;
+
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.autoconfigure.spi.Ordered;
+import io.opentelemetry.sdk.resources.Resource;
+import java.util.Collection;
+
+/**
+ * A service provider interface (SPI) for providing a collection of {@link Entity} that are merged
+ * into the {@link Resource#getDefault() default resource}.
+ */
+public interface EntityDetector extends Ordered {
+ /**
+ * Detects entities based on the configuration.
+ *
+ * @param config the configuration to use for detection
+ */
+ Collection detect(ConfigProperties config);
+
+ /**
+ * Returns the name of the detector (e.g., "service", "env") or the fully qualified class name.
+ * Used for configuration filtering by-name.
+ */
+ default String getName() {
+ return getClass().getName();
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java
new file mode 100644
index 00000000000..b866eb2434c
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.resources;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/*
+ * An EntityDetector that parses the OTEL_ENTITIES environment variable.
+ *
+ * See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/entities/entity-propagation.md
+ * for more information about the OTEL_ENTITIES environment variable.
+ */
+public class EnvEntityDetector implements EntityDetector {
+
+ private static final Logger logger = Logger.getLogger(EnvEntityDetector.class.getName());
+ private static final String PROPERTY_KEY = "otel.entities";
+
+ @Override
+ public String getName() {
+ return "env";
+ }
+
+ @Override
+ public Collection detect(ConfigProperties config) {
+ String entitiesStr = config.getString(PROPERTY_KEY);
+ if (entitiesStr == null || entitiesStr.isEmpty()) {
+ return new ArrayList<>();
+ }
+
+ return new EntityParser(entitiesStr).parse();
+ }
+
+ /**
+ * Segment class represents a start/stop endpoint within a source String.
+ *
+ *
A segment can be used to extract a substring from the source string *without interning* the
+ * string into the JDK's string tables. This can dramatically reduce allocations when parsing.
+ * Segment is intended to provide a similar interface to using {@code substring} on {@code
+ * String}.
+ *
+ *
Additionally, a Segment can be denoted as URL-encoded (e.g. using '%20' to denote a
+ * character.) In this case, the segment will be decoded when extracting its String value.
+ */
+ private static final class Segment {
+ private final String source;
+ private int start;
+ private int end;
+ private boolean needsDecoding;
+
+ Segment(String source) {
+ this.source = source;
+ reset(0);
+ }
+
+ /** Reset the segment for the next use, starting from the given start index. */
+ void reset(int start) {
+ this.start = start;
+ this.end = start;
+ this.needsDecoding = false;
+ }
+
+ /** Update the end of the segment (non-inclusive). */
+ void markEnd(int end) {
+ this.end = end;
+ }
+
+ /**
+ * Denotes that the segment is URL encoded, and should be decoded when calling {@code
+ * getValue()}.
+ */
+ void markNeedsDecoding() {
+ this.needsDecoding = true;
+ }
+
+ /** Return true if the segment is empty. */
+ boolean isEmpty() {
+ return start >= end;
+ }
+
+ /**
+ * Returns the string represented by the bounds of the segment *and* decodes it if {@code
+ * markNeedsDecoding} has been called.
+ *
+ *
Note: This will trim whitespace from the segment before returning it.
+ */
+ String getValue() {
+ if (isEmpty()) {
+ return "";
+ }
+ // TODO - avoid using substring and then triming to avoid interning more than
+ // one string.
+ String substring = source.substring(start, end).trim();
+ return needsDecoding ? decode(substring) : substring;
+ }
+
+ // Percent decoding logic moved here
+ private static String decode(String value) {
+ if (value.indexOf('%') < 0) {
+ return value;
+ }
+
+ int n = value.length();
+ byte[] bytes = new byte[n];
+ int pos = 0;
+
+ for (int i = 0; i < n; i++) {
+ char c = value.charAt(i);
+ if (c == '%' && i + 2 < n) {
+ int d1 = Character.digit(value.charAt(i + 1), 16);
+ int d2 = Character.digit(value.charAt(i + 2), 16);
+ if (d1 != -1 && d2 != -1) {
+ bytes[pos++] = (byte) ((d1 << 4) + d2);
+ i += 2;
+ continue;
+ }
+ }
+ bytes[pos++] = (byte) c;
+ }
+ return new String(bytes, 0, pos, StandardCharsets.UTF_8);
+ }
+ }
+
+ // State machine parser
+ private static final class EntityParser {
+ /**
+ * The current state of parsing.
+ *
+ *
The format is TYPE{KEY1=VAL1,KEY2=VAL2}[ATTR1=VAL1,ATTR2=VAL2]@SCHEMA_URL;
+ *
+ *
The parser state machine transitions between the following states: - TYPE: Parsing an
+ * entity type - ID_KEY: Parsing a "key" of an identity attribute - ID_VAL: Parsing a "value" of
+ * an identity attribute - DESC_KEY: Parsing a "key" of a description attribute - DESC_VAL:
+ * Parsing a "value" of a description attribute - SCHEMA_URL: Parsing the schema URL of a
+ * specific entity - SKIP_TO_NEXT: Skip to the next entity
+ */
+ private enum State {
+ TYPE,
+ ID_KEY,
+ ID_VAL,
+ DESC_KEY,
+ DESC_VAL,
+ SCHEMA_URL,
+ SKIP_TO_NEXT
+ // TODO - do we need specific states to represent "TYPE_COMPLETE",
+ // "ID_COMPLETE", "DESC_COMPLETE"?
+ }
+
+ /** The input entity string. */
+ private final String input;
+
+ /** The current state of parsing. (i.e. where we are in the grammar) */
+ private State state = State.TYPE;
+
+ /** The segment of the input string that we are currently parsing. */
+ private final Segment currentSegment;
+
+ /** The list of entities we've parsed. */
+ private final List entities = new ArrayList<>();
+
+ // Temporary state for building an entity.
+
+ /** The parsed entity type. */
+ @Nullable private String currentType;
+
+ /** Parsed attributes denoting the entity identity. */
+ private Attributes currentIdAttrs = Attributes.empty();
+
+ /** Parsed attributes denoting the entity description. */
+ private Attributes currentDescAttrs = Attributes.empty();
+
+ /** Parsed schema URL for the entity. */
+ @Nullable private String currentSchemaUrl;
+
+ /** A temporary builder we use when parsing key-value pairs for identity or description. */
+ @Nullable private AttributesBuilder currentBuilder;
+
+ /** The current key of a key-value pair that we are parsing. */
+ @Nullable private String currentKey;
+
+ EntityParser(String input) {
+ this.input = input;
+ this.currentSegment = new Segment(input);
+ }
+
+ /**
+ * Parses the input string and returns a list of entities.
+ *
+ * @return the list of entities parsed from the input string.
+ */
+ List parse() {
+ int n = input.length();
+ for (int i = 0; i < n; i++) {
+ char c = input.charAt(i);
+
+ // We finished the previous entity, or hit a syntax error.
+ // Skip to the next entity and try to parse it.
+ if (state == State.SKIP_TO_NEXT) {
+ if (c == ';') {
+ resetEntityState(i + 1);
+ state = State.TYPE;
+ }
+ continue;
+ }
+
+ switch (c) {
+ case '{':
+ // Finish writing entity type, start identity parsing.
+ if (state == State.TYPE) {
+ currentSegment.markEnd(i);
+ currentType = currentSegment.getValue();
+ if (currentType == null || currentType.isEmpty()) {
+ logger.log(Level.WARNING, "Malformed entity definition (empty type): " + input);
+ state = State.SKIP_TO_NEXT;
+ } else {
+ state = State.ID_KEY;
+ currentSegment.reset(i + 1);
+ currentBuilder = Attributes.builder();
+ }
+ }
+ break;
+ case '}':
+ // End identity parsing.
+ if (state == State.ID_VAL || state == State.ID_KEY) {
+ currentSegment.markEnd(i);
+ if (state == State.ID_VAL) {
+ putAttr();
+ }
+ if (currentBuilder != null) {
+ currentIdAttrs = currentBuilder.build();
+ }
+ if (currentIdAttrs.isEmpty()) {
+ logger.log(
+ Level.WARNING,
+ "Malformed entity definition (missing identifying attributes): " + input);
+ state = State.SKIP_TO_NEXT;
+ } else {
+ state = State.TYPE; // Default next state, might change if [ or @ follows
+ currentSegment.reset(i + 1);
+ }
+ }
+ break;
+ case '[':
+ // We finished identity, we're moving to parse description.
+ if (state == State.TYPE) {
+ // After } we are in TYPE state again but expecting [ or @ or ;
+ // TODO - Should we create new state to denote "ID_COMPLETE" for this?
+ state = State.DESC_KEY;
+ currentSegment.reset(i + 1);
+ currentBuilder = Attributes.builder();
+ }
+ break;
+ case ']':
+ // We finished description, update attributes for description and move
+ // back to TYPE state.
+ // TODO - should we create a new state to denote "DESC_COMPLETE"?
+ // Since DESC is optional, we would would transition to the same state as
+ // ID_COMPLETE, but
+ // not allowing DESC to show up again.
+ if (state == State.DESC_VAL || state == State.DESC_KEY) {
+ currentSegment.markEnd(i);
+ if (state == State.DESC_VAL) {
+ putAttr();
+ }
+ if (currentBuilder != null) {
+ currentDescAttrs = currentBuilder.build();
+ }
+ state = State.TYPE;
+ currentSegment.reset(i + 1);
+ }
+ break;
+ case '=':
+ // Finish our "key" parsing and start looking for a value.
+ if (state == State.ID_KEY || state == State.DESC_KEY) {
+ currentSegment.markEnd(i);
+ currentKey = currentSegment.getValue();
+ if (currentKey == null || currentKey.isEmpty()) {
+ logger.log(Level.WARNING, "Malformed key-value pair (empty key): " + input);
+ state = State.SKIP_TO_NEXT;
+ } else {
+ state = (state == State.ID_KEY) ? State.ID_VAL : State.DESC_VAL;
+ currentSegment.reset(i + 1);
+ }
+ }
+ break;
+ case ',':
+ // Finish our "value" parsing and start looking for the next key-value.
+ if (state == State.ID_VAL || state == State.DESC_VAL) {
+ currentSegment.markEnd(i);
+ putAttr();
+ state = (state == State.ID_VAL) ? State.ID_KEY : State.DESC_KEY;
+ currentSegment.reset(i + 1);
+ }
+ break;
+ case '@':
+ // Start looking for schema url
+ if (state == State.TYPE) { // After } or ] we are in TYPE state
+ state = State.SCHEMA_URL;
+ currentSegment.reset(i + 1);
+ }
+ break;
+ case ';':
+ // Finish up the current entity, and get ready to parse the next.
+ if (state == State.TYPE || state == State.SCHEMA_URL) {
+ if (state == State.SCHEMA_URL) {
+ currentSegment.markEnd(i);
+ currentSchemaUrl = currentSegment.getValue();
+ }
+ buildAndAddEntity();
+ resetEntityState(i + 1);
+ state = State.TYPE;
+ } else if (state == State.ID_KEY
+ || state == State.ID_VAL
+ || state == State.DESC_KEY
+ || state == State.DESC_VAL) {
+ logger.log(Level.WARNING, "Malformed entity definition (unexpected ';'): " + input);
+ resetEntityState(i + 1);
+ state = State.TYPE;
+ }
+ break;
+ case '%':
+ // Found an escape character, mark the segment as needing decoding, which
+ // requires special handling.
+ currentSegment.markNeedsDecoding();
+ break;
+ default:
+ // Keep scanning
+ break;
+ }
+ }
+
+ // Handle end of string
+ if (state == State.TYPE || state == State.SCHEMA_URL) {
+ if (state == State.SCHEMA_URL) {
+ currentSegment.markEnd(input.length());
+ currentSchemaUrl = currentSegment.getValue();
+ }
+ buildAndAddEntity();
+ }
+
+ return entities;
+ }
+
+ /** Adds the current attribute key-value pair into the current attribute builder. */
+ private void putAttr() {
+ String val = currentSegment.getValue();
+ if (currentKey != null && !currentKey.isEmpty() && currentBuilder != null) {
+ currentBuilder.put(currentKey, val);
+ }
+ }
+
+ /** Finishes building the current entity and adds it to the parsed list. */
+ private void buildAndAddEntity() {
+ if (currentType != null && !currentType.isEmpty() && !currentIdAttrs.isEmpty()) {
+ EntityBuilder builder = Entity.builder(currentType).setIdentity(currentIdAttrs);
+ if (!currentDescAttrs.isEmpty()) {
+ builder.setDescription(currentDescAttrs);
+ }
+ if (currentSchemaUrl != null && !currentSchemaUrl.isEmpty()) {
+ builder.setSchemaUrl(currentSchemaUrl);
+ }
+ entities.add(builder.build());
+ }
+ }
+
+ /**
+ * Resets the state of the entity parser.
+ *
+ * @param nextStart the start index of the next entity (e.g. after the `;`).
+ */
+ private void resetEntityState(int nextStart) {
+ currentType = null;
+ currentIdAttrs = Attributes.empty();
+ currentDescAttrs = Attributes.empty();
+ currentSchemaUrl = null;
+ currentBuilder = null;
+ currentKey = null;
+ currentSegment.reset(nextStart);
+ }
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java
new file mode 100644
index 00000000000..77494e23586
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.resources;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.UUID;
+
+/** Detects `service` and `service.instance` entities. */
+public class ServiceEntityDetector implements EntityDetector {
+ // TODO - Pull this in from semconv.
+ private static final String SCHEMA_URL = "https://opentelemetry.io/schemas/1.40.0";
+ private static final String SERVICE_TYPE = "service";
+ private static final String SERVICE_INSTANCE_TYPE = "service.instance";
+
+ private static final AttributeKey SERVICE_NAME = AttributeKey.stringKey("service.name");
+ public static final AttributeKey SERVICE_INSTANCE_ID =
+ AttributeKey.stringKey("service.instance.id");
+
+ // multiple calls to this detector provider should return the same value
+ private static final String RANDOM = UUID.randomUUID().toString();
+
+ @Override
+ public String getName() {
+ return "service";
+ }
+
+ @Override
+ public Collection detect(ConfigProperties config) {
+ String serviceName = config.getString("otel.service.name");
+
+ return Arrays.asList(
+ Entity.builder(SERVICE_TYPE)
+ .setIdentity(Attributes.builder().put(SERVICE_NAME, serviceName).build())
+ // TODO: Add other service descriptive attributes.
+ .setSchemaUrl(SCHEMA_URL)
+ .build(),
+ Entity.builder(SERVICE_INSTANCE_TYPE)
+ .setIdentity(
+ Attributes.builder()
+ // TODO: pull from env variable if needed.
+ .put(SERVICE_INSTANCE_ID, RANDOM)
+ .build())
+ .setSchemaUrl(SCHEMA_URL)
+ .build());
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java
new file mode 100644
index 00000000000..d1d7a3b5580
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.resources;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.common.internal.OtelVersion;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * Detection for {@code telemetry.sdk} entity.
+ *
+ *
See: teleemtry.sdk entity
+ */
+public final class TelemetrySdkEntityDetector implements EntityDetector {
+ private static final String SCHEMA_URL = "https://opentelemetry.io/schemas/1.40.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 String getName() {
+ return "telemetry.sdk";
+ }
+
+ @Override
+ public Collection detect(ConfigProperties config) {
+ return Collections.singletonList(
+ Entity.builder(ENTITY_TYPE)
+ .setSchemaUrl(SCHEMA_URL)
+ .setIdentity(
+ Attributes.builder()
+ .put(TELEMETRY_SDK_NAME, "opentelemetry")
+ .put(TELEMETRY_SDK_LANGUAGE, "java")
+ .build())
+ .setDescription(
+ Attributes.builder().put(TELEMETRY_SDK_VERSION, OtelVersion.VERSION).build())
+ .build());
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java
new file mode 100644
index 00000000000..d3ae43da500
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.resources.internal;
+
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.extension.incubator.resources.Entity;
+import io.opentelemetry.sdk.extension.incubator.resources.EntityDetector;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.resources.ResourceBuilder;
+import io.opentelemetry.sdk.resources.internal.EntityBuilder;
+import io.opentelemetry.sdk.resources.internal.EntityUtil;
+import java.util.Collection;
+
+/**
+ * This class is internal and experimental. Its APIs are unstable and can change at any time. Its
+ * APIs (or a version of them) may be promoted to the public stable API in the future, but no
+ * guarantees are made.
+ */
+public final class ExtendedEntityUtil {
+ private ExtendedEntityUtil() {}
+
+ /** Convert between the incubator API entity and the internal-implementation SDK entity. */
+ static io.opentelemetry.sdk.resources.internal.Entity convertEntity(Entity entity) {
+ SdkEntity api = (SdkEntity) entity;
+ EntityBuilder builder = io.opentelemetry.sdk.resources.internal.Entity.builder(api.getType());
+ if (api.getSchemaUrl() != null) {
+ builder.setSchemaUrl(api.getSchemaUrl());
+ }
+ builder.withId(api.getIdentity());
+ builder.withDescription(api.getDescription());
+ return builder.build();
+ }
+
+ /** Runs a set of EntityDetectors (in priority order) and merges the results into a Resource. */
+ public static Resource runDetection(
+ Collection detectors, ConfigProperties config) {
+ ResourceBuilder builder = Resource.builder();
+ for (EntityDetector detector : detectors) {
+ for (Entity entity : detector.detect(config)) {
+ if (entity != null) {
+ EntityUtil.addEntity(builder, convertEntity(entity));
+ }
+ }
+ }
+ return builder.build();
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntity.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntity.java
new file mode 100644
index 00000000000..bf9d7cb36a6
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntity.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.resources.internal;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.extension.incubator.resources.Entity;
+import io.opentelemetry.sdk.extension.incubator.resources.EntityBuilder;
+import javax.annotation.Nullable;
+
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
+public final class SdkEntity implements Entity {
+
+ private final String entityType;
+ @Nullable private final String schemaUrl;
+ private final Attributes identity;
+ private final Attributes description;
+
+ SdkEntity(
+ String entityType, @Nullable String schemaUrl, Attributes identity, Attributes description) {
+ this.entityType = entityType;
+ this.schemaUrl = schemaUrl;
+ this.identity = identity;
+ this.description = description;
+ }
+
+ /**
+ * Returns the entity type string of this entity. Must not be null.
+ *
+ * @return the entity type.
+ */
+ public String getType() {
+ return entityType;
+ }
+
+ /**
+ * Returns a map of attributes that identify the entity.
+ *
+ * @return the entity identity.
+ */
+ public Attributes getIdentity() {
+ return this.identity;
+ }
+
+ /**
+ * Returns a map of attributes that describe the entity.
+ *
+ * @return the entity description.
+ */
+ public Attributes getDescription() {
+ return this.description;
+ }
+
+ /**
+ * 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
+ public String getSchemaUrl() {
+ return this.schemaUrl;
+ }
+
+ @Override
+ public EntityBuilder toBuilder() {
+ return new SdkEntityBuilder(this);
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntityBuilder.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntityBuilder.java
new file mode 100644
index 00000000000..35baf12cfe8
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntityBuilder.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.resources.internal;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.extension.incubator.resources.Entity;
+import io.opentelemetry.sdk.extension.incubator.resources.EntityBuilder;
+import javax.annotation.Nullable;
+
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
+public final class SdkEntityBuilder implements EntityBuilder {
+ private final String entityType;
+ @Nullable private String schemaUrl;
+ private Attributes identity;
+ private Attributes description;
+
+ public SdkEntityBuilder(String entityType) {
+ this.entityType = entityType;
+ this.identity = Attributes.empty();
+ this.description = Attributes.empty();
+ }
+
+ SdkEntityBuilder(SdkEntity entity) {
+ this.entityType = entity.getType();
+ this.schemaUrl = entity.getSchemaUrl();
+ this.identity = entity.getIdentity();
+ this.description = entity.getDescription();
+ }
+
+ @Override
+ public EntityBuilder setSchemaUrl(String schemaUrl) {
+ this.schemaUrl = schemaUrl;
+ return this;
+ }
+
+ @Override
+ public EntityBuilder setIdentity(Attributes identity) {
+ this.identity = identity;
+ return this;
+ }
+
+ @Override
+ public EntityBuilder setDescription(Attributes description) {
+ this.description = description;
+ return this;
+ }
+
+ @Override
+ public Entity build() {
+ // TODO - assertions around safe entity builds.
+ // TODO - identity is not empty.
+ return new SdkEntity(entityType, schemaUrl, identity, description);
+ }
+}
diff --git a/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.resources.EntityDetector b/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.resources.EntityDetector
new file mode 100644
index 00000000000..5941d4c812b
--- /dev/null
+++ b/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.resources.EntityDetector
@@ -0,0 +1,3 @@
+io.opentelemetry.sdk.extension.incubator.resources.ServiceEntityDetector
+io.opentelemetry.sdk.extension.incubator.resources.EnvEntityDetector
+io.opentelemetry.sdk.extension.incubator.resources.TelemetrySdkEntityDetector
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 f1eb6a2e82b..9abf22ff39f 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
@@ -182,7 +182,7 @@ void create_ModelCustomizer() {
.getSdk();
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-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java
new file mode 100644
index 00000000000..ff209cef067
--- /dev/null
+++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.resources;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
+import io.opentelemetry.sdk.extension.incubator.resources.internal.SdkEntity;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class EnvEntityDetectorTest {
+
+ @Test
+ void testEmpty() {
+ EnvEntityDetector detector = new EnvEntityDetector();
+ Collection entities =
+ detector.detect(DefaultConfigProperties.createFromMap(Collections.emptyMap()));
+ assertThat(entities).isEmpty();
+ }
+
+ @Test
+ void testSingleEntity() {
+ EnvEntityDetector detector = new EnvEntityDetector();
+ String value =
+ "service{service.name=my-app,service.instance.id=instance-1}[service.version=1.0.0]";
+ Collection entities =
+ detector.detect(
+ DefaultConfigProperties.createFromMap(
+ Collections.singletonMap("otel.entities", value)));
+
+ assertThat(entities).hasSize(1);
+ SdkEntity entity = (SdkEntity) entities.iterator().next();
+ assertThat(entity.getType()).isEqualTo("service");
+ assertThat(entity.getIdentity())
+ .isEqualTo(
+ Attributes.builder()
+ .put("service.name", "my-app")
+ .put("service.instance.id", "instance-1")
+ .build());
+ assertThat(entity.getDescription())
+ .isEqualTo(Attributes.builder().put("service.version", "1.0.0").build());
+ assertThat(entity.getSchemaUrl()).isNull();
+ }
+
+ @Test
+ void testMultipleEntitiesWithSchemaUrl() {
+ EnvEntityDetector detector = new EnvEntityDetector();
+ String value =
+ "service{service.name=my-app}@https://opentelemetry.io/schemas/1.21.0;host{host.id=host-123}[host.name=web-server-01]";
+ Collection entities =
+ detector.detect(
+ DefaultConfigProperties.createFromMap(
+ Collections.singletonMap("otel.entities", value)));
+
+ assertThat(entities).hasSize(2);
+ List list = new ArrayList<>(entities);
+
+ SdkEntity entity1 = (SdkEntity) list.get(0);
+ assertThat(entity1.getType()).isEqualTo("service");
+ assertThat(entity1.getIdentity())
+ .isEqualTo(Attributes.builder().put("service.name", "my-app").build());
+ assertThat(entity1.getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.21.0");
+
+ SdkEntity entity2 = (SdkEntity) list.get(1);
+ assertThat(entity2.getType()).isEqualTo("host");
+ assertThat(entity2.getIdentity())
+ .isEqualTo(Attributes.builder().put("host.id", "host-123").build());
+ assertThat(entity2.getDescription())
+ .isEqualTo(Attributes.builder().put("host.name", "web-server-01").build());
+ assertThat(entity2.getSchemaUrl()).isNull();
+ }
+
+ @Test
+ void testPercentDecoding() {
+ EnvEntityDetector detector = new EnvEntityDetector();
+ String value = "service{service.name=my%2Capp}[config=key%3Dvalue%5Bprod%5D]";
+ Collection entities =
+ detector.detect(
+ DefaultConfigProperties.createFromMap(
+ Collections.singletonMap("otel.entities", value)));
+
+ assertThat(entities).hasSize(1);
+ SdkEntity entity = (SdkEntity) entities.iterator().next();
+ assertThat(entity.getType()).isEqualTo("service");
+ assertThat(entity.getIdentity())
+ .isEqualTo(Attributes.builder().put("service.name", "my,app").build());
+ assertThat(entity.getDescription())
+ .isEqualTo(Attributes.builder().put("config", "key=value[prod]").build());
+ }
+
+ @Test
+ void testEmptyStringsIgnored() {
+ EnvEntityDetector detector = new EnvEntityDetector();
+ String value = ";service{service.name=app1};;host{host.id=host-123};";
+ Collection entities =
+ detector.detect(
+ DefaultConfigProperties.createFromMap(
+ Collections.singletonMap("otel.entities", value)));
+
+ assertThat(entities).hasSize(2);
+ }
+
+ @Test
+ void testMalformedSyntax_MissingBrace() {
+ EnvEntityDetector detector = new EnvEntityDetector();
+ String value = "service service.name=app1};host{host.id=host-123}";
+ Collection entities =
+ detector.detect(
+ DefaultConfigProperties.createFromMap(
+ Collections.singletonMap("otel.entities", value)));
+
+ // Should skip the malformed one and process the valid one
+ assertThat(entities).hasSize(1);
+ SdkEntity entity = (SdkEntity) entities.iterator().next();
+ assertThat(entity.getType()).isEqualTo("host");
+ }
+
+ @Test
+ void testMalformedSyntax_MissingBraceEnd() {
+ EnvEntityDetector detector = new EnvEntityDetector();
+ String value = "service{service.name=app1;host{host.id=host-123}";
+ Collection entities =
+ detector.detect(
+ DefaultConfigProperties.createFromMap(
+ Collections.singletonMap("otel.entities", value)));
+
+ assertThat(entities).hasSize(1);
+ SdkEntity entity = (SdkEntity) entities.iterator().next();
+ assertThat(entity.getType()).isEqualTo("host");
+ }
+
+ @Test
+ void testMissingRequiredFields_EmptyIdentity() {
+ EnvEntityDetector detector = new EnvEntityDetector();
+ String value = "service{};host{host.id=host-123}";
+ Collection entities =
+ detector.detect(
+ DefaultConfigProperties.createFromMap(
+ Collections.singletonMap("otel.entities", value)));
+
+ assertThat(entities).hasSize(1);
+ SdkEntity entity = (SdkEntity) entities.iterator().next();
+ assertThat(entity.getType()).isEqualTo("host");
+ }
+}
diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java
new file mode 100644
index 00000000000..40018d77fb7
--- /dev/null
+++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.resources;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
+import io.opentelemetry.sdk.extension.incubator.resources.internal.SdkEntity;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class ServiceEntityDetectorTest {
+
+ @Test
+ void testDetect() {
+ ServiceEntityDetector detector = new ServiceEntityDetector();
+ Collection entities =
+ detector.detect(
+ DefaultConfigProperties.createFromMap(
+ Collections.singletonMap("otel.service.name", "my-service")));
+
+ assertThat(entities).hasSize(2);
+ List list = new ArrayList<>(entities);
+
+ SdkEntity serviceEntity = (SdkEntity) list.get(0);
+ assertThat(serviceEntity.getType()).isEqualTo("service");
+ assertThat(serviceEntity.getIdentity())
+ .isEqualTo(Attributes.builder().put("service.name", "my-service").build());
+ assertThat(serviceEntity.getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.40.0");
+
+ SdkEntity serviceInstanceEntity = (SdkEntity) list.get(1);
+ assertThat(serviceInstanceEntity.getType()).isEqualTo("service.instance");
+ assertThat(serviceInstanceEntity.getIdentity().get(ServiceEntityDetector.SERVICE_INSTANCE_ID))
+ .isNotNull()
+ .isNotEmpty();
+ assertThat(serviceInstanceEntity.getSchemaUrl())
+ .isEqualTo("https://opentelemetry.io/schemas/1.40.0");
+
+ // Verify that another call returns the same instance ID (static final RANDOM)
+ Collection entities2 =
+ detector.detect(
+ DefaultConfigProperties.createFromMap(
+ Collections.singletonMap("otel.service.name", "my-service")));
+ List list2 = new ArrayList<>(entities2);
+ SdkEntity serviceInstanceEntity2 = (SdkEntity) list2.get(1);
+ assertThat(serviceInstanceEntity2.getIdentity().get(ServiceEntityDetector.SERVICE_INSTANCE_ID))
+ .isEqualTo(
+ serviceInstanceEntity.getIdentity().get(ServiceEntityDetector.SERVICE_INSTANCE_ID));
+ }
+}
diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java
new file mode 100644
index 00000000000..8e74bfc8065
--- /dev/null
+++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.extension.incubator.resources;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
+import io.opentelemetry.sdk.common.internal.OtelVersion;
+import io.opentelemetry.sdk.extension.incubator.resources.internal.SdkEntity;
+import java.util.Collection;
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+
+class TelemetrySdkEntityDetectorTest {
+
+ @Test
+ void testDetect() {
+ TelemetrySdkEntityDetector detector = new TelemetrySdkEntityDetector();
+ Collection entities =
+ detector.detect(DefaultConfigProperties.createFromMap(Collections.emptyMap()));
+
+ assertThat(entities).hasSize(1);
+ SdkEntity entity = (SdkEntity) entities.iterator().next();
+
+ assertThat(entity.getType()).isEqualTo("telemetry.sdk");
+ assertThat(entity.getIdentity())
+ .isEqualTo(
+ Attributes.builder()
+ .put("telemetry.sdk.name", "opentelemetry")
+ .put("telemetry.sdk.language", "java")
+ .build());
+ assertThat(entity.getDescription())
+ .isEqualTo(Attributes.builder().put("telemetry.sdk.version", OtelVersion.VERSION).build());
+ assertThat(entity.getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.40.0");
+ }
+}
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..163104bd8ff 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");
@@ -33,14 +33,6 @@ public abstract class Resource {
AttributeKey.stringKey("telemetry.sdk.name");
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 +83,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 +97,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 +129,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 entities.
+ *
+ * @return a map of attributes.
+ */
+ abstract Attributes getRawAttributes();
+
+ /**
+ * Returns a collection 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 +180,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..5bb87bf3de4 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;
/**
@@ -192,8 +200,33 @@ public ResourceBuilder setSchemaUrl(String schemaUrl) {
return this;
}
+ /** Create the {@link Resource} from this. */
/** Create the {@link Resource} from this. */
public Resource build() {
- return Resource.create(attributesBuilder.build(), schemaUrl);
+ // Derive schemaUrl from entity, 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();
+ }
+ }
+
+ // 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