diff --git a/buildSrc/src/main/java/io/opentelemetry/gradle/js2p/OtelDescriptionRule.java b/buildSrc/src/main/java/io/opentelemetry/gradle/js2p/OtelDescriptionRule.java
new file mode 100644
index 00000000000..a254a8e6e7f
--- /dev/null
+++ b/buildSrc/src/main/java/io/opentelemetry/gradle/js2p/OtelDescriptionRule.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.gradle.js2p;
+
+import com.sun.codemodel.JDocComment;
+import com.sun.codemodel.JDocCommentable;
+import com.sun.codemodel.JFieldVar;
+import com.sun.codemodel.JMethod;
+import org.jsonschema2pojo.Schema;
+import org.jsonschema2pojo.rules.DescriptionRule;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * A {@link DescriptionRule} that suppresses javadoc placement on fields and getters.
+ *
+ *
jsonschema2pojo's {@code PropertyRule.apply()} calls {@code resolveRefs()} before applying
+ * annotations. When a property node is {@code {"$ref": "..."}}, the resolved node is the
+ * referenced {@code $def}'s content. If that {@code $def} has a {@code description}, jsonschema2pojo
+ * applies it to both the field and the getter via this rule.
+ *
+ *
This rule skips {@link JFieldVar} targets entirely (fields carry no javadoc) and skips
+ * {@link JMethod} targets (descriptions on getters are applied explicitly by
+ * {@link OtelPropertyRule}, using the property's own {@code description}, not the {@code $def}'s).
+ * Class-level targets ({@link com.sun.codemodel.JDefinedClass}) are passed through unchanged.
+ */
+class OtelDescriptionRule extends DescriptionRule {
+
+ @Override
+ public JDocComment apply(
+ String nodeName,
+ JsonNode node,
+ JsonNode parent,
+ JDocCommentable generatableType,
+ Schema schema) {
+ if (generatableType instanceof JFieldVar || generatableType instanceof JMethod) {
+ // Do not call generatableType.javadoc() here: invoking javadoc() on a target that has
+ // no existing JDocComment auto-creates an empty one, which causes JFieldVar.declare() to
+ // emit an empty /** */ block in the generated source.
+ return null;
+ }
+ return super.apply(nodeName, node, parent, generatableType, schema);
+ }
+}
diff --git a/buildSrc/src/main/java/io/opentelemetry/gradle/js2p/OtelPropertyRule.java b/buildSrc/src/main/java/io/opentelemetry/gradle/js2p/OtelPropertyRule.java
index d16cb8bc030..273b7648ce2 100644
--- a/buildSrc/src/main/java/io/opentelemetry/gradle/js2p/OtelPropertyRule.java
+++ b/buildSrc/src/main/java/io/opentelemetry/gradle/js2p/OtelPropertyRule.java
@@ -13,7 +13,6 @@
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JMethod;
import org.jsonschema2pojo.Schema;
-import org.jsonschema2pojo.exception.GenerationException;
import org.jsonschema2pojo.rules.PropertyRule;
import org.jsonschema2pojo.rules.RuleFactory;
@@ -32,9 +31,14 @@
* valid; re-applying from the original node restores those descriptions on the getter too.
*
*
Mechanism: strip {@code description} from the node before delegating to {@link
- * PropertyRule#apply} so the superclass adds it nowhere (not the field javadoc, the getter javadoc,
- * or a {@code @JsonPropertyDescription}), then apply it to the getter only — leaving fields without
- * any javadoc.
+ * PropertyRule#apply} so the superclass does not add the property description to the field or
+ * getter. The superclass calls {@code resolveRefs()} before applying annotations, so if the
+ * property node is a {@code $ref} whose target {@code $def} has its own description, that
+ * description would otherwise land on the field and getter via {@link
+ * org.jsonschema2pojo.rules.DescriptionRule}. {@link OtelDescriptionRule} suppresses all
+ * description placements on fields and getters from that path. The property's own description
+ * (stripped from the node before delegation) is then applied to the getter only by
+ * {@link #applyGetterDescription}, which writes directly to the getter's javadoc.
*
*
{@link OtelJacksonAnnotator} puts {@code @JsonProperty} on the getter, but has no hook for the
* {@code withX} builder methods, so this rule annotates them.
@@ -69,19 +73,6 @@ public JDefinedClass apply(
return result;
}
- // A $ref target with its own top-level description would land on the field via DescriptionRule
- // and conflict with the sibling description we re-apply to the getter. The schema puts
- // descriptions on properties, not $defs, so this doesn't happen today; fail loud if it does.
- if (referencedTypeHasDescription(node, schema)) {
- throw new GenerationException(
- "Property '"
- + nodeName
- + "' resolves to a type that defines its own top-level description, which is not"
- + " handled (it would land on the field). See "
- + OtelPropertyRule.class.getName()
- + ".");
- }
-
annotateBuilderMethod(result, propertyName, node, nodeName);
if (description != null) {
@@ -102,29 +93,6 @@ private void annotateBuilderMethod(
}
}
- /**
- * Returns whether a (possibly chained) {@code $ref} resolves to a schema with its own
- * description. Uses {@link org.jsonschema2pojo.SchemaStore} rather than inspecting {@code
- * field.javadoc()} (which would instantiate an empty comment).
- */
- private boolean referencedTypeHasDescription(JsonNode node, Schema schema) {
- JsonNode current = node;
- Schema currentSchema = schema;
- boolean resolved = false;
- while (current.has("$ref")) {
- currentSchema =
- ruleFactory
- .getSchemaStore()
- .create(
- currentSchema,
- current.get("$ref").asText(),
- ruleFactory.getGenerationConfig().getRefFragmentPathDelimiters());
- current = currentSchema.getContent();
- resolved = true;
- }
- return resolved && current.has("description");
- }
-
private void applyGetterDescription(
String nodeName,
JsonNode node,
@@ -134,11 +102,13 @@ private void applyGetterDescription(
JFieldVar field,
JsonNode description) {
String getterName = ruleFactory.getNameHelper().getGetterName(propertyName, field.type(), node);
+ String text = preserveLineBreaks(description).asText();
for (JMethod method : jclass.methods()) {
if (method.name().equals(getterName)) {
- ruleFactory
- .getDescriptionRule()
- .apply(nodeName, preserveLineBreaks(description), node, method, schema);
+ // OtelDescriptionRule suppresses DescriptionRule for JMethod targets, so write the
+ // description directly. JCommentPart.format() handles embedded '\n' characters by
+ // emitting line breaks, so append the whole text as one string to preserve newlines.
+ method.javadoc().append(text);
return;
}
}
diff --git a/buildSrc/src/main/java/io/opentelemetry/gradle/js2p/OtelRuleFactory.java b/buildSrc/src/main/java/io/opentelemetry/gradle/js2p/OtelRuleFactory.java
index 77750a99dc9..d7c67283d11 100644
--- a/buildSrc/src/main/java/io/opentelemetry/gradle/js2p/OtelRuleFactory.java
+++ b/buildSrc/src/main/java/io/opentelemetry/gradle/js2p/OtelRuleFactory.java
@@ -7,6 +7,7 @@
import com.sun.codemodel.JClassContainer;
import com.sun.codemodel.JDefinedClass;
+import com.sun.codemodel.JDocComment;
import com.sun.codemodel.JDocCommentable;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JType;
@@ -51,6 +52,11 @@ public Rule getPropertyRule() {
return new OtelPropertyRule(this);
}
+ @Override
+ public Rule getDescriptionRule() {
+ return new OtelDescriptionRule();
+ }
+
@Override
public Rule getAdditionalPropertiesRule() {
return new OtelAdditionalPropertiesRule(this);
diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-autoconfigure.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-autoconfigure.txt
index 2ab71f7e8b0..11d677af0cb 100644
--- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-autoconfigure.txt
+++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-autoconfigure.txt
@@ -1,2 +1,8 @@
Comparing source compatibility of opentelemetry-sdk-extension-autoconfigure-1.65.0-SNAPSHOT.jar against opentelemetry-sdk-extension-autoconfigure-1.64.0.jar
-No changes.
\ No newline at end of file
++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.autoconfigure.EnvironmentEntityResourceProvider (not serializable)
+ +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a.
+ +++ NEW INTERFACE: io.opentelemetry.sdk.autoconfigure.spi.Ordered
+ +++ NEW INTERFACE: io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider
+ +++ NEW SUPERCLASS: java.lang.Object
+ +++ NEW CONSTRUCTOR: PUBLIC(+) EnvironmentEntityResourceProvider()
+ +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.Resource createResource(io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties)
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..d11ac55fa16
--- /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);
+ }
+
+ /** Constructs 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);
+ size += MarshalerUtil.sizeRepeatedString(EntityRef.ID_KEYS, idKeysUtf8);
+ size += 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..b7bc0cad505 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 += 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..fcf9907dfb1
--- /dev/null
+++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshalerTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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", Attributes.builder().put("id.key", "id.value").build())
+ .setSchemaUrl("test-url")
+ .setDescription(Attributes.builder().put("desc.key", "desc.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
+ // time. 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/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/ResourceMarshalerTest.java b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/ResourceMarshalerTest.java
new file mode 100644
index 00000000000..e55690adb37
--- /dev/null
+++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/ResourceMarshalerTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.exporter.internal.otlp;
+
+import static io.opentelemetry.api.common.AttributeKey.stringKey;
+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.resource.v1.Resource;
+import io.opentelemetry.sdk.resources.ResourceBuilder;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import io.opentelemetry.sdk.resources.internal.EntityUtil;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import org.junit.jupiter.api.Test;
+
+class ResourceMarshalerTest {
+
+ @Test
+ void marshalResourceWithEntities() {
+ Entity entity =
+ Entity.builder("process", Attributes.of(stringKey("process.pid"), "1234"))
+ .setDescription(Attributes.of(stringKey("process.executable.name"), "java"))
+ .setSchemaUrl("http://process.schema")
+ .build();
+
+ ResourceBuilder builder =
+ io.opentelemetry.sdk.resources.Resource.builder().put("service.name", "my-service");
+ EntityUtil.addEntity(builder, entity);
+ io.opentelemetry.sdk.resources.Resource resourceWithEntity = builder.build();
+
+ Resource proto =
+ parse(Resource.getDefaultInstance(), ResourceMarshaler.create(resourceWithEntity));
+
+ assertThat(proto.getAttributesList()).hasSize(3);
+ assertThat(proto.getAttributesList().stream().map(a -> a.getKey()))
+ .containsExactlyInAnyOrder("service.name", "process.pid", "process.executable.name");
+
+ assertThat(proto.getEntityRefsList()).hasSize(1);
+ assertThat(proto.getEntityRefs(0).getType()).isEqualTo("process");
+ assertThat(proto.getEntityRefs(0).getSchemaUrl()).isEqualTo("http://process.schema");
+ assertThat(proto.getEntityRefs(0).getIdKeysList()).containsExactly("process.pid");
+ assertThat(proto.getEntityRefs(0).getDescriptionKeysList())
+ .containsExactly("process.executable.name");
+ }
+
+ @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);
+ }
+ assertThat(result.getSerializedSize()).isEqualTo(serialized.length);
+
+ // Compare JSON
+ String json = toJson(marshaler);
+ Message.Builder protoBuilder = prototype.newBuilderForType();
+ try {
+ JsonFormat.parser().merge(json, protoBuilder);
+ } catch (InvalidProtocolBufferException e) {
+ throw new UncheckedIOException(e);
+ }
+ assertThat(protoBuilder.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/build.gradle.kts b/sdk-extensions/autoconfigure/build.gradle.kts
index 288fff4be41..7f596a40fca 100644
--- a/sdk-extensions/autoconfigure/build.gradle.kts
+++ b/sdk-extensions/autoconfigure/build.gradle.kts
@@ -31,6 +31,7 @@ dependencies {
api(project(":sdk-extensions:autoconfigure-spi"))
compileOnly(project(":api:incubator"))
+ compileOnly(project(":sdk-extensions:incubator"))
compileOnly(project(":sdk-extensions:declarative-config"))
annotationProcessor("com.google.auto.value:auto-value")
@@ -107,6 +108,7 @@ testing {
register("testIncubating") {
dependencies {
implementation(project(":sdk-extensions:declarative-config"))
+ implementation(project(":sdk-extensions:incubator"))
implementation(project(":exporters:logging"))
implementation(project(":exporters:otlp:all"))
implementation(project(":sdk:testing"))
diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EnvironmentEntityResourceProvider.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EnvironmentEntityResourceProvider.java
new file mode 100644
index 00000000000..f85628fa694
--- /dev/null
+++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EnvironmentEntityResourceProvider.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.autoconfigure;
+
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
+import io.opentelemetry.sdk.resources.Resource;
+
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
+// TODO(jack-berg): Figure out how to avoid this as new public API surface area prior to merging
+public final class EnvironmentEntityResourceProvider implements ResourceProvider {
+ @Override
+ public Resource createResource(ConfigProperties config) {
+ return EnvironmentResource.otelEntitiesResource(config);
+ }
+}
diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EnvironmentResource.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EnvironmentResource.java
index 67372de748b..8e0c65ba7a1 100644
--- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EnvironmentResource.java
+++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EnvironmentResource.java
@@ -5,13 +5,22 @@
package io.opentelemetry.sdk.autoconfigure;
-import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.common.internal.SemConvAttributes;
import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.resources.ResourceBuilder;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import io.opentelemetry.sdk.resources.internal.EntityBuilder;
+import io.opentelemetry.sdk.resources.internal.EntityUtil;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
/**
* Creates an OpenTelemetry {@link Resource} from environment configuration.
@@ -22,11 +31,11 @@
*/
final class EnvironmentResource {
- private static final AttributeKey SERVICE_NAME = AttributeKey.stringKey("service.name");
-
// Visible for testing
- static final String ATTRIBUTE_PROPERTY = "otel.resource.attributes";
+ static final String RESOURCE_ATTRIBUTES_PROPERTY = "otel.resource.attributes";
static final String SERVICE_NAME_PROPERTY = "otel.service.name";
+ static final String ENTITIES_PROPERTY = "otel.entities";
+ static final String EXPERIMENTAL_ENTITIES_ENABLED = "otel.experimental.entities.enabled";
/**
* Create a {@link Resource} from the environment. The resource contains attributes parsed from
@@ -37,22 +46,62 @@ final class EnvironmentResource {
* @return the resource.
*/
@SuppressWarnings("JdkObsolete") // Recommended alternative was introduced in java 10
- static Resource createEnvironmentResource(ConfigProperties config) {
- AttributesBuilder resourceAttributes = Attributes.builder();
- for (Map.Entry entry : config.getMap(ATTRIBUTE_PROPERTY).entrySet()) {
- resourceAttributes.put(
+ static Resource otelResourceAttributesResource(ConfigProperties config) {
+ ResourceBuilder resourceBuilder = Resource.builder();
+
+ for (Map.Entry entry : config.getMap(RESOURCE_ATTRIBUTES_PROPERTY).entrySet()) {
+ resourceBuilder.put(
entry.getKey(),
// Attributes specified via otel.resource.attributes follow the W3C Baggage spec and
// characters outside the baggage-octet range are percent encoded
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable
decodeResourceAttributes(entry.getValue()));
}
+
+ return resourceBuilder.build();
+ }
+
+ static Resource otelEntitiesResource(ConfigProperties config) {
+ ResourceBuilder resourceBuilder = Resource.builder();
+
+ boolean entitiesEnabled = config.getBoolean(EXPERIMENTAL_ENTITIES_ENABLED, false);
+
+ String entitiesStr = config.getString(ENTITIES_PROPERTY);
+ if (entitiesEnabled && entitiesStr != null && !entitiesStr.isEmpty()) {
+ List parsedEntities = new EntityParser(entitiesStr).parse();
+ for (Entity entity : parsedEntities) {
+ EntityUtil.addEntity(resourceBuilder, entity);
+ }
+ }
+
+ return resourceBuilder.build();
+ }
+
+ static Resource otelServiceNameResource(ConfigProperties config) {
String serviceName = config.getString(SERVICE_NAME_PROPERTY);
- if (serviceName != null) {
- resourceAttributes.put(SERVICE_NAME, serviceName);
+ if (serviceName == null) {
+ return Resource.empty();
}
- return Resource.create(resourceAttributes.build());
+ Entity serviceEntity =
+ Entity.builder(
+ SemConvAttributes.SERVICE_TYPE,
+ Attributes.of(SemConvAttributes.SERVICE_NAME, serviceName))
+ .setSchemaUrl(SemConvAttributes.SCHEMA_URL_V1_40_0)
+ .build();
+ return EntityUtil.addEntity(Resource.builder(), serviceEntity).build();
+ }
+
+ static Resource eraseEntities(Resource resource) {
+ ResourceBuilder entityErasedBuilder = Resource.builder().putAll(resource.getAttributes());
+ // TODO: entities include schemaUrl, which the resource merge algorithm retains as long as all
+ // entities agree. However, schemaUrl is not set by by pre-entity resource detectors. Preserving
+ // the schemaUrl of while erasing entities adds new schemaUrl to the resource when none was
+ // typically used before. Therefore, we do not map the schemaUrl.
+ // if (resource.getSchemaUrl() != null) {
+ // entityErasedBuilder.setSchemaUrl(resource.getSchemaUrl());
+ // }
+ return entityErasedBuilder.build();
}
/**
@@ -102,5 +151,237 @@ private static String decodeResourceAttributes(String value) {
return new String(bytes, 0, pos, StandardCharsets.UTF_8);
}
+ 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);
+ }
+
+ void reset(int start) {
+ this.start = start;
+ this.end = start;
+ this.needsDecoding = false;
+ }
+
+ void markEnd(int end) {
+ this.end = end;
+ }
+
+ void markNeedsDecoding() {
+ this.needsDecoding = true;
+ }
+
+ boolean isEmpty() {
+ return start >= end;
+ }
+
+ String getValue() {
+ if (isEmpty()) {
+ return "";
+ }
+ String substring = source.substring(start, end).trim();
+ return needsDecoding ? decodeResourceAttributes(substring) : substring;
+ }
+ }
+
+ // State machine parser
+ private static final class EntityParser {
+ private static final Logger logger = Logger.getLogger(EntityParser.class.getName());
+
+ private enum State {
+ TYPE,
+ ID_KEY,
+ ID_VAL,
+ DESC_KEY,
+ DESC_VAL,
+ SCHEMA_URL,
+ SKIP_TO_NEXT
+ }
+
+ private final String input;
+ private State state = State.TYPE;
+ private final Segment currentSegment;
+ private final List entities = new ArrayList<>();
+
+ @Nullable private String currentType;
+ private Attributes currentIdAttrs = Attributes.empty();
+ private Attributes currentDescAttrs = Attributes.empty();
+ @Nullable private String currentSchemaUrl;
+ @Nullable private AttributesBuilder currentBuilder;
+ @Nullable private String currentKey;
+
+ EntityParser(String input) {
+ this.input = input;
+ this.currentSegment = new Segment(input);
+ }
+
+ List parse() {
+ int n = input.length();
+ for (int i = 0; i < n; i++) {
+ char c = input.charAt(i);
+
+ if (state == State.SKIP_TO_NEXT) {
+ if (c == ';') {
+ resetEntityState(i + 1);
+ state = State.TYPE;
+ }
+ continue;
+ }
+
+ switch (c) {
+ case '{':
+ 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 '}':
+ 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;
+ currentSegment.reset(i + 1);
+ }
+ }
+ break;
+ case '[':
+ if (state == State.TYPE) {
+ state = State.DESC_KEY;
+ currentSegment.reset(i + 1);
+ currentBuilder = Attributes.builder();
+ }
+ break;
+ case ']':
+ 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 '=':
+ 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 ',':
+ 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 '@':
+ if (state == State.TYPE) {
+ state = State.SCHEMA_URL;
+ currentSegment.reset(i + 1);
+ }
+ break;
+ case ';':
+ 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 '%':
+ currentSegment.markNeedsDecoding();
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (state == State.TYPE || state == State.SCHEMA_URL) {
+ if (state == State.SCHEMA_URL) {
+ currentSegment.markEnd(input.length());
+ currentSchemaUrl = currentSegment.getValue();
+ }
+ buildAndAddEntity();
+ }
+
+ return entities;
+ }
+
+ private void putAttr() {
+ String val = currentSegment.getValue();
+ if (currentKey != null && !currentKey.isEmpty() && currentBuilder != null) {
+ currentBuilder.put(currentKey, val);
+ }
+ }
+
+ private void buildAndAddEntity() {
+ if (currentType != null && !currentType.isEmpty() && !currentIdAttrs.isEmpty()) {
+ EntityBuilder builder = Entity.builder(currentType, currentIdAttrs);
+ if (!currentDescAttrs.isEmpty()) {
+ builder.setDescription(currentDescAttrs);
+ }
+ if (currentSchemaUrl != null && !currentSchemaUrl.isEmpty()) {
+ builder.setSchemaUrl(currentSchemaUrl);
+ }
+ entities.add(builder.build());
+ }
+ }
+
+ private void resetEntityState(int nextStart) {
+ currentType = null;
+ currentIdAttrs = Attributes.empty();
+ currentDescAttrs = Attributes.empty();
+ currentSchemaUrl = null;
+ currentBuilder = null;
+ currentKey = null;
+ currentSegment.reset(nextStart);
+ }
+ }
+
private EnvironmentResource() {}
}
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..cda8d8b8e94 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
@@ -5,6 +5,9 @@
package io.opentelemetry.sdk.autoconfigure;
+import static io.opentelemetry.sdk.autoconfigure.EnvironmentResource.EXPERIMENTAL_ENTITIES_ENABLED;
+import static io.opentelemetry.sdk.autoconfigure.EnvironmentResource.eraseEntities;
+
import io.opentelemetry.common.ComponentLoader;
import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
@@ -54,7 +57,10 @@ public static Resource createEnvironmentResource() {
* @return the resource.
*/
public static Resource createEnvironmentResource(ConfigProperties config) {
- return EnvironmentResource.createEnvironmentResource(config);
+ ResourceBuilder builder = Resource.builder();
+ builder.putAll(EnvironmentResource.otelResourceAttributesResource(config));
+ builder.putAll(EnvironmentResource.otelServiceNameResource(config));
+ return maybeEraseEntities(builder.build(), config);
}
static Resource configureResource(
@@ -83,6 +89,8 @@ static Resource configureResource(
result = filterAttributes(result, config);
+ maybeEraseEntities(result, config);
+
return resourceCustomizer.apply(result, config);
}
@@ -101,5 +109,13 @@ static Resource filterAttributes(Resource resource, ConfigProperties configPrope
return builder.build();
}
+ static Resource maybeEraseEntities(Resource resource, ConfigProperties config) {
+ boolean entitiesEnabled = config.getBoolean(EXPERIMENTAL_ENTITIES_ENABLED, false);
+ if (entitiesEnabled) {
+ return resource;
+ }
+ return eraseEntities(resource);
+ }
+
private ResourceConfiguration() {}
}
diff --git a/sdk-extensions/autoconfigure/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider b/sdk-extensions/autoconfigure/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider
index f9bd3554d79..f6632f3cb22 100644
--- a/sdk-extensions/autoconfigure/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider
+++ b/sdk-extensions/autoconfigure/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider
@@ -1 +1,2 @@
io.opentelemetry.sdk.autoconfigure.EnvironmentResourceProvider
+io.opentelemetry.sdk.autoconfigure.EnvironmentEntityResourceProvider
diff --git a/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceConfigurationTest.java b/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceConfigurationTest.java
index d8e97e382ca..4c897f9c06e 100644
--- a/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceConfigurationTest.java
+++ b/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceConfigurationTest.java
@@ -17,6 +17,11 @@
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import io.opentelemetry.sdk.resources.internal.EntityUtil;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
@@ -166,4 +171,79 @@ void filterAttributes() {
assertThat(resource.getAttributes().get(stringKey("bar"))).isNull();
});
}
+
+ @ParameterizedTest
+ @MethodSource("createEnvironmentResourceEntitiesTestCases")
+ void createEnvironmentResource_WithEntities(
+ Map properties, Collection expectedEntities) {
+ ConfigProperties configProperties = DefaultConfigProperties.createFromMap(properties);
+
+ Resource resource =
+ ResourceConfiguration.configureResource(
+ configProperties,
+ SpiHelper.create(ResourceConfigurationTest.class.getClassLoader()),
+ (r, c) -> r);
+
+ Collection entities = EntityUtil.getEntities(resource);
+ assertThat(entities).hasSize(expectedEntities.size());
+ assertThat(entities).containsAll(expectedEntities);
+ }
+
+ static Stream createEnvironmentResourceEntitiesTestCases() {
+ return Stream.of(
+ Arguments.argumentSet(
+ "otel.entities happy path",
+ ImmutableMap.of(
+ "otel.experimental.entities.enabled",
+ "true",
+ "otel.entities",
+ "process{process.pid=1234}[process.executable.name=java]@http://schema;host{host.id=myhost}"),
+ Arrays.asList(
+ Entity.builder("process", Attributes.of(stringKey("process.pid"), "1234"))
+ .setSchemaUrl("http://schema")
+ .setDescription(Attributes.of(stringKey("process.executable.name"), "java"))
+ .build(),
+ Entity.builder("host", Attributes.of(stringKey("host.id"), "myhost")).build())),
+ Arguments.argumentSet(
+ "percent decoding",
+ ImmutableMap.of(
+ "otel.experimental.entities.enabled",
+ "true",
+ "otel.entities",
+ "service{service.name=my+app,space=hello%20world,utf8=%C3%A9,invalid=%2G,incomplete=%2,end=%}"),
+ Collections.singletonList(
+ Entity.builder(
+ "service",
+ Attributes.builder()
+ .put("service.name", "my+app")
+ .put("space", "hello world")
+ .put("utf8", "é")
+ .put("invalid", "%2G")
+ .put("incomplete", "%2")
+ .put("end", "%")
+ .build())
+ .build())),
+ Arguments.argumentSet(
+ "malformed",
+ ImmutableMap.of(
+ "otel.experimental.entities.enabled",
+ "true",
+ "otel.entities",
+ "{empty.type=val};process{};process{=val};process{key;=val};host{host.id=valid}"),
+ Collections.singletonList(
+ Entity.builder("host", Attributes.builder().put("host.id", "valid").build())
+ .build())),
+ Arguments.argumentSet(
+ "empty",
+ ImmutableMap.of("otel.experimental.entities.enabled", "true", "otel.entities", ""),
+ Collections.emptyList()),
+ Arguments.argumentSet(
+ "otel.experimental.entities.enabled=false",
+ ImmutableMap.of(
+ "otel.experimental.entities.enabled",
+ "false",
+ "otel.entities",
+ "process{process.pid=1234}[process.executable.name=java]@http://schema;host{host.id=myhost}"),
+ Collections.emptyList()));
+ }
}
diff --git a/sdk-extensions/declarative-config/build.gradle.kts b/sdk-extensions/declarative-config/build.gradle.kts
index 3971acd71ab..81818c2746c 100644
--- a/sdk-extensions/declarative-config/build.gradle.kts
+++ b/sdk-extensions/declarative-config/build.gradle.kts
@@ -61,8 +61,8 @@ dependencies {
// To regenerate (e.g. after a schema update), run: ./gradlew :sdk-extensions:declarative-config:syncPojoModelsToSrc
val configurationTag = "1.1.0"
-val configurationRef = "refs/tags/v$configurationTag" // Replace with commit SHA to point to experiment with a specific commit
-val configurationRepoZip = "https://github.com/open-telemetry/opentelemetry-configuration/archive/$configurationRef.zip"
+val configurationRef = "1a0e07738e8b54a465e90d1dc1e4ce7be4ad66ae" // "refs/tags/v$configurationTag" // Replace with commit SHA to point to experiment with a specific commit
+val configurationRepoZip = "https://github.com/jack-berg/opentelemetry-configuration/archive/$configurationRef.zip"
val buildDirectory = layout.buildDirectory.asFile.get()
val downloadConfigurationSchema = tasks.register("downloadConfigurationSchema") {
diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetector.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetector.java
new file mode 100644
index 00000000000..89e1349e69b
--- /dev/null
+++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetector.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.autoconfigure.declarativeconfig;
+
+import static io.opentelemetry.sdk.autoconfigure.declarativeconfig.EnvironmentResource.ENTITIES_PROPERTY;
+import static io.opentelemetry.sdk.autoconfigure.declarativeconfig.EnvironmentResource.EXPERIMENTAL_ENTITIES_ENABLED;
+
+import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider;
+import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
+import io.opentelemetry.sdk.resources.Resource;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class EntitiesEnvResourceDetector implements ComponentProvider {
+
+ @Override
+ public Class getType() {
+ return Resource.class;
+ }
+
+ @Override
+ public String getName() {
+ return "env";
+ }
+
+ @Override
+ public Resource create(DeclarativeConfigProperties config) {
+ ConfigProperties envConfigProperties =
+ DefaultConfigProperties.create(Collections.emptyMap(), config.getComponentLoader());
+
+ Map configPropertiesMap = new HashMap<>();
+ configPropertiesMap.put(ENTITIES_PROPERTY, envConfigProperties.getString(ENTITIES_PROPERTY));
+ configPropertiesMap.put(EXPERIMENTAL_ENTITIES_ENABLED, "true");
+
+ return EnvironmentResource.otelEntitiesResource(
+ DefaultConfigProperties.createFromMap(configPropertiesMap));
+ }
+}
diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ResourceFactory.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ResourceFactory.java
index 9f670487c28..26e3a1dfc5e 100644
--- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ResourceFactory.java
+++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ResourceFactory.java
@@ -5,10 +5,9 @@
package io.opentelemetry.sdk.autoconfigure.declarativeconfig;
-import static io.opentelemetry.sdk.autoconfigure.declarativeconfig.EnvironmentResource.ATTRIBUTE_PROPERTY;
-import static io.opentelemetry.sdk.autoconfigure.declarativeconfig.EnvironmentResource.createEnvironmentResource;
+import static io.opentelemetry.sdk.autoconfigure.declarativeconfig.EnvironmentResource.RESOURCE_ATTRIBUTES_PROPERTY;
+import static io.opentelemetry.sdk.autoconfigure.declarativeconfig.EnvironmentResource.otelResourceAttributesResource;
-import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.AttributeNameValueModel;
import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.IncludeExcludeModel;
import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.ResourceModel;
@@ -52,20 +51,24 @@ public Resource create(ResourceModel model, DeclarativeConfigContext context) {
attributesIncludeExcludeModel == null
? ResourceFactory::matchAll
: IncludeExcludeFactory.getInstance().create(attributesIncludeExcludeModel, context);
- Attributes filteredDetectedAttributes =
- detectedResourceBuilder.build().getAttributes().toBuilder()
+ Resource filteredDetectedResource =
+ detectedResourceBuilder
.removeIf(attributeKey -> !detectorAttributeFilter.test(attributeKey.getKey()))
.build();
- builder.putAll(filteredDetectedAttributes);
+ builder.putAll(filteredDetectedResource);
+
+ if (detectionModel.getEntitiesEnabled() == null || !detectionModel.getEntitiesEnabled()) {
+ builder = EnvironmentResource.eraseEntities(builder.build()).toBuilder();
+ }
}
String attributeList = model.getAttributesList();
if (attributeList != null) {
builder.putAll(
- createEnvironmentResource(
+ otelResourceAttributesResource(
DefaultConfigProperties.createFromMap(
- Collections.singletonMap(ATTRIBUTE_PROPERTY, attributeList))));
+ Collections.singletonMap(RESOURCE_ATTRIBUTES_PROPERTY, attributeList))));
}
List attributeNameValueModel = model.getAttributes();
diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ServiceResourceDetector.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ServiceResourceDetector.java
index f09d44a6275..a370cdc6e3c 100644
--- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ServiceResourceDetector.java
+++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ServiceResourceDetector.java
@@ -5,24 +5,24 @@
package io.opentelemetry.sdk.autoconfigure.declarativeconfig;
-import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider;
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
+import io.opentelemetry.sdk.common.internal.SemConvAttributes;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.resources.ResourceBuilder;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import io.opentelemetry.sdk.resources.internal.EntityUtil;
import java.util.Collections;
import java.util.UUID;
public class ServiceResourceDetector implements ComponentProvider {
- private static final AttributeKey SERVICE_NAME = AttributeKey.stringKey("service.name");
- private static final AttributeKey SERVICE_INSTANCE_ID =
- AttributeKey.stringKey("service.instance.id");
-
// multiple calls to this resource provider should return the same value
- private static final String RANDOM_SERVICE_INSTANCE_ID = UUID.randomUUID().toString();
+ // visible for testing
+ static final String RANDOM_SERVICE_INSTANCE_ID = UUID.randomUUID().toString();
@Override
public Class getType() {
@@ -40,12 +40,16 @@ public Resource create(DeclarativeConfigProperties config) {
ConfigProperties properties =
DefaultConfigProperties.create(Collections.emptyMap(), config.getComponentLoader());
- String serviceName = properties.getString("otel.service.name");
- if (serviceName != null) {
- builder.put(SERVICE_NAME, serviceName).build();
- }
- builder.put(SERVICE_INSTANCE_ID, RANDOM_SERVICE_INSTANCE_ID);
+ builder.putAll(EnvironmentResource.otelServiceNameResource(properties));
+
+ Entity serviceInstanceEntity =
+ Entity.builder(
+ SemConvAttributes.SERVICE_INSTANCE_TYPE,
+ Attributes.of(SemConvAttributes.SERVICE_INSTANCE_ID, RANDOM_SERVICE_INSTANCE_ID))
+ .setSchemaUrl(SemConvAttributes.SCHEMA_URL_V1_40_0)
+ .build();
+ EntityUtil.addEntity(builder, serviceInstanceEntity);
return builder.build();
}
diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/MetricProducerModel.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/MetricProducerModel.java
index 9569bf4cb3d..3377ad51b5f 100644
--- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/MetricProducerModel.java
+++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/MetricProducerModel.java
@@ -27,6 +27,16 @@ public class MetricProducerModel {
/**
* Configure metric producer to be opencensus.
*
+ *
**Deprecated** as of v1.2.0, may be removed in v2.0.0. The OpenCensus
+ *
+ *
compatibility specification it relies on was deprecated in
+ *
+ *
SDKs MAY continue to support this entry for backwards compatibility;
+ *
+ *
new configurations SHOULD NOT use it.
+ *
*
If omitted, ignore.
*/
@JsonProperty("opencensus")
diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/OpenCensusMetricProducerModel.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/OpenCensusMetricProducerModel.java
index d949facf6a3..1aa36fcb57c 100644
--- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/OpenCensusMetricProducerModel.java
+++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/OpenCensusMetricProducerModel.java
@@ -10,6 +10,12 @@
import javax.annotation.Generated;
import javax.annotation.Nullable;
+/**
+ * **Deprecated** as of v1.2.0, may be removed in v2.0.0. The OpenCensus compatibility specification
+ * it relies on was deprecated in
+ * https://github.com/open-telemetry/opentelemetry-specification/pull/5138. See also the [OpenCensus
+ * sunset announcement](https://opentelemetry.io/blog/2023/sunsetting-opencensus/).
+ */
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({})
@Generated("jsonschema2pojo")
diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/internal/ExperimentalResourceDetectionModel.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/internal/ExperimentalResourceDetectionModel.java
index 4ddf3d90c48..4ce336ee061 100644
--- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/internal/ExperimentalResourceDetectionModel.java
+++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/internal/ExperimentalResourceDetectionModel.java
@@ -14,12 +14,13 @@
import javax.annotation.Nullable;
@JsonInclude(JsonInclude.Include.NON_NULL)
-@JsonPropertyOrder({"attributes", "detectors"})
+@JsonPropertyOrder({"attributes", "detectors", "entities_enabled"})
@Generated("jsonschema2pojo")
public class ExperimentalResourceDetectionModel {
@Nullable private IncludeExcludeModel attributes;
@Nullable private List detectors;
+ @Nullable private Boolean entitiesEnabled;
/**
* Configure attributes provided by resource detectors.
@@ -59,6 +60,29 @@ public ExperimentalResourceDetectionModel withDetectors(
return this;
}
+ /**
+ * Configure whether resource detectors emit entity-aware resources.
+ *
+ *
Resource detectors always produce entity-aware output internally. When this option is
+ *
+ *
not true, the SDK erases entity information from detector results before merging
+ *
+ *
them into the resource.
+ *
+ *
If omitted or null, false is used.
+ */
+ @JsonProperty("entities_enabled")
+ @Nullable
+ public Boolean getEntitiesEnabled() {
+ return entitiesEnabled;
+ }
+
+ @JsonProperty("entities_enabled")
+ public ExperimentalResourceDetectionModel withEntitiesEnabled(Boolean entitiesEnabled) {
+ this.entitiesEnabled = entitiesEnabled;
+ return this;
+ }
+
@Override
public String toString() {
return "ExperimentalResourceDetectionModel{"
@@ -66,6 +90,8 @@ public String toString() {
+ attributes
+ ", detectors="
+ detectors
+ + ", entitiesEnabled="
+ + entitiesEnabled
+ "}";
}
@@ -76,6 +102,8 @@ public int hashCode() {
h ^= (this.attributes == null) ? 0 : this.attributes.hashCode();
h *= 1000003;
h ^= (this.detectors == null) ? 0 : this.detectors.hashCode();
+ h *= 1000003;
+ h ^= (this.entitiesEnabled == null) ? 0 : this.entitiesEnabled.hashCode();
return h;
}
@@ -91,7 +119,10 @@ public boolean equals(@Nullable Object o) {
: this.attributes.equals(that.attributes))
&& (this.detectors == null
? that.detectors == null
- : this.detectors.equals(that.detectors));
+ : this.detectors.equals(that.detectors))
+ && (this.entitiesEnabled == null
+ ? that.entitiesEnabled == null
+ : this.entitiesEnabled.equals(that.entitiesEnabled));
}
return false;
}
diff --git a/sdk-extensions/declarative-config/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider b/sdk-extensions/declarative-config/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider
index fcfd61599d1..596144da6a4 100644
--- a/sdk-extensions/declarative-config/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider
+++ b/sdk-extensions/declarative-config/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider
@@ -1 +1,2 @@
+io.opentelemetry.sdk.autoconfigure.declarativeconfig.EntitiesEnvResourceDetector
io.opentelemetry.sdk.autoconfigure.declarativeconfig.ServiceResourceDetector
diff --git a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationCreateTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationCreateTest.java
index 053953a5a94..63e65ab8181 100644
--- a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationCreateTest.java
+++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationCreateTest.java
@@ -183,15 +183,13 @@ void create_ModelCustomizer() {
ComponentLoader.forClassLoader(
DeclarativeConfigurationCreateTest.class.getClassLoader()))
.getSdk();
- assertThat(sdk.toString())
- .contains(
- "resource=Resource{schemaUrl=null, attributes={"
- + "color=\"blue\", "
- + "foo=\"bar\", "
- + "service.name=\"unknown_service:java\", "
- + "telemetry.sdk.language=\"java\", "
- + "telemetry.sdk.name=\"opentelemetry\", "
- + "telemetry.sdk.version=\"");
+ String sdkStr = sdk.toString();
+ assertThat(sdkStr).contains("color=\"blue\"");
+ assertThat(sdkStr).contains("foo=\"bar\"");
+ assertThat(sdkStr).contains("service.name=\"unknown_service:java\"");
+ assertThat(sdkStr).contains("telemetry.sdk.language=\"java\"");
+ assertThat(sdkStr).contains("telemetry.sdk.name=\"opentelemetry\"");
+ assertThat(sdkStr).contains("telemetry.sdk.version=\"");
}
@Test
diff --git a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ResourceFactoryTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ResourceFactoryTest.java
index 220b774f2b2..d5b12fb9992 100644
--- a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ResourceFactoryTest.java
+++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ResourceFactoryTest.java
@@ -5,9 +5,11 @@
package io.opentelemetry.sdk.autoconfigure.declarativeconfig;
+import static io.opentelemetry.api.common.AttributeKey.stringKey;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.incubator.config.DeclarativeConfigException;
import io.opentelemetry.common.ComponentLoader;
import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.AttributeNameValueModel;
@@ -16,9 +18,12 @@
import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ExperimentalResourceDetectionModel;
import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ExperimentalResourceDetectorModel;
import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import io.opentelemetry.sdk.resources.internal.EntityUtil;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.junit.jupiter.params.ParameterizedTest;
@@ -32,14 +37,21 @@ class ResourceFactoryTest {
@ParameterizedTest
@MethodSource("createArgs")
- void create(ResourceModel model, Resource expectedResource) {
- assertThat(ResourceFactory.getInstance().create(model, context)).isEqualTo(expectedResource);
+ void create(
+ Map systemProperties, ResourceModel model, Resource expectedResource) {
+ systemProperties.forEach(System::setProperty);
+ try {
+ assertThat(ResourceFactory.getInstance().create(model, context)).isEqualTo(expectedResource);
+ } finally {
+ systemProperties.forEach((key, unused) -> System.clearProperty(key));
+ }
}
private static Stream createArgs() {
return Stream.of(
Arguments.argumentSet(
"with attributes",
+ Collections.emptyMap(),
new ResourceModel()
.withAttributes(
Arrays.asList(
@@ -55,12 +67,135 @@ private static Stream createArgs() {
.build()),
Arguments.argumentSet(
"with schema url",
+ Collections.emptyMap(),
new ResourceModel().withSchemaUrl("http://foo"),
Resource.getDefault().toBuilder().setSchemaUrl("http://foo").build()),
Arguments.argumentSet(
"with attributes list",
+ Collections.emptyMap(),
new ResourceModel().withAttributesList("key1=val1,key2=val2"),
- Resource.getDefault().toBuilder().put("key1", "val1").put("key2", "val2").build()));
+ Resource.getDefault().toBuilder().put("key1", "val1").put("key2", "val2").build()),
+ Arguments.argumentSet(
+ "env detector - happy path",
+ Collections.singletonMap(
+ "otel.entities",
+ "process{process.pid=1234}[process.executable.name=java]@http://schema;host{host.id=myhost}"),
+ new ResourceModel()
+ .withDetectionDevelopment(
+ new ExperimentalResourceDetectionModel()
+ .withEntitiesEnabled(true)
+ .withDetectors(
+ Collections.singletonList(
+ new ExperimentalResourceDetectorModel()
+ .withAdditionalProperty("env", null)))),
+ Resource.getDefault().toBuilder()
+ .putAll(
+ EntityUtil.createResource(
+ Arrays.asList(
+ Entity.builder(
+ "process", Attributes.of(stringKey("process.pid"), "1234"))
+ .setSchemaUrl("http://schema")
+ .setDescription(
+ Attributes.of(stringKey("process.executable.name"), "java"))
+ .build(),
+ Entity.builder("host", Attributes.of(stringKey("host.id"), "myhost"))
+ .build())))
+ .build()),
+ Arguments.argumentSet(
+ "env detector - empty",
+ Collections.singletonMap("otel.entities", ""),
+ new ResourceModel()
+ .withDetectionDevelopment(
+ new ExperimentalResourceDetectionModel()
+ .withEntitiesEnabled(true)
+ .withDetectors(
+ Collections.singletonList(
+ new ExperimentalResourceDetectorModel()
+ .withAdditionalProperty("env", null)))),
+ Resource.getDefault()),
+ Arguments.argumentSet(
+ "env detector - entities disabled",
+ Collections.singletonMap(
+ "otel.entities",
+ "process{process.pid=1234}[process.executable.name=java]@http://schema;host{host.id=myhost}"),
+ new ResourceModel()
+ .withDetectionDevelopment(
+ new ExperimentalResourceDetectionModel()
+ .withEntitiesEnabled(false)
+ .withDetectors(
+ Collections.singletonList(
+ new ExperimentalResourceDetectorModel()
+ .withAdditionalProperty("env", null)))),
+ Resource.getDefault().toBuilder()
+ .put("process.pid", "1234")
+ .put("process.executable.name", "java")
+ .put("host.id", "myhost")
+ .build()),
+ Arguments.argumentSet(
+ "service detector - happy path",
+ Collections.singletonMap("otel.service.name", "test"),
+ new ResourceModel()
+ .withDetectionDevelopment(
+ new ExperimentalResourceDetectionModel()
+ .withEntitiesEnabled(true)
+ .withDetectors(
+ Collections.singletonList(
+ new ExperimentalResourceDetectorModel()
+ .withAdditionalProperty("service", null)))),
+ Resource.getDefault().toBuilder()
+ .putAll(
+ EntityUtil.createResource(
+ Arrays.asList(
+ Entity.builder(
+ "service", Attributes.of(stringKey("service.name"), "test"))
+ .setSchemaUrl("https://opentelemetry.io/schemas/1.40.0")
+ .build(),
+ Entity.builder(
+ "service.instance",
+ Attributes.of(
+ stringKey("service.instance.id"),
+ ServiceResourceDetector.RANDOM_SERVICE_INSTANCE_ID))
+ .setSchemaUrl("https://opentelemetry.io/schemas/1.40.0")
+ .build())))
+ .build()),
+ Arguments.argumentSet(
+ "service detector - no otel.service.name",
+ Collections.emptyMap(),
+ new ResourceModel()
+ .withDetectionDevelopment(
+ new ExperimentalResourceDetectionModel()
+ .withEntitiesEnabled(true)
+ .withDetectors(
+ Collections.singletonList(
+ new ExperimentalResourceDetectorModel()
+ .withAdditionalProperty("service", null)))),
+ Resource.getDefault().toBuilder()
+ .putAll(
+ EntityUtil.createResource(
+ Collections.singletonList(
+ Entity.builder(
+ "service.instance",
+ Attributes.of(
+ stringKey("service.instance.id"),
+ ServiceResourceDetector.RANDOM_SERVICE_INSTANCE_ID))
+ .setSchemaUrl("https://opentelemetry.io/schemas/1.40.0")
+ .build())))
+ .build()),
+ Arguments.argumentSet(
+ "service detector - entities disabled",
+ Collections.singletonMap("otel.service.name", "test"),
+ new ResourceModel()
+ .withDetectionDevelopment(
+ new ExperimentalResourceDetectionModel()
+ .withEntitiesEnabled(false)
+ .withDetectors(
+ Collections.singletonList(
+ new ExperimentalResourceDetectorModel()
+ .withAdditionalProperty("service", null)))),
+ Resource.getDefault().toBuilder()
+ .put("service.name", "test")
+ .put("service.instance.id", ServiceResourceDetector.RANDOM_SERVICE_INSTANCE_ID)
+ .build()));
}
@ParameterizedTest
diff --git a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ServiceResourceDetectorTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ServiceResourceDetectorTest.java
deleted file mode 100644
index 8513423a0a9..00000000000
--- a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ServiceResourceDetectorTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.sdk.autoconfigure.declarativeconfig;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatCode;
-
-import io.opentelemetry.api.common.AttributeKey;
-import io.opentelemetry.api.common.Attributes;
-import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
-import io.opentelemetry.sdk.resources.Resource;
-import java.util.Objects;
-import java.util.UUID;
-import org.junit.jupiter.api.Test;
-import org.junitpioneer.jupiter.ClearSystemProperty;
-
-class ServiceResourceDetectorTest {
-
- @Test
- void getTypeAndName() {
- ServiceResourceDetector detector = new ServiceResourceDetector();
-
- assertThat(detector.getType()).isEqualTo(Resource.class);
- assertThat(detector.getName()).isEqualTo("service");
- }
-
- @Test
- @ClearSystemProperty(key = "otel.service.name")
- void create_SystemPropertySet() {
- System.setProperty("otel.service.name", "test");
-
- assertThat(new ServiceResourceDetector().create(DeclarativeConfigProperties.empty()))
- .satisfies(
- resource -> {
- Attributes attributes = resource.getAttributes();
- assertThat(attributes.get(AttributeKey.stringKey("service.name"))).isEqualTo("test");
- assertThatCode(
- () ->
- UUID.fromString(
- Objects.requireNonNull(
- attributes.get(AttributeKey.stringKey("service.instance.id")))))
- .doesNotThrowAnyException();
- });
- }
-
- @Test
- void create_NoSystemProperty() {
- assertThat(new ServiceResourceDetector().create(DeclarativeConfigProperties.empty()))
- .satisfies(
- resource -> {
- Attributes attributes = resource.getAttributes();
- assertThat(attributes.get(AttributeKey.stringKey("service.name"))).isNull();
- assertThatCode(
- () ->
- UUID.fromString(
- Objects.requireNonNull(
- attributes.get(AttributeKey.stringKey("service.instance.id")))))
- .doesNotThrowAnyException();
- });
- }
-}
diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceInstanceIdResourceProvider.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceInstanceIdResourceProvider.java
index 97e84f3c686..0e9de5efbb8 100644
--- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceInstanceIdResourceProvider.java
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceInstanceIdResourceProvider.java
@@ -5,12 +5,14 @@
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.autoconfigure.spi.ResourceProvider;
import io.opentelemetry.sdk.autoconfigure.spi.internal.ConditionalResourceProvider;
+import io.opentelemetry.sdk.common.internal.SemConvAttributes;
import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.resources.internal.Entity;
+import io.opentelemetry.sdk.resources.internal.EntityUtil;
import java.util.UUID;
/**
@@ -19,23 +21,27 @@
*/
public final class ServiceInstanceIdResourceProvider implements ConditionalResourceProvider {
- public static final AttributeKey SERVICE_INSTANCE_ID =
- AttributeKey.stringKey("service.instance.id");
-
// multiple calls to this resource provider should return the same value
- private static final Resource RANDOM =
- Resource.create(Attributes.of(SERVICE_INSTANCE_ID, UUID.randomUUID().toString()));
+ private static final String RANDOM_SERVICE_INSTANCE_ID = UUID.randomUUID().toString();
static final int ORDER = Integer.MAX_VALUE;
@Override
public Resource createResource(ConfigProperties config) {
- return RANDOM;
+ return EntityUtil.addEntity(
+ Resource.builder(),
+ Entity.builder(
+ SemConvAttributes.SERVICE_INSTANCE_TYPE,
+ Attributes.of(
+ SemConvAttributes.SERVICE_INSTANCE_ID, RANDOM_SERVICE_INSTANCE_ID))
+ .setSchemaUrl(SemConvAttributes.SCHEMA_URL_V1_40_0)
+ .build())
+ .build();
}
@Override
public boolean shouldApply(ConfigProperties config, Resource existing) {
- return existing.getAttribute(SERVICE_INSTANCE_ID) == null;
+ return existing.getAttribute(SemConvAttributes.SERVICE_INSTANCE_ID) == null;
}
@Override
diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceInstanceIdResourceProviderTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceInstanceIdResourceProviderTest.java
index b359b9fbcf0..c51d134d486 100644
--- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceInstanceIdResourceProviderTest.java
+++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceInstanceIdResourceProviderTest.java
@@ -11,6 +11,7 @@
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
+import io.opentelemetry.sdk.common.internal.SemConvAttributes;
import io.opentelemetry.sdk.resources.Resource;
import java.util.Collections;
import java.util.Map;
@@ -32,8 +33,7 @@ void createResource(String expectedValue, Map attributes) {
Resource resource =
provider.shouldApply(config, existing) ? provider.createResource(config) : Resource.empty();
- String actual =
- resource.getAttributes().get(ServiceInstanceIdResourceProvider.SERVICE_INSTANCE_ID);
+ String actual = resource.getAttributes().get(SemConvAttributes.SERVICE_INSTANCE_ID);
if ("random".equals(expectedValue)) {
assertThat(actual).isNotNull();
} else {
diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/SemConvAttributes.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/SemConvAttributes.java
index 7f21cd94551..0072f3d462e 100644
--- a/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/SemConvAttributes.java
+++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/SemConvAttributes.java
@@ -19,6 +19,22 @@ public class SemConvAttributes {
private SemConvAttributes() {}
+ // Schema url
+
+ // TODO(jack-berg): decide when and why we change schema url
+ public static final String SCHEMA_URL_V1_40_0 = "https://opentelemetry.io/schemas/1.40.0";
+
+ // Entity types
+
+ public static final String SERVICE_TYPE = "service";
+ public static final String SERVICE_INSTANCE_TYPE = "service.instance";
+
+ // Attributes
+
+ public static final AttributeKey SERVICE_NAME = AttributeKey.stringKey("service.name");
+ public static final AttributeKey SERVICE_INSTANCE_ID =
+ AttributeKey.stringKey("service.instance.id");
+
public static final AttributeKey OTEL_COMPONENT_TYPE =
AttributeKey.stringKey("otel.component.type");
public static final AttributeKey OTEL_COMPONENT_NAME =
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 baebb3cce38..fb5609961cf 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,36 @@ 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"));
+ // Memoize the full set of attributes
+ AttributesBuilder fullAttributes = Attributes.builder();
+ entities.forEach(
+ e -> {
+ fullAttributes.putAll(e.getId());
+ fullAttributes.putAll(e.getDescription());
+ });
+ // In merge rules, raw comes last, so we return these last.
+ fullAttributes.putAll(attributes);
+ return new AutoValue_Resource(schemaUrl, entities, fullAttributes.build());
}
/**
@@ -121,6 +138,30 @@ 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.
+ */
+ final Attributes getUnassociatedAttributes() {
+ AttributesBuilder unassociatedAttributes = getAttributes().toBuilder();
+ unassociatedAttributes.removeIf(
+ key ->
+ getEntities().stream()
+ .anyMatch(
+ entity ->
+ entity.getId().get(key) != null
+ || entity.getDescription().get(key) != null));
+ return unassociatedAttributes.build();
+ }
+
+ /**
+ * Returns a collection of associated entities.
+ *
+ * @return a collection of entities.
+ */
+ abstract Collection getEntities();
+
/**
* Returns a map of attributes that describe the resource.
*
@@ -146,63 +187,7 @@ public T getAttribute(AttributeKey key) {
* @return the newly merged {@code Resource}.
*/
public Resource merge(@Nullable Resource other) {
- if (other == null || other.equals(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 b01437bd6fc..7fd18735fe0 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,13 @@
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.List;
+import java.util.Set;
import java.util.function.Predicate;
+import java.util.stream.Collectors;
import javax.annotation.Nullable;
/**
@@ -20,6 +26,7 @@
public class ResourceBuilder {
private final AttributesBuilder attributesBuilder = Attributes.builder();
+ private final List entities = new ArrayList<>();
@Nullable private String schemaUrl;
/**
@@ -169,7 +176,11 @@ public ResourceBuilder putAll(Attributes attributes) {
/** Puts all attributes from {@link Resource} into this. */
public ResourceBuilder putAll(Resource resource) {
if (resource != null) {
- attributesBuilder.putAll(resource.getAttributes());
+ // Preserve entities when merging resources.
+ entities.addAll(resource.getEntities());
+ // Only pull "raw" attributes - we expect entities to carry some of the full
+ // set.
+ attributesBuilder.putAll(resource.getUnassociatedAttributes());
}
return this;
}
@@ -194,6 +205,24 @@ public ResourceBuilder setSchemaUrl(String schemaUrl) {
/** 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 addEntity(Entity e) {
+ this.entities.add(e);
+ 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..49eec3563c9
--- /dev/null
+++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/AttributeCheckUtil.java
@@ -0,0 +1,65 @@
+/*
+ * 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 entity attributes.
+ *
+ *
This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ *
+ * @see SdkEntityBuilder
+ */
+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..694ebeac4bb
--- /dev/null
+++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/Entity.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+// TODO(jack-berg): Should Entity/EntityBuilder be replaced with autovalue equivalent
+@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.
+ * @param id the identifying attributes of this entity
+ */
+ static EntityBuilder builder(String entityType, Attributes id) {
+ return new SdkEntityBuilder(entityType, id);
+ }
+}
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..55d90f01f5c
--- /dev/null
+++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityBuilder.java
@@ -0,0 +1,36 @@
+/*
+ * 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 setDescription(Attributes description);
+
+ /** 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..ecc6c0cc5bd
--- /dev/null
+++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java
@@ -0,0 +1,282 @@
+/*
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+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);
+ method.setAccessible(true);
+ Object result = method.invoke(null, attributes, schemaUrl, entities);
+ return (Resource) result;
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ throw new IllegalStateException("Error calling create on Resource", e);
+ }
+ }
+
+ /** 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("addEntity", Entity.class);
+ method.setAccessible(true);
+ method.invoke(rb, e);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
+ throw new IllegalStateException("Error calling addEntity on ResourceBuilder", ex);
+ }
+ return rb;
+ }
+
+ /**
+ * Returns a collection of associated entities.
+ *
+ * @return a collection of entities.
+ */
+ @SuppressWarnings("unchecked")
+ public static Collection getEntities(Resource r) {
+ try {
+ Method method = Resource.class.getDeclaredMethod("getEntities");
+ method.setAccessible(true);
+ return (Collection) method.invoke(r);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ throw new IllegalStateException("Error calling getEntities on Resource", e);
+ }
+ }
+
+ /**
+ * Returns a map of attributes that describe the resource, not associated with entities.
+ *
+ * @return a map of attributes.
+ */
+ public static Attributes getUnassociatedAttributes(Resource r) {
+ try {
+ Method method = Resource.class.getDeclaredMethod("getUnassociatedAttributes");
+ method.setAccessible(true);
+ return (Attributes) method.invoke(r);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ throw new IllegalStateException("Error calling getUnassociatedAttributes on Resource", e);
+ }
+ }
+
+ /** 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 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.
+ List 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