From 7f67a895beb5347f82baeb4b76b56c3ba06babc1 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Fri, 24 Apr 2026 12:41:34 +0000 Subject: [PATCH 01/22] Start the reboot of entities SDK for Java, this time something mergable. --- .../internal/otlp/EntityRefMarshaler.java | 83 ++++ .../internal/otlp/EntityRefMarshalerTest.java | 87 ++++ .../extension/incubator/resources/Entity.java | 19 + .../incubator/resources/EntityBuilder.java | 24 + .../incubator/resources/EntityDetector.java | 24 + .../resources/EnvEntityDetector.java | 412 ++++++++++++++++++ .../internal/ExtendedEntityUtil.java | 40 ++ .../resources/internal/SdkEntity.java | 74 ++++ .../resources/internal/SdkEntityBuilder.java | 60 +++ .../resources/EnvEntityDetectorTest.java | 153 +++++++ .../opentelemetry/sdk/resources/Resource.java | 128 +++--- .../sdk/resources/ResourceBuilder.java | 35 +- .../internal/AttributeCheckUtil.java | 63 +++ .../sdk/resources/internal/Entity.java | 73 ++++ .../sdk/resources/internal/EntityBuilder.java | 44 ++ .../sdk/resources/internal/EntityUtil.java | 311 +++++++++++++ .../internal/RawAttributeMergeResult.java | 32 ++ .../sdk/resources/internal/SdkEntity.java | 48 ++ .../resources/internal/SdkEntityBuilder.java | 62 +++ .../sdk/resources/ResourceTest.java | 9 + .../sdk/logs/SdkLoggerProviderTest.java | 12 +- .../sdk/trace/SdkSpanBuilderTest.java | 4 +- 22 files changed, 1712 insertions(+), 85 deletions(-) create mode 100644 exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshaler.java create mode 100644 exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshalerTest.java create mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/Entity.java create mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityBuilder.java create mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java create mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java create mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java create mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntity.java create mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntityBuilder.java create mode 100644 sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/AttributeCheckUtil.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/Entity.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityBuilder.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/RawAttributeMergeResult.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntity.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntityBuilder.java diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshaler.java new file mode 100644 index 00000000000..e77ab7f8308 --- /dev/null +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshaler.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal.otlp; + +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.exporter.internal.marshal.MarshalerUtil; +import io.opentelemetry.exporter.internal.marshal.MarshalerWithSize; +import io.opentelemetry.exporter.internal.marshal.Serializer; +import io.opentelemetry.proto.common.v1.internal.EntityRef; +import io.opentelemetry.sdk.resources.internal.Entity; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import javax.annotation.Nullable; + +/** + * A Marshaler of {@link io.opentelemetry.sdk.resources.internal.Entity}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +final class EntityRefMarshaler extends MarshalerWithSize { + @Nullable private final byte[] schemaUrlUtf8; + private final byte[] typeUtf8; + private final byte[][] idKeysUtf8; + private final byte[][] descriptionKeysUtf8; + + @Override + protected void writeTo(Serializer output) throws IOException { + if (schemaUrlUtf8 != null) { + output.writeString(EntityRef.SCHEMA_URL, schemaUrlUtf8); + } + output.writeString(EntityRef.TYPE, typeUtf8); + output.writeRepeatedString(EntityRef.ID_KEYS, idKeysUtf8); + output.writeRepeatedString(EntityRef.DESCRIPTION_KEYS, descriptionKeysUtf8); + } + + /** Consttructs an entity reference marshaler from a full entity. */ + static EntityRefMarshaler createForEntity(Entity e) { + byte[] schemaUrlUtf8 = null; + if (!StringUtils.isNullOrEmpty(e.getSchemaUrl())) { + schemaUrlUtf8 = e.getSchemaUrl().getBytes(StandardCharsets.UTF_8); + } + return new EntityRefMarshaler( + schemaUrlUtf8, + e.getType().getBytes(StandardCharsets.UTF_8), + e.getId().asMap().keySet().stream() + .map(key -> key.getKey().getBytes(StandardCharsets.UTF_8)) + .toArray(byte[][]::new), + e.getDescription().asMap().keySet().stream() + .map(key -> key.getKey().getBytes(StandardCharsets.UTF_8)) + .toArray(byte[][]::new)); + } + + private EntityRefMarshaler( + @Nullable byte[] schemaUrlUtf8, + byte[] typeUtf8, + byte[][] idKeysUtf8, + byte[][] descriptionKeysUtf8) { + super(calculateSize(schemaUrlUtf8, typeUtf8, idKeysUtf8, descriptionKeysUtf8)); + this.schemaUrlUtf8 = schemaUrlUtf8; + this.typeUtf8 = typeUtf8; + this.idKeysUtf8 = idKeysUtf8; + this.descriptionKeysUtf8 = descriptionKeysUtf8; + } + + private static int calculateSize( + @Nullable byte[] schemaUrlUtf8, + byte[] typeUtf8, + byte[][] idKeysUtf8, + byte[][] descriptionKeysUtf8) { + int size = 0; + if (schemaUrlUtf8 != null) { + size += MarshalerUtil.sizeBytes(EntityRef.SCHEMA_URL, schemaUrlUtf8); + } + size += MarshalerUtil.sizeBytes(EntityRef.TYPE, typeUtf8); + MarshalerUtil.sizeRepeatedString(EntityRef.ID_KEYS, idKeysUtf8); + MarshalerUtil.sizeRepeatedString(EntityRef.DESCRIPTION_KEYS, descriptionKeysUtf8); + return size; + } +} diff --git a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshalerTest.java b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshalerTest.java new file mode 100644 index 00000000000..b5e8cad39f5 --- /dev/null +++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshalerTest.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal.otlp; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.exporter.internal.marshal.Marshaler; +import io.opentelemetry.proto.common.v1.EntityRef; +import io.opentelemetry.sdk.resources.internal.Entity; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class EntityRefMarshalerTest { + @Test + void toEntityRefs() { + Entity e = + Entity.builder("test") + .setSchemaUrl("test-url") + .withDescription(Attributes.builder().put("desc.key", "desc.value").build()) + .withId(Attributes.builder().put("id.key", "id.value").build()) + .build(); + EntityRef proto = parse(EntityRef.getDefaultInstance(), EntityRefMarshaler.createForEntity(e)); + assertThat(proto.getType()).isEqualTo("test"); + assertThat(proto.getSchemaUrl()).isEqualTo("test-url"); + assertThat(proto.getIdKeysList()).containsExactly("id.key"); + assertThat(proto.getDescriptionKeysList()).containsExactly("desc.key"); + } + + @SuppressWarnings("unchecked") + private static T parse(T prototype, Marshaler marshaler) { + byte[] serialized = toByteArray(marshaler); + T result; + try { + result = (T) prototype.newBuilderForType().mergeFrom(serialized).build(); + } catch (InvalidProtocolBufferException e) { + throw new UncheckedIOException(e); + } + // Our marshaler should produce the exact same length of serialized output (for example, field + // default values are not outputted), so we check that here. The output itself may have slightly + // different ordering, mostly due to the way we don't output oneof values in field order all the + // tieme. If the lengths are equal and the resulting protos are equal, the marshaling is + // guaranteed to be valid. + assertThat(result.getSerializedSize()).isEqualTo(serialized.length); + + // Compare JSON + String json = toJson(marshaler); + Message.Builder builder = prototype.newBuilderForType(); + try { + JsonFormat.parser().merge(json, builder); + } catch (InvalidProtocolBufferException e) { + throw new UncheckedIOException(e); + } + assertThat(builder.build()).isEqualTo(result); + + return result; + } + + private static byte[] toByteArray(Marshaler marshaler) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + marshaler.writeBinaryTo(bos); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return bos.toByteArray(); + } + + private static String toJson(Marshaler marshaler) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + marshaler.writeJsonTo(bos); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return new String(bos.toByteArray(), StandardCharsets.UTF_8); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/Entity.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/Entity.java new file mode 100644 index 00000000000..b5468e14c7a --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/Entity.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources; + +import io.opentelemetry.sdk.extension.incubator.resources.internal.SdkEntityBuilder; + +/** An instance of an Entity. */ +public interface Entity { + /** Constructs a new builder for creating Entities. */ + static EntityBuilder builder(String entityType) { + return new SdkEntityBuilder(entityType); + } + + /** Converts this entity to a builder. */ + EntityBuilder toBuilder(); +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityBuilder.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityBuilder.java new file mode 100644 index 00000000000..fc95ec47210 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityBuilder.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources; + +import io.opentelemetry.api.common.Attributes; + +/** A builder of {@link Entity}. */ +public interface EntityBuilder { + + /** Sets the schema_url of the Entity. */ + EntityBuilder setSchemaUrl(String schemaUrl); + + /** Sets the identity of the Entity. */ + EntityBuilder setIdentity(Attributes identity); + + /** Sets the description of the Entity. */ + EntityBuilder setDescription(Attributes description); + + /** Builds an entity. */ + Entity build(); +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java new file mode 100644 index 00000000000..d6696d6643a --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.Ordered; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collection; + +/** + * A service provider interface (SPI) for providing a collection of {@link Entity} that are merged + * into the {@link Resource#getDefault() default resource}. + */ +public interface EntityDetector extends Ordered { + /** + * Detects entities based on the configuration. + * + * @param config the configuration to use for detection + */ + Collection detect(ConfigProperties config); +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java new file mode 100644 index 00000000000..677f006644e --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java @@ -0,0 +1,412 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/* + * An EntityDetector that parses the OTEL_ENTITIES environment variable. + * + * See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/entities/entity-propagation.md + * for more information about the OTEL_ENTITIES environment variable. + */ +class EnvEntityDetector implements EntityDetector { + + private static final Logger logger = Logger.getLogger(EnvEntityDetector.class.getName()); + private static final String PROPERTY_KEY = "otel.entities"; + + @Override + public Collection detect(ConfigProperties config) { + String entitiesStr = config.getString(PROPERTY_KEY); + if (entitiesStr == null || entitiesStr.isEmpty()) { + return new ArrayList<>(); + } + + return new EntityParser(entitiesStr).parse(); + } + + /** + * Segment class represents a start/stop endpoint within a source String. + * + *

+ * A segment can be used to extract a substring from the source string *without + * interning* the + * string into the JDK's string tables. This can dramatically reduce allocations + * when parsing. + * Segment is intended to provide a similar interface to using {@code substring} + * on {@code + * String}. + * + *

+ * Additionally, a Segment can be denoted as URL-encoded (e.g. using '%20' to + * denote a + * character.) In this case, the segment will be decoded when extracting its + * String value. + */ + private static final class Segment { + private final String source; + private int start; + private int end; + private boolean needsDecoding; + + Segment(String source) { + this.source = source; + reset(0); + } + + /** Reset the segment for the next use, starting from the given start index. */ + void reset(int start) { + this.start = start; + this.end = start; + this.needsDecoding = false; + } + + /** Update the end of the segment (non-inclusive). */ + void markEnd(int end) { + this.end = end; + } + + /** + * Denotes that the segment is URL encoded, and should be decoded when calling + * {@code + * getValue()}. + */ + void markNeedsDecoding() { + this.needsDecoding = true; + } + + /** Return true if the segment is empty. */ + boolean isEmpty() { + return start >= end; + } + + /** + * Returns the string represented by the bounds of the segment *and* decodes it + * if {@code + * markNeedsDecoding} has been called. + * + *

+ * Note: This will trim whitespace from the segment before returning it. + */ + String getValue() { + if (isEmpty()) { + return ""; + } + // TODO - avoid using substring and then triming to avoid interning more than + // one string. + String substring = source.substring(start, end).trim(); + return needsDecoding ? decode(substring) : substring; + } + + // Percent decoding logic moved here + private static String decode(String value) { + if (value.indexOf('%') < 0) { + return value; + } + + int n = value.length(); + byte[] bytes = new byte[n]; + int pos = 0; + + for (int i = 0; i < n; i++) { + char c = value.charAt(i); + if (c == '%' && i + 2 < n) { + int d1 = Character.digit(value.charAt(i + 1), 16); + int d2 = Character.digit(value.charAt(i + 2), 16); + if (d1 != -1 && d2 != -1) { + bytes[pos++] = (byte) ((d1 << 4) + d2); + i += 2; + continue; + } + } + bytes[pos++] = (byte) c; + } + return new String(bytes, 0, pos, StandardCharsets.UTF_8); + } + } + + // State machine parser + private static final class EntityParser { + /** + * The current state of parsing. + * + *

+ * The format is TYPE{KEY1=VAL1,KEY2=VAL2}[ATTR1=VAL1,ATTR2=VAL2]@SCHEMA_URL; + * + *

+ * The parser state machine transitions between the following states: - TYPE: + * Parsing an + * entity type - ID_KEY: Parsing a "key" of an identity attribute - ID_VAL: + * Parsing a "value" of + * an identity attribute - DESC_KEY: Parsing a "key" of a description attribute + * - DESC_VAL: + * Parsing a "value" of a description attribute - SCHEMA_URL: Parsing the schema + * URL of a + * specific entity - SKIP_TO_NEXT: Skip to the next entity + */ + private enum State { + TYPE, + ID_KEY, + ID_VAL, + DESC_KEY, + DESC_VAL, + SCHEMA_URL, + SKIP_TO_NEXT + // TODO - do we need specific states to represent "TYPE_COMPLETE", + // "ID_COMPLETE", "DESC_COMPLETE"? + } + + /** The input entity string. */ + private final String input; + + /** The current state of parsing. (i.e. where we are in the grammar) */ + private State state = State.TYPE; + + /** The segment of the input string that we are currently parsing. */ + private final Segment currentSegment; + + /** The list of entities we've parsed. */ + private final List entities = new ArrayList<>(); + + // Temporary state for building an entity. + + /** The parsed entity type. */ + @Nullable + private String currentType; + + /** Parsed attributes denoting the entity identity. */ + private Attributes currentIdAttrs = Attributes.empty(); + + /** Parsed attributes denoting the entity description. */ + private Attributes currentDescAttrs = Attributes.empty(); + + /** Parsed schema URL for the entity. */ + @Nullable + private String currentSchemaUrl; + + /** + * A temporary builder we use when parsing key-value pairs for identity or + * description. + */ + @Nullable + private AttributesBuilder currentBuilder; + + /** The current key of a key-value pair that we are parsing. */ + @Nullable + private String currentKey; + + EntityParser(String input) { + this.input = input; + this.currentSegment = new Segment(input); + } + + /** + * Parses the input string and returns a list of entities. + * + * @return the list of entities parsed from the input string. + */ + List parse() { + int n = input.length(); + for (int i = 0; i < n; i++) { + char c = input.charAt(i); + + // We finished the previous entity, or hit a syntax error. + // Skip to the next entity and try to parse it. + if (state == State.SKIP_TO_NEXT) { + if (c == ';') { + resetEntityState(i + 1); + state = State.TYPE; + } + continue; + } + + switch (c) { + case '{': + // Finish writing entity type, start identity parsing. + if (state == State.TYPE) { + currentSegment.markEnd(i); + currentType = currentSegment.getValue(); + if (currentType == null || currentType.isEmpty()) { + logger.log(Level.WARNING, "Malformed entity definition (empty type): " + input); + state = State.SKIP_TO_NEXT; + } else { + state = State.ID_KEY; + currentSegment.reset(i + 1); + currentBuilder = Attributes.builder(); + } + } + break; + case '}': + // End identity parsing. + if (state == State.ID_VAL || state == State.ID_KEY) { + currentSegment.markEnd(i); + if (state == State.ID_VAL) { + putAttr(); + } + if (currentBuilder != null) { + currentIdAttrs = currentBuilder.build(); + } + if (currentIdAttrs.isEmpty()) { + logger.log( + Level.WARNING, + "Malformed entity definition (missing identifying attributes): " + input); + state = State.SKIP_TO_NEXT; + } else { + state = State.TYPE; // Default next state, might change if [ or @ follows + currentSegment.reset(i + 1); + } + } + break; + case '[': + // We finished identity, we're moving to parse description. + if (state == State.TYPE) { + // After } we are in TYPE state again but expecting [ or @ or ; + // TODO - Should we create new state to denote "ID_COMPLETE" for this? + state = State.DESC_KEY; + currentSegment.reset(i + 1); + currentBuilder = Attributes.builder(); + } + break; + case ']': + // We finished description, update attributes for description and move + // back to TYPE state. + // TODO - should we create a new state to denote "DESC_COMPLETE"? + // Since DESC is optional, we would would transition to the same state as + // ID_COMPLETE, but + // not allowing DESC to show up again. + if (state == State.DESC_VAL || state == State.DESC_KEY) { + currentSegment.markEnd(i); + if (state == State.DESC_VAL) { + putAttr(); + } + if (currentBuilder != null) { + currentDescAttrs = currentBuilder.build(); + } + state = State.TYPE; + currentSegment.reset(i + 1); + } + break; + case '=': + // Finish our "key" parsing and start looking for a value. + if (state == State.ID_KEY || state == State.DESC_KEY) { + currentSegment.markEnd(i); + currentKey = currentSegment.getValue(); + if (currentKey == null || currentKey.isEmpty()) { + logger.log(Level.WARNING, "Malformed key-value pair (empty key): " + input); + state = State.SKIP_TO_NEXT; + } else { + state = (state == State.ID_KEY) ? State.ID_VAL : State.DESC_VAL; + currentSegment.reset(i + 1); + } + } + break; + case ',': + // Finish our "value" parsing and start looking for the next key-value. + if (state == State.ID_VAL || state == State.DESC_VAL) { + currentSegment.markEnd(i); + putAttr(); + state = (state == State.ID_VAL) ? State.ID_KEY : State.DESC_KEY; + currentSegment.reset(i + 1); + } + break; + case '@': + // Start looking for schema url + if (state == State.TYPE) { // After } or ] we are in TYPE state + state = State.SCHEMA_URL; + currentSegment.reset(i + 1); + } + break; + case ';': + // Finish up the current entity, and get ready to parse the next. + if (state == State.TYPE || state == State.SCHEMA_URL) { + if (state == State.SCHEMA_URL) { + currentSegment.markEnd(i); + currentSchemaUrl = currentSegment.getValue(); + } + buildAndAddEntity(); + resetEntityState(i + 1); + state = State.TYPE; + } else if (state == State.ID_KEY + || state == State.ID_VAL + || state == State.DESC_KEY + || state == State.DESC_VAL) { + logger.log(Level.WARNING, "Malformed entity definition (unexpected ';'): " + input); + resetEntityState(i + 1); + state = State.TYPE; + } + break; + case '%': + // Found an escape character, mark the segment as needing decoding, which + // requires special handling. + currentSegment.markNeedsDecoding(); + break; + default: + // Keep scanning + break; + } + } + + // Handle end of string + if (state == State.TYPE || state == State.SCHEMA_URL) { + if (state == State.SCHEMA_URL) { + currentSegment.markEnd(input.length()); + currentSchemaUrl = currentSegment.getValue(); + } + buildAndAddEntity(); + } + + return entities; + } + + /** + * Adds the current attribute key-value pair into the current attribute builder. + */ + private void putAttr() { + String val = currentSegment.getValue(); + if (currentKey != null && !currentKey.isEmpty() && currentBuilder != null) { + currentBuilder.put(currentKey, val); + } + } + + /** Finishes building the current entity and adds it to the parsed list. */ + private void buildAndAddEntity() { + if (currentType != null && !currentType.isEmpty() && !currentIdAttrs.isEmpty()) { + EntityBuilder builder = Entity.builder(currentType).setIdentity(currentIdAttrs); + if (!currentDescAttrs.isEmpty()) { + builder.setDescription(currentDescAttrs); + } + if (currentSchemaUrl != null && !currentSchemaUrl.isEmpty()) { + builder.setSchemaUrl(currentSchemaUrl); + } + entities.add(builder.build()); + } + } + + /** + * Resets the state of the entity parser. + * + * @param nextStart the start index of the next entity (e.g. after the `;`). + */ + private void resetEntityState(int nextStart) { + currentType = null; + currentIdAttrs = Attributes.empty(); + currentDescAttrs = Attributes.empty(); + currentSchemaUrl = null; + currentBuilder = null; + currentKey = null; + currentSegment.reset(nextStart); + } + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java new file mode 100644 index 00000000000..f856983cded --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources.internal; + +import io.opentelemetry.sdk.extension.incubator.resources.Entity; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.EntityBuilder; +import io.opentelemetry.sdk.resources.internal.EntityUtil; +import java.util.Collection; +import java.util.stream.Collectors; + +/** + * This class is internal and experimental. Its APIs are unstable and can change at any time. Its + * APIs (or a version of them) may be promoted to the public stable API in the future, but no + * guarantees are made. + */ +public final class ExtendedEntityUtil { + private ExtendedEntityUtil() {} + + /** Convert between the incubator API entity and the internal-implementation SDK entity. */ + public static io.opentelemetry.sdk.resources.internal.Entity convertEntity(Entity entity) { + SdkEntity api = (SdkEntity) entity; + EntityBuilder builder = io.opentelemetry.sdk.resources.internal.Entity.builder(api.getType()); + if (api.getSchemaUrl() != null) { + builder.setSchemaUrl(api.getSchemaUrl()); + } + builder.withId(api.getIdentity()); + builder.withDescription(api.getDescription()); + return builder.build(); + } + + /** Constructs a resource from a prioritized list of entities. */ + public static Resource createResource(Collection entities) { + return EntityUtil.createResource( + entities.stream().map(ExtendedEntityUtil::convertEntity).collect(Collectors.toList())); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntity.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntity.java new file mode 100644 index 00000000000..bf9d7cb36a6 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntity.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources.internal; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.extension.incubator.resources.Entity; +import io.opentelemetry.sdk.extension.incubator.resources.EntityBuilder; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class SdkEntity implements Entity { + + private final String entityType; + @Nullable private final String schemaUrl; + private final Attributes identity; + private final Attributes description; + + SdkEntity( + String entityType, @Nullable String schemaUrl, Attributes identity, Attributes description) { + this.entityType = entityType; + this.schemaUrl = schemaUrl; + this.identity = identity; + this.description = description; + } + + /** + * Returns the entity type string of this entity. Must not be null. + * + * @return the entity type. + */ + public String getType() { + return entityType; + } + + /** + * Returns a map of attributes that identify the entity. + * + * @return the entity identity. + */ + public Attributes getIdentity() { + return this.identity; + } + + /** + * Returns a map of attributes that describe the entity. + * + * @return the entity description. + */ + public Attributes getDescription() { + return this.description; + } + + /** + * Returns the URL of the OpenTelemetry schema used by this resource. May be null if this entity + * does not abide by schema conventions (i.e. is custom). + * + * @return An OpenTelemetry schema URL. + */ + @Nullable + public String getSchemaUrl() { + return this.schemaUrl; + } + + @Override + public EntityBuilder toBuilder() { + return new SdkEntityBuilder(this); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntityBuilder.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntityBuilder.java new file mode 100644 index 00000000000..35baf12cfe8 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntityBuilder.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources.internal; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.extension.incubator.resources.Entity; +import io.opentelemetry.sdk.extension.incubator.resources.EntityBuilder; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class SdkEntityBuilder implements EntityBuilder { + private final String entityType; + @Nullable private String schemaUrl; + private Attributes identity; + private Attributes description; + + public SdkEntityBuilder(String entityType) { + this.entityType = entityType; + this.identity = Attributes.empty(); + this.description = Attributes.empty(); + } + + SdkEntityBuilder(SdkEntity entity) { + this.entityType = entity.getType(); + this.schemaUrl = entity.getSchemaUrl(); + this.identity = entity.getIdentity(); + this.description = entity.getDescription(); + } + + @Override + public EntityBuilder setSchemaUrl(String schemaUrl) { + this.schemaUrl = schemaUrl; + return this; + } + + @Override + public EntityBuilder setIdentity(Attributes identity) { + this.identity = identity; + return this; + } + + @Override + public EntityBuilder setDescription(Attributes description) { + this.description = description; + return this; + } + + @Override + public Entity build() { + // TODO - assertions around safe entity builds. + // TODO - identity is not empty. + return new SdkEntity(entityType, schemaUrl, identity, description); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java new file mode 100644 index 00000000000..ff209cef067 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.extension.incubator.resources.internal.SdkEntity; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +class EnvEntityDetectorTest { + + @Test + void testEmpty() { + EnvEntityDetector detector = new EnvEntityDetector(); + Collection entities = + detector.detect(DefaultConfigProperties.createFromMap(Collections.emptyMap())); + assertThat(entities).isEmpty(); + } + + @Test + void testSingleEntity() { + EnvEntityDetector detector = new EnvEntityDetector(); + String value = + "service{service.name=my-app,service.instance.id=instance-1}[service.version=1.0.0]"; + Collection entities = + detector.detect( + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.entities", value))); + + assertThat(entities).hasSize(1); + SdkEntity entity = (SdkEntity) entities.iterator().next(); + assertThat(entity.getType()).isEqualTo("service"); + assertThat(entity.getIdentity()) + .isEqualTo( + Attributes.builder() + .put("service.name", "my-app") + .put("service.instance.id", "instance-1") + .build()); + assertThat(entity.getDescription()) + .isEqualTo(Attributes.builder().put("service.version", "1.0.0").build()); + assertThat(entity.getSchemaUrl()).isNull(); + } + + @Test + void testMultipleEntitiesWithSchemaUrl() { + EnvEntityDetector detector = new EnvEntityDetector(); + String value = + "service{service.name=my-app}@https://opentelemetry.io/schemas/1.21.0;host{host.id=host-123}[host.name=web-server-01]"; + Collection entities = + detector.detect( + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.entities", value))); + + assertThat(entities).hasSize(2); + List list = new ArrayList<>(entities); + + SdkEntity entity1 = (SdkEntity) list.get(0); + assertThat(entity1.getType()).isEqualTo("service"); + assertThat(entity1.getIdentity()) + .isEqualTo(Attributes.builder().put("service.name", "my-app").build()); + assertThat(entity1.getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.21.0"); + + SdkEntity entity2 = (SdkEntity) list.get(1); + assertThat(entity2.getType()).isEqualTo("host"); + assertThat(entity2.getIdentity()) + .isEqualTo(Attributes.builder().put("host.id", "host-123").build()); + assertThat(entity2.getDescription()) + .isEqualTo(Attributes.builder().put("host.name", "web-server-01").build()); + assertThat(entity2.getSchemaUrl()).isNull(); + } + + @Test + void testPercentDecoding() { + EnvEntityDetector detector = new EnvEntityDetector(); + String value = "service{service.name=my%2Capp}[config=key%3Dvalue%5Bprod%5D]"; + Collection entities = + detector.detect( + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.entities", value))); + + assertThat(entities).hasSize(1); + SdkEntity entity = (SdkEntity) entities.iterator().next(); + assertThat(entity.getType()).isEqualTo("service"); + assertThat(entity.getIdentity()) + .isEqualTo(Attributes.builder().put("service.name", "my,app").build()); + assertThat(entity.getDescription()) + .isEqualTo(Attributes.builder().put("config", "key=value[prod]").build()); + } + + @Test + void testEmptyStringsIgnored() { + EnvEntityDetector detector = new EnvEntityDetector(); + String value = ";service{service.name=app1};;host{host.id=host-123};"; + Collection entities = + detector.detect( + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.entities", value))); + + assertThat(entities).hasSize(2); + } + + @Test + void testMalformedSyntax_MissingBrace() { + EnvEntityDetector detector = new EnvEntityDetector(); + String value = "service service.name=app1};host{host.id=host-123}"; + Collection entities = + detector.detect( + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.entities", value))); + + // Should skip the malformed one and process the valid one + assertThat(entities).hasSize(1); + SdkEntity entity = (SdkEntity) entities.iterator().next(); + assertThat(entity.getType()).isEqualTo("host"); + } + + @Test + void testMalformedSyntax_MissingBraceEnd() { + EnvEntityDetector detector = new EnvEntityDetector(); + String value = "service{service.name=app1;host{host.id=host-123}"; + Collection entities = + detector.detect( + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.entities", value))); + + assertThat(entities).hasSize(1); + SdkEntity entity = (SdkEntity) entities.iterator().next(); + assertThat(entity.getType()).isEqualTo("host"); + } + + @Test + void testMissingRequiredFields_EmptyIdentity() { + EnvEntityDetector detector = new EnvEntityDetector(); + String value = "service{};host{host.id=host-123}"; + Collection entities = + detector.detect( + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.entities", value))); + + assertThat(entities).hasSize(1); + SdkEntity entity = (SdkEntity) entities.iterator().next(); + assertThat(entity.getType()).isEqualTo("host"); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java index c406ef87a5e..163104bd8ff 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java @@ -9,11 +9,13 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.api.internal.StringUtils; -import io.opentelemetry.api.internal.Utils; import io.opentelemetry.sdk.common.internal.OtelVersion; +import io.opentelemetry.sdk.resources.internal.AttributeCheckUtil; +import io.opentelemetry.sdk.resources.internal.Entity; +import io.opentelemetry.sdk.resources.internal.EntityUtil; +import java.util.Collection; +import java.util.Collections; import java.util.Objects; -import java.util.logging.Logger; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -24,8 +26,6 @@ @Immutable @AutoValue public abstract class Resource { - private static final Logger logger = Logger.getLogger(Resource.class.getName()); - private static final AttributeKey SERVICE_NAME = AttributeKey.stringKey("service.name"); private static final AttributeKey TELEMETRY_SDK_LANGUAGE = AttributeKey.stringKey("telemetry.sdk.language"); @@ -33,14 +33,6 @@ public abstract class Resource { AttributeKey.stringKey("telemetry.sdk.name"); private static final AttributeKey TELEMETRY_SDK_VERSION = AttributeKey.stringKey("telemetry.sdk.version"); - - private static final int MAX_LENGTH = 255; - private static final String ERROR_MESSAGE_INVALID_CHARS = - " should be a ASCII string with a length greater than 0 and not exceed " - + MAX_LENGTH - + " characters."; - private static final String ERROR_MESSAGE_INVALID_VALUE = - " should be a ASCII string with a length not exceed " + MAX_LENGTH + " characters."; private static final Resource EMPTY = create(Attributes.empty()); private static final Resource TELEMETRY_SDK; @@ -91,7 +83,7 @@ public static Resource empty() { * @return a {@code Resource}. * @throws NullPointerException if {@code attributes} is null. * @throws IllegalArgumentException if attribute key or attribute value is not a valid printable - * ASCII string or exceed {@link #MAX_LENGTH} characters. + * ASCII string or exceed {@link AttributeCheckUtil#MAX_LENGTH} characters. */ public static Resource create(Attributes attributes) { return create(attributes, null); @@ -105,11 +97,27 @@ public static Resource create(Attributes attributes) { * @return a {@code Resource}. * @throws NullPointerException if {@code attributes} is null. * @throws IllegalArgumentException if attribute key or attribute value is not a valid printable - * ASCII string or exceed {@link #MAX_LENGTH} characters. + * ASCII string or exceed {@link AttributeCheckUtil#MAX_LENGTH} characters. */ public static Resource create(Attributes attributes, @Nullable String schemaUrl) { - checkAttributes(Objects.requireNonNull(attributes, "attributes")); - return new AutoValue_Resource(schemaUrl, attributes); + return create(attributes, schemaUrl, Collections.emptyList()); + } + + /** + * Returns a {@link Resource}. + * + * @param attributes a map of {@link Attributes} that describe the resource. + * @param schemaUrl The URL of the OpenTelemetry schema used to create this Resource. + * @param entities The set of detected {@link Entity}s that participate in this resource. + * @return a {@code Resource}. + * @throws NullPointerException if {@code attributes} is null. + * @throws IllegalArgumentException if attribute key or attribute value is not a valid printable + * ASCII string or exceed {@link AttributeCheckUtil#MAX_LENGTH} characters. + */ + static Resource create( + Attributes attributes, @Nullable String schemaUrl, Collection entities) { + AttributeCheckUtil.checkAttributes(Objects.requireNonNull(attributes, "attributes")); + return new AutoValue_Resource(schemaUrl, attributes, entities); } /** @@ -121,12 +129,38 @@ public static Resource create(Attributes attributes, @Nullable String schemaUrl) @Nullable public abstract String getSchemaUrl(); + /** + * Returns a map of attributes that describe the resource, not associated with entities. + * + * @return a map of attributes. + */ + abstract Attributes getRawAttributes(); + + /** + * Returns a collection of associated entities. + * + * @return a collection of entities. + */ + abstract Collection getEntities(); + /** * Returns a map of attributes that describe the resource. * * @return a map of attributes. */ - public abstract Attributes getAttributes(); + // @Memoized - This breaks nullaway. + public Attributes getAttributes() { + AttributesBuilder result = Attributes.builder(); + getEntities() + .forEach( + e -> { + result.putAll(e.getId()); + result.putAll(e.getDescription()); + }); + // In merge rules, raw comes last, so we return these last. + result.putAll(getRawAttributes()); + return result.build(); + } /** * Returns the value for a given resource attribute key. @@ -146,63 +180,7 @@ public T getAttribute(AttributeKey key) { * @return the newly merged {@code Resource}. */ public Resource merge(@Nullable Resource other) { - if (other == null || other == EMPTY) { - return this; - } - - AttributesBuilder attrBuilder = Attributes.builder(); - attrBuilder.putAll(this.getAttributes()); - attrBuilder.putAll(other.getAttributes()); - - if (other.getSchemaUrl() == null) { - return create(attrBuilder.build(), getSchemaUrl()); - } - if (getSchemaUrl() == null) { - return create(attrBuilder.build(), other.getSchemaUrl()); - } - if (!other.getSchemaUrl().equals(getSchemaUrl())) { - logger.info( - "Attempting to merge Resources with different schemaUrls. " - + "The resulting Resource will have no schemaUrl assigned. Schema 1: " - + getSchemaUrl() - + " Schema 2: " - + other.getSchemaUrl()); - // currently, behavior is undefined if schema URLs don't match. In the future, we may - // apply schema transformations if possible. - return create(attrBuilder.build(), null); - } - return create(attrBuilder.build(), getSchemaUrl()); - } - - private static void checkAttributes(Attributes attributes) { - attributes.forEach( - (key, value) -> { - Utils.checkArgument( - isValidAndNotEmpty(key), "Attribute key" + ERROR_MESSAGE_INVALID_CHARS); - Objects.requireNonNull(value, "Attribute value" + ERROR_MESSAGE_INVALID_VALUE); - }); - } - - /** - * Determines whether the given {@code String} is a valid printable ASCII string with a length not - * exceed {@link #MAX_LENGTH} characters. - * - * @param name the name to be validated. - * @return whether the name is valid. - */ - private static boolean isValid(String name) { - return name.length() <= MAX_LENGTH && StringUtils.isPrintableString(name); - } - - /** - * Determines whether the given {@code String} is a valid printable ASCII string with a length - * greater than 0 and not exceed {@link #MAX_LENGTH} characters. - * - * @param name the name to be validated. - * @return whether the name is valid. - */ - private static boolean isValidAndNotEmpty(AttributeKey name) { - return !name.getKey().isEmpty() && isValid(name.getKey()); + return EntityUtil.merge(this, other); } /** diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java index 9963eeaf541..5bb87bf3de4 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java @@ -8,7 +8,14 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.internal.Entity; +import io.opentelemetry.sdk.resources.internal.EntityUtil; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.annotation.Nullable; /** @@ -20,6 +27,7 @@ public class ResourceBuilder { private final AttributesBuilder attributesBuilder = Attributes.builder(); + private final List entities = new ArrayList<>(); @Nullable private String schemaUrl; /** @@ -192,8 +200,33 @@ public ResourceBuilder setSchemaUrl(String schemaUrl) { return this; } + /** Create the {@link Resource} from this. */ /** Create the {@link Resource} from this. */ public Resource build() { - return Resource.create(attributesBuilder.build(), schemaUrl); + // Derive schemaUrl from entity, if able. + if (schemaUrl == null) { + Set entitySchemas = + entities.stream().map(Entity::getSchemaUrl).collect(Collectors.toSet()); + if (entitySchemas.size() == 1) { + // Updated Entities use same schema, we can preserve it. + schemaUrl = entitySchemas.iterator().next(); + } + } + + // When adding an entity, we remove any raw attributes it may conflict with. + this.attributesBuilder.removeIf(key -> EntityUtil.hasAttributeKey(this.entities, key)); + return Resource.create(attributesBuilder.build(), schemaUrl, entities); + } + + /** Appends a new entity on to the end of the list of entities. */ + ResourceBuilder add(Entity e) { + this.entities.add(e); + return this; + } + + /** Appends a new collection of entities on to the end of the list of entities. */ + ResourceBuilder addAll(Collection entities) { + this.entities.addAll(entities); + return this; } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/AttributeCheckUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/AttributeCheckUtil.java new file mode 100644 index 00000000000..966c5b58277 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/AttributeCheckUtil.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.api.internal.Utils; +import java.util.Objects; + +/** + * Helpers to check resource attributes. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class AttributeCheckUtil { + private AttributeCheckUtil() {} + + // Note: Max length is actually configurable by specification. + private static final int MAX_LENGTH = 255; + private static final String ERROR_MESSAGE_INVALID_CHARS = + " should be a ASCII string with a length greater than 0 and not exceed " + + MAX_LENGTH + + " characters."; + private static final String ERROR_MESSAGE_INVALID_VALUE = + " should be a ASCII string with a length not exceed " + MAX_LENGTH + " characters."; + + /** Determine if the set of attributes if valid for Resource / Entity. */ + public static void checkAttributes(Attributes attributes) { + attributes.forEach( + (key, value) -> { + Utils.checkArgument( + isValidAndNotEmpty(key), "Attribute key" + ERROR_MESSAGE_INVALID_CHARS); + Objects.requireNonNull(value, "Attribute value" + ERROR_MESSAGE_INVALID_VALUE); + }); + } + + /** + * Determines whether the given {@code String} is a valid printable ASCII string with a length + * greater than 0 and not exceed {@link #MAX_LENGTH} characters. + * + * @param name the name to be validated. + * @return whether the name is valid. + */ + public static boolean isValidAndNotEmpty(AttributeKey name) { + return !name.getKey().isEmpty() && isValid(name.getKey()); + } + + /** + * Determines whether the given {@code String} is a valid printable ASCII string with a length not + * exceed {@link #MAX_LENGTH} characters. + * + * @param name the name to be validated. + * @return whether the name is valid. + */ + public static boolean isValid(String name) { + return name.length() <= MAX_LENGTH && StringUtils.isPrintableString(name); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/Entity.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/Entity.java new file mode 100644 index 00000000000..40cf63e126b --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/Entity.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import io.opentelemetry.api.common.Attributes; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Entity represents an object of interest associated with produced telemetry: traces, metrics or + * logs. + * + *

For example, telemetry produced using OpenTelemetry SDK is normally associated with a Service + * entity. Similarly, OpenTelemetry defines system metrics for a host. The Host is the entity we + * want to associate metrics with in this case. + * + *

Entities may be also associated with produced telemetry indirectly. For example a service that + * produces telemetry is also related with a process in which the service runs, so we say that the + * Service entity is related to the Process entity. The process normally also runs on a host, so we + * say that the Process entity is related to the Host entity. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@Immutable +public interface Entity { + /** + * Returns the entity type string of this entity. Must not be null. + * + * @return the entity type. + */ + String getType(); + + /** + * Returns a map of attributes that identify the entity. + * + * @return the entity identity. + */ + Attributes getId(); + + /** + * Returns a map of attributes that describe the entity. + * + * @return the entity description. + */ + Attributes getDescription(); + + /** + * Returns the URL of the OpenTelemetry schema used by this resource. May be null if this entity + * does not abide by schema conventions (i.e. is custom). + * + * @return An OpenTelemetry schema URL. + */ + @Nullable + String getSchemaUrl(); + + /** + * Returns a new {@link EntityBuilder} instance populated with the data of this {@link Entity}. + */ + EntityBuilder toBuilder(); + + /** + * Returns a new {@link EntityBuilder} instance for creating arbitrary {@link Entity}. + * + * @param entityType the entity type string of this entity. + */ + public static EntityBuilder builder(String entityType) { + return SdkEntity.builder(entityType); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityBuilder.java new file mode 100644 index 00000000000..bb2b21c998b --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityBuilder.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import io.opentelemetry.api.common.Attributes; + +/** + * A builder of {@link Entity} that allows to add identifying or descriptive {@link Attributes}, as + * well as type and schema_url. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public interface EntityBuilder { + /** + * Assign an OpenTelemetry schema URL to the resulting Entity. + * + * @param schemaUrl The URL of the OpenTelemetry schema being used to create this Entity. + * @return this + */ + EntityBuilder setSchemaUrl(String schemaUrl); + + /** + * Modify the descriptive attributes of this Entity. + * + * @param description The attributes that describe the Entity. + * @return this + */ + EntityBuilder withDescription(Attributes description); + + /** + * Modify the identifying attributes of this Entity. + * + * @param id The identifying attributes. + * @return this + */ + EntityBuilder withId(Attributes id); + + /** Create the {@link Entity} from this. */ + Entity build(); +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java new file mode 100644 index 00000000000..d2379f1a4e0 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java @@ -0,0 +1,311 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.ResourceBuilder; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** + * Helper class for dealing with Entities. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class EntityUtil { + private static final Logger logger = Logger.getLogger(EntityUtil.class.getName()); + + private EntityUtil() {} + + /** + * Constructs a new {@link Resource} with Entity support. + * + * @param entities The set of entities the resource needs. + * @return A constructed resource. + */ + public static Resource createResource(Collection entities) { + return createResourceRaw( + Attributes.empty(), EntityUtil.mergeResourceSchemaUrl(entities, null, null), entities); + } + + /** + * Constructs a new {@link Resource} with Entity support. + * + * @param attributes The raw attributes for the resource. + * @param schemaUrl The schema url for the resource. + * @param entities The set of entities the resource needs. + * @return A constructed resource. + */ + static Resource createResourceRaw( + Attributes attributes, @Nullable String schemaUrl, Collection entities) { + try { + Method method = + Resource.class.getDeclaredMethod( + "create", Attributes.class, String.class, Collection.class); + if (method != null) { + method.setAccessible(true); + Object result = method.invoke(null, attributes, schemaUrl, entities); + if (result instanceof Resource) { + return (Resource) result; + } + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", e); + } + // Fall back to non-entity behavior? + logger.log(Level.WARNING, "Attempting to use entities with unsupported resource"); + return Resource.empty(); + } + + /** Appends a new entity on to the end of the list of entities. */ + public static ResourceBuilder addEntity(ResourceBuilder rb, Entity e) { + try { + Method method = ResourceBuilder.class.getDeclaredMethod("add", Entity.class); + if (method != null) { + method.setAccessible(true); + method.invoke(rb, e); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { + logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", ex); + } + return rb; + } + + /** Appends a new collection of entities on to the end of the list of entities. */ + public static ResourceBuilder addAllEntity(ResourceBuilder rb, Collection e) { + try { + Method method = ResourceBuilder.class.getDeclaredMethod("addAll", Collection.class); + if (method != null) { + method.setAccessible(true); + method.invoke(rb, e); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { + logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", ex); + } + return rb; + } + + /** + * Returns a collectoion of associated entities. + * + * @return a collection of entities. + */ + @SuppressWarnings("unchecked") + public static Collection getEntities(Resource r) { + try { + Method method = Resource.class.getDeclaredMethod("getEntities"); + if (method != null) { + method.setAccessible(true); + return (Collection) method.invoke(r); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", e); + } + return Collections.emptyList(); + } + + /** + * Returns a map of attributes that describe the resource, not associated with entites. + * + * @return a map of attributes. + */ + public static Attributes getRawAttributes(Resource r) { + try { + Method method = Resource.class.getDeclaredMethod("getRawAttributes"); + if (method != null) { + method.setAccessible(true); + return (Attributes) method.invoke(r); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", e); + } + return Attributes.empty(); + } + + /** Returns true if any entity in the collection has the attribute key, in id or description. */ + public static boolean hasAttributeKey(Collection entities, AttributeKey key) { + return entities.stream() + .anyMatch( + e -> e.getId().asMap().containsKey(key) || e.getDescription().asMap().containsKey(key)); + } + + /** Decides on a final SchemaURL for OTLP Resource based on entities chosen. */ + @Nullable + static String mergeResourceSchemaUrl( + Collection entities, @Nullable String baseUrl, @Nullable String nextUrl) { + // Check if entities all share the same URL. + Set entitySchemas = + entities.stream().map(Entity::getSchemaUrl).collect(Collectors.toSet()); + // If we have no entities, we preserve previous schema url behavior. + String result = baseUrl; + if (entitySchemas.size() == 1) { + // Updated Entities use same schema, we can preserve it. + result = entitySchemas.iterator().next(); + } else if (entitySchemas.size() > 1) { + // Entities use different schemas, resource must treat this as no schema_url. + result = null; + } + + // If schema url of merging resource is null, we use our current result. + if (nextUrl == null) { + return result; + } + // When there are no entities, we use old schema url merge behavior + if (result == null && entities.isEmpty()) { + return nextUrl; + } + if (!nextUrl.equals(result)) { + logger.info( + "Attempting to merge Resources with different schemaUrls. " + + "The resulting Resource will have no schemaUrl assigned. Schema 1: " + + baseUrl + + " Schema 2: " + + nextUrl); + return null; + } + return result; + } + + /** + * Merges "loose" attributes on resource, removing those which conflict with the set of entities. + * + * @param base loose attributes from base resource + * @param additional additional attributes to add to the resource. + * @param entities the set of entites on the resource. + * @return the new set of raw attributes for Resource and the set of conflicting entities that + * MUST NOT be reported on OTLP resource. + */ + @SuppressWarnings("unchecked") + static final RawAttributeMergeResult mergeRawAttributes( + Attributes base, Attributes additional, Collection entities) { + AttributesBuilder result = base.toBuilder(); + // We know attribute conflicts were handled perviously on the resource, so + // This needs to account for entity merge of new entities, and remove raw + // attributes that would have been removed with new entities. + result.removeIf(key -> hasAttributeKey(entities, key)); + // For every "raw" attribute on the other resource, we merge into the + // resource, but check for entity conflicts from previous entities. + ArrayList conflicts = new ArrayList<>(); + if (!additional.isEmpty()) { + additional.forEach( + (key, value) -> { + for (Entity e : entities) { + if (e.getId().get(key) != null || e.getDescription().get(key) != null) { + // Remove the entity and push all attributes as raw, + // we have an override. + conflicts.add(e); + result.putAll(e.getId()).putAll(e.getDescription()); + } + } + result.put((AttributeKey) key, value); + }); + } + return RawAttributeMergeResult.create(result.build(), conflicts); + } + + /** + * Merges entities according to specification rules. + * + * @param base the initial set of entities. + * @param additional Additional entities to merge with base set. + * @return A new set of entities with no duplicate types. + */ + static Collection mergeEntities(Collection base, Collection additional) { + if (base.isEmpty()) { + return additional; + } + if (additional.isEmpty()) { + return base; + } + Map entities = new HashMap<>(); + base.forEach(e -> entities.put(e.getType(), e)); + for (Entity e : additional) { + if (!entities.containsKey(e.getType())) { + entities.put(e.getType(), e); + } else { + Entity old = entities.get(e.getType()); + // If the entity identity is the same, but schema_url is different: drop the new entity d' + // Note: We could offer configuration in this case + if (old.getSchemaUrl() == null || !old.getSchemaUrl().equals(e.getSchemaUrl())) { + logger.info( + "Discovered conflicting entities. Entity [" + + old.getType() + + "] has different schema url [" + + old.getSchemaUrl() + + "], new entity with schema url[" + + e.getSchemaUrl() + + "] is dropped."); + } else if (!old.getId().equals(e.getId())) { + // If the entity identity is different: drop the new entity d'. + logger.info( + "Discovered conflicting entities. Entity [" + + old.getType() + + "] has identity [" + + old.getId() + + "], new entity [" + + e.getId() + + "] is dropped."); + } else { + // If the entity identity and schema_url are the same, merge the descriptive attributes + // of d' into e': + // For each descriptive attribute da' in d' + // If da'.key does not exist in e', then add da' to ei + // otherwise, ignore. + Entity next = + old.toBuilder() + .withDescription( + Attributes.builder() + .putAll(e.getDescription()) + .putAll(old.getDescription()) + .build()) + .build(); + entities.put(next.getType(), next); + } + } + } + return entities.values(); + } + + /** + * Returns a new, merged {@link Resource} by merging the {@code base} {@code Resource} with the + * {@code next} {@code Resource}. In case of a collision, the "next" {@code Resource} takes + * precedence. + * + * @param base the {@code Resource} into which we merge new values. + * @param next the {@code Resource} that will be merged with {@code base}. + * @return the newly merged {@code Resource}. + */ + public static Resource merge(Resource base, @Nullable Resource next) { + if (next == null || next == Resource.empty()) { + return base; + } + // Merge Algorithm from + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/entities/0264-resource-and-entities.md#entity-merging-and-resource + Collection entities = EntityUtil.mergeEntities(getEntities(base), getEntities(next)); + RawAttributeMergeResult attributeResult = + EntityUtil.mergeRawAttributes(getRawAttributes(base), getRawAttributes(next), entities); + // Remove entities that are conflicting with raw attributes, and therefore in an unknown state. + entities.removeAll(attributeResult.getConflicts()); + // Now figure out schema url for overall resource. + String schemaUrl = + EntityUtil.mergeResourceSchemaUrl(entities, base.getSchemaUrl(), next.getSchemaUrl()); + return createResourceRaw(attributeResult.getAttributes(), schemaUrl, entities); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/RawAttributeMergeResult.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/RawAttributeMergeResult.java new file mode 100644 index 00000000000..f1cc081b035 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/RawAttributeMergeResult.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import java.util.Collection; +import javax.annotation.concurrent.Immutable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@Immutable +@AutoValue +abstract class RawAttributeMergeResult { + /** Merged raw attributes. */ + abstract Attributes getAttributes(); + + /** + * Entities in conflict that should be removed from resource to avoid reporting invalid attribute + * sets in OTLP resource. + */ + abstract Collection getConflicts(); + + static final RawAttributeMergeResult create(Attributes attributes, Collection conflicts) { + return new AutoValue_RawAttributeMergeResult(attributes, conflicts); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntity.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntity.java new file mode 100644 index 00000000000..2ddf17dde33 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntity.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * SDK implementation of Entity. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@Immutable +@AutoValue +abstract class SdkEntity implements Entity { + /** + * Returns a {@link Entity}. + * + * @param entityType the entity type string of this entity. + * @param id a map of attributes that identify the entity. + * @param description a map of attributes that describe the entity. + * @return a {@code Entity}. + */ + static final Entity create( + String entityType, Attributes id, Attributes description, @Nullable String schemaUrl) { + return new AutoValue_SdkEntity(entityType, id, description, schemaUrl); + } + + @Override + public final EntityBuilder toBuilder() { + return new SdkEntityBuilder(this); + } + + /** + * Returns a new {@link EntityBuilder} instance for creating arbitrary {@link Entity}. + * + * @param entityType the entity type string of this entity. + */ + public static final EntityBuilder builder(String entityType) { + return new SdkEntityBuilder(entityType); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntityBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntityBuilder.java new file mode 100644 index 00000000000..2a425f5a0d3 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntityBuilder.java @@ -0,0 +1,62 @@ +/* + * 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; + +/** + * 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. + */ +final class SdkEntityBuilder implements EntityBuilder { + private final String entityType; + private Attributes description; + private Attributes id; + @Nullable private String schemaUrl; + + SdkEntityBuilder(String entityType) { + AttributeCheckUtil.isValid(entityType); + this.entityType = entityType; + this.description = Attributes.empty(); + this.id = Attributes.empty(); + } + + SdkEntityBuilder(Entity seed) { + this.entityType = seed.getType(); + this.schemaUrl = seed.getSchemaUrl(); + this.id = seed.getId(); + this.description = seed.getDescription(); + } + + @Override + public EntityBuilder setSchemaUrl(String schemaUrl) { + this.schemaUrl = schemaUrl; + return this; + } + + @Override + public EntityBuilder withDescription(Attributes description) { + AttributeCheckUtil.checkAttributes(description); + this.description = description; + return this; + } + + @Override + public EntityBuilder withId(Attributes id) { + AttributeCheckUtil.checkAttributes(id); + this.id = id; + return this; + } + + @Override + public Entity build() { + return SdkEntity.create(entityType, id, description, schemaUrl); + } +} diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java index 62a2e2219f4..6bc1ca19710 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java @@ -201,6 +201,15 @@ void testResourceEquals() { .testEquals(); } + @Test + void testToString() { + Attributes attribute1 = Attributes.of(stringKey("a"), "1", stringKey("b"), "2"); + Resource resource = Resource.create(attribute1, "http://schema"); + assertThat(resource.toString()) + .isEqualTo( + "Resource{schemaUrl=http://schema, rawAttributes={a=\"1\", b=\"2\"}, entities=[]}"); + } + @Test void testMergeResources() { Attributes expectedAttributes = diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java index aa58b8869c3..9ae3db2631c 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java @@ -344,14 +344,12 @@ void close() { void toString_Valid() { when(logRecordProcessor.toString()).thenReturn("MockLogRecordProcessor"); assertThat(sdkLoggerProvider.toString()) - .isEqualTo( - "SdkLoggerProvider{" - + "clock=SystemClock{}, " - + "resource=Resource{schemaUrl=null, attributes={key=\"value\"}}, " - + "logLimits=LogLimits{maxNumberOfAttributes=128, maxAttributeValueLength=2147483647}, " + .matches( + "SdkLoggerProvider\\{clock=SystemClock\\{\\}, " + + "resource=Resource\\{schemaUrl=null, (attributes|rawAttributes)=\\{key=\"value\"\\}(, entities=\\[\\])?\\}, " + + "logLimits=LogLimits\\{maxNumberOfAttributes=128, maxAttributeValueLength=2147483647\\}, " + "logRecordProcessor=MockLogRecordProcessor, " - + "loggerConfigurator=ScopeConfiguratorImpl{conditions=[]}" - + "}"); + + "loggerConfigurator=ScopeConfiguratorImpl\\{conditions=\\[\\]\\}\\}"); } private static ScopeConfigurator flipConfigurator(boolean enabled) { diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java index e6be6c41df0..a408823a681 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java @@ -1068,9 +1068,9 @@ void spanDataToString() { + "traceFlags=00, " + "traceState=ArrayBasedTraceState\\{entries=\\[]}, remote=false, valid=false}, " + "resource=Resource\\{schemaUrl=null, " - + "attributes=\\{service.name=\"unknown_service:java\", " + + "(attributes|rawAttributes)=\\{service.name=\"unknown_service:java\", " + "telemetry.sdk.language=\"java\", telemetry.sdk.name=\"opentelemetry\", " - + "telemetry.sdk.version=\"\\d+.\\d+.\\d+(-rc.\\d+)?(-SNAPSHOT)?\"}}, " + + "telemetry.sdk.version=\"\\d+.\\d+.\\d+(-rc.\\d+)?(-SNAPSHOT)?\"\\}(, entities=\\[\\])?\\}, " + "instrumentationScopeInfo=InstrumentationScopeInfo\\{" + "name=SpanBuilderSdkTest, version=null, schemaUrl=null, attributes=\\{}}, " + "name=span_name, " From f6111757755b7be8b046cede49219bde6ced5b9a Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Fri, 24 Apr 2026 17:17:03 +0000 Subject: [PATCH 02/22] Add missing tests, spotlessApply --- .../resources/EnvEntityDetector.java | 63 ++--- .../sdk/resources/ResourceTest.java | 27 ++ .../resources/internal/EntityUtilTest.java | 253 ++++++++++++++++++ 3 files changed, 299 insertions(+), 44 deletions(-) create mode 100644 sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/EntityUtilTest.java diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java index 677f006644e..6a4ea541af6 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java @@ -40,20 +40,13 @@ public Collection detect(ConfigProperties config) { /** * Segment class represents a start/stop endpoint within a source String. * - *

- * A segment can be used to extract a substring from the source string *without - * interning* the - * string into the JDK's string tables. This can dramatically reduce allocations - * when parsing. - * Segment is intended to provide a similar interface to using {@code substring} - * on {@code + *

A segment can be used to extract a substring from the source string *without interning* the + * string into the JDK's string tables. This can dramatically reduce allocations when parsing. + * Segment is intended to provide a similar interface to using {@code substring} on {@code * String}. * - *

- * Additionally, a Segment can be denoted as URL-encoded (e.g. using '%20' to - * denote a - * character.) In this case, the segment will be decoded when extracting its - * String value. + *

Additionally, a Segment can be denoted as URL-encoded (e.g. using '%20' to denote a + * character.) In this case, the segment will be decoded when extracting its String value. */ private static final class Segment { private final String source; @@ -79,8 +72,7 @@ void markEnd(int end) { } /** - * Denotes that the segment is URL encoded, and should be decoded when calling - * {@code + * Denotes that the segment is URL encoded, and should be decoded when calling {@code * getValue()}. */ void markNeedsDecoding() { @@ -93,12 +85,10 @@ boolean isEmpty() { } /** - * Returns the string represented by the bounds of the segment *and* decodes it - * if {@code + * Returns the string represented by the bounds of the segment *and* decodes it if {@code * markNeedsDecoding} has been called. * - *

- * Note: This will trim whitespace from the segment before returning it. + *

Note: This will trim whitespace from the segment before returning it. */ String getValue() { if (isEmpty()) { @@ -142,18 +132,12 @@ private static final class EntityParser { /** * The current state of parsing. * - *

- * The format is TYPE{KEY1=VAL1,KEY2=VAL2}[ATTR1=VAL1,ATTR2=VAL2]@SCHEMA_URL; + *

The format is TYPE{KEY1=VAL1,KEY2=VAL2}[ATTR1=VAL1,ATTR2=VAL2]@SCHEMA_URL; * - *

- * The parser state machine transitions between the following states: - TYPE: - * Parsing an - * entity type - ID_KEY: Parsing a "key" of an identity attribute - ID_VAL: - * Parsing a "value" of - * an identity attribute - DESC_KEY: Parsing a "key" of a description attribute - * - DESC_VAL: - * Parsing a "value" of a description attribute - SCHEMA_URL: Parsing the schema - * URL of a + *

The parser state machine transitions between the following states: - TYPE: Parsing an + * entity type - ID_KEY: Parsing a "key" of an identity attribute - ID_VAL: Parsing a "value" of + * an identity attribute - DESC_KEY: Parsing a "key" of a description attribute - DESC_VAL: + * Parsing a "value" of a description attribute - SCHEMA_URL: Parsing the schema URL of a * specific entity - SKIP_TO_NEXT: Skip to the next entity */ private enum State { @@ -183,8 +167,7 @@ private enum State { // Temporary state for building an entity. /** The parsed entity type. */ - @Nullable - private String currentType; + @Nullable private String currentType; /** Parsed attributes denoting the entity identity. */ private Attributes currentIdAttrs = Attributes.empty(); @@ -193,19 +176,13 @@ private enum State { private Attributes currentDescAttrs = Attributes.empty(); /** Parsed schema URL for the entity. */ - @Nullable - private String currentSchemaUrl; + @Nullable private String currentSchemaUrl; - /** - * A temporary builder we use when parsing key-value pairs for identity or - * description. - */ - @Nullable - private AttributesBuilder currentBuilder; + /** A temporary builder we use when parsing key-value pairs for identity or description. */ + @Nullable private AttributesBuilder currentBuilder; /** The current key of a key-value pair that we are parsing. */ - @Nullable - private String currentKey; + @Nullable private String currentKey; EntityParser(String input) { this.input = input; @@ -370,9 +347,7 @@ List parse() { return entities; } - /** - * Adds the current attribute key-value pair into the current attribute builder. - */ + /** Adds the current attribute key-value pair into the current attribute builder. */ private void putAttr() { String val = currentSegment.getValue(); if (currentKey != null && !currentKey.isEmpty() && currentBuilder != null) { diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java index 6bc1ca19710..0f8d0a0c99f 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java @@ -23,6 +23,8 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.common.Value; +import io.opentelemetry.sdk.resources.internal.Entity; +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; @@ -236,6 +238,31 @@ void testMergeResources_schema() { assertThat(schemaTwo.merge(schemaOne).getSchemaUrl()).isNull(); } + @Test + void testMergeResources_entities_separate_types_and_schema() { + Resource resource1 = + Resource.builder() + .add( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "a").build()) + .build()) + .build(); + Resource resource2 = + Resource.builder() + .add( + Entity.builder("b") + .setSchemaUrl("two") + .withId(Attributes.builder().put("b.id", "b").build()) + .build()) + .build(); + Resource merged = resource1.merge(resource2); + assertThat(merged.getSchemaUrl()).isNull(); + assertThat(merged.getEntities()).hasSize(2); + OpenTelemetryAssertions.assertThat(merged.getAttributes()).containsEntry("a.id", "a"); + OpenTelemetryAssertions.assertThat(merged.getAttributes()).containsEntry("b.id", "b"); + } + @Test void testMergeResources_Resource1() { Attributes expectedAttributes = Attributes.of(stringKey("a"), "1", stringKey("b"), "2"); diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/EntityUtilTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/EntityUtilTest.java new file mode 100644 index 00000000000..23aece73575 --- /dev/null +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/EntityUtilTest.java @@ -0,0 +1,253 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link EntityUtil}. */ +class EntityUtilTest { + @Test + void testMerge_entities_same_types_and_id() { + Collection base = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "a").build()) + .withDescription(Attributes.builder().put("a.desc1", "a").build()) + .build()); + Collection added = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "a").build()) + .withDescription(Attributes.builder().put("a.desc2", "b").build()) + .build()); + Collection merged = EntityUtil.mergeEntities(base, added); + assertThat(merged).hasSize(1); + assertThat(merged) + .anySatisfy( + entity -> { + assertThat(entity.getType()).isEqualTo("a"); + assertThat(entity.getSchemaUrl()).isEqualTo("one"); + assertThat(entity.getId()).containsEntry("a.id", "a"); + assertThat(entity.getDescription()) + .containsEntry("a.desc1", "a") + .containsEntry("a.desc2", "b"); + }); + } + + @Test + void testMerge_entities_same_types_and_id_different_schema() { + Collection base = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "a").build()) + .withDescription(Attributes.builder().put("a.desc1", "a").build()) + .build()); + Collection added = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("two") + .withId(Attributes.builder().put("a.id", "a").build()) + .withDescription(Attributes.builder().put("a.desc2", "b").build()) + .build()); + Collection merged = EntityUtil.mergeEntities(base, added); + assertThat(merged).hasSize(1); + assertThat(merged) + .anySatisfy( + entity -> { + assertThat(entity.getType()).isEqualTo("a"); + assertThat(entity.getSchemaUrl()).isEqualTo("one"); + assertThat(entity.getId()).containsEntry("a.id", "a"); + assertThat(entity.getDescription()) + .containsEntry("a.desc1", "a") + // Don't merge between versions. + .doesNotContainKey("a.desc2"); + }); + } + + @Test + void testMerge_entities_same_types_different_id() { + Collection base = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "a").build()) + .withDescription(Attributes.builder().put("a.desc1", "a").build()) + .build()); + Collection added = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "b").build()) + .withDescription(Attributes.builder().put("a.desc2", "b").build()) + .build()); + Collection merged = EntityUtil.mergeEntities(base, added); + assertThat(merged).hasSize(1); + assertThat(merged) + .satisfiesExactly( + entity -> { + assertThat(entity.getType()).isEqualTo("a"); + assertThat(entity.getSchemaUrl()).isEqualTo("one"); + assertThat(entity.getId()).containsEntry("a.id", "a"); + assertThat(entity.getDescription()) + .containsEntry("a.desc1", "a") + // Don't merge between different ids. + .doesNotContainKey("a.desc2"); + }); + } + + @Test + void testMerge_entities_separate_types_and_schema() { + Collection base = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "a").build()) + .build()); + Collection added = + Arrays.asList( + Entity.builder("b") + .setSchemaUrl("two") + .withId(Attributes.builder().put("b.id", "b").build()) + .build()); + Collection merged = EntityUtil.mergeEntities(base, added); + // Make sure we keep both entities when no conflict. + assertThat(merged) + .satisfiesExactlyInAnyOrder( + a -> assertThat(a.getType()).isEqualTo("a"), + b -> assertThat(b.getType()).isEqualTo("b")); + } + + @Test + void testSchemaUrlMerge_no_entities_differentUrls() { + // If the we find conflicting schema URLs in resource we must drop schema url (set to null). + String result = EntityUtil.mergeResourceSchemaUrl(Collections.emptyList(), "one", "two"); + assertThat(result).isNull(); + } + + @Test + void testSchemaUrlMerge_no_entities_base_null() { + // If the our resource had no schema url it abides by, we use the incoming schema url. + String result = EntityUtil.mergeResourceSchemaUrl(Collections.emptyList(), null, "two"); + assertThat(result).isEqualTo("two"); + } + + @Test + void testSchemaUrlMerge_no_entities_next_null() { + // If the new resource had no schema url it abides by, we preserve ours. + // NOTE: this is by specification, but seems problematic if conflicts in merge + // cause violation of SchemaURL. + String result = EntityUtil.mergeResourceSchemaUrl(Collections.emptyList(), "one", null); + assertThat(result).isEqualTo("one"); + } + + @Test + void testSchemaUrlMerge_entities_same_url() { + // If the new resource had no schema url it abides by, we preserve ours. + // NOTE: this is by specification, but seems problematic if conflicts in merge + // cause violation of SchemaURL. + String result = + EntityUtil.mergeResourceSchemaUrl( + Arrays.asList( + Entity.builder("t") + .setSchemaUrl("one") + .withId(Attributes.builder().put("id", 1).build()) + .build()), + "one", + null); + assertThat(result).isEqualTo("one"); + } + + @Test + void testSchemaUrlMerge_entities_different_url() { + // When entities have conflicting schema urls, we cannot fill out resource schema url, + // no matter what. + String result = + EntityUtil.mergeResourceSchemaUrl( + Arrays.asList( + Entity.builder("t") + .setSchemaUrl("one") + .withId(Attributes.builder().put("id", 1).build()) + .build(), + Entity.builder("t2") + .setSchemaUrl("two") + .withId(Attributes.builder().put("id2", 1).build()) + .build()), + "one", + "one"); + assertThat(result).isEqualTo(null); + } + + @Test + void testRawAttributeMerge_no_entities() { + // When no entities are present all attributes are merged. + RawAttributeMergeResult result = + EntityUtil.mergeRawAttributes( + Attributes.builder().put("a", 1).put("b", 1).build(), + Attributes.builder().put("b", 2).put("c", 2).build(), + Collections.emptyList()); + assertThat(result.getConflicts()).isEmpty(); + assertThat(result.getAttributes()) + .hasSize(3) + .containsEntry("a", 1) + .containsEntry("b", 2) + .containsEntry("c", 2); + } + + @Test + void testRawAttributeMerge_entity_with_conflict() { + // When an entity conflicts with incoming raw attributes, we need to call out that conflict + // so resource merge logic can remove the entity from resource. + RawAttributeMergeResult result = + EntityUtil.mergeRawAttributes( + Attributes.builder().put("a", 1).put("b", 1).build(), + Attributes.builder().put("b", 2).put("c", 2).build(), + Arrays.asList( + Entity.builder("c").withId(Attributes.builder().put("c", 1).build()).build())); + assertThat(result.getConflicts()).satisfiesExactly(e -> assertThat(e.getType()).isEqualTo("c")); + assertThat(result.getAttributes()) + .hasSize(3) + .containsEntry("a", 1) + .containsEntry("b", 2) + .containsEntry("c", 2); + } + + @Test + void testAddEntity_reflection() { + Resource result = + EntityUtil.addEntity( + Resource.builder(), + Entity.builder("a").withId(Attributes.builder().put("a", 1).build()).build()) + .build(); + assertThat(EntityUtil.getEntities(result)) + .satisfiesExactlyInAnyOrder(e -> assertThat(e.getType()).isEqualTo("a")); + } + + @Test + void testAddAllEntity_reflection() { + Resource result = + EntityUtil.addAllEntity( + Resource.builder(), + Arrays.asList( + Entity.builder("a").withId(Attributes.builder().put("a", 1).build()).build(), + Entity.builder("b").withId(Attributes.builder().put("b", 1).build()).build())) + .build(); + assertThat(EntityUtil.getEntities(result)) + .satisfiesExactlyInAnyOrder( + e -> assertThat(e.getType()).isEqualTo("a"), + e -> assertThat(e.getType()).isEqualTo("b")); + } +} From 2ccb30053a9812abcecec4155906db649945f9e6 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Fri, 24 Apr 2026 21:43:47 +0000 Subject: [PATCH 03/22] Add more tests and entity detectors. --- .../resources/ServiceEntityDetector.java | 48 ++++++++++++++++ .../resources/TelemetrySdkEntityDetector.java | 45 +++++++++++++++ .../internal/ExtendedEntityUtil.java | 22 +++++-- .../resources/ServiceEntityDetectorTest.java | 57 +++++++++++++++++++ .../TelemetrySdkEntityDetectorTest.java | 40 +++++++++++++ 5 files changed, 206 insertions(+), 6 deletions(-) create mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java create mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java create mode 100644 sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java create mode 100644 sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java new file mode 100644 index 00000000000..f2381dee739 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.Arrays; +import java.util.Collection; +import java.util.UUID; + +/** Detects `service` and `service.instance` entities. */ +public class ServiceEntityDetector implements EntityDetector { + // TODO - Pull this in from semconv. + private static final String SCHEMA_URL = "https://opentelemetry.io/schemas/1.40.0"; + private static final String SERVICE_TYPE = "service"; + private static final String SERVICE_INSTANCE_TYPE = "service.instance"; + + private static final AttributeKey SERVICE_NAME = AttributeKey.stringKey("service.name"); + public static final AttributeKey SERVICE_INSTANCE_ID = + AttributeKey.stringKey("service.instance.id"); + + // multiple calls to this detector provider should return the same value + private static final String RANDOM = UUID.randomUUID().toString(); + + @Override + public Collection detect(ConfigProperties config) { + String serviceName = config.getString("otel.service.name"); + + return Arrays.asList( + Entity.builder(SERVICE_TYPE) + .setIdentity(Attributes.builder().put(SERVICE_NAME, serviceName).build()) + // TODO: Add other service descriptive attributes. + .setSchemaUrl(SCHEMA_URL) + .build(), + Entity.builder(SERVICE_INSTANCE_TYPE) + .setIdentity( + Attributes.builder() + // TODO: pull from env variable if needed. + .put(SERVICE_INSTANCE_ID, RANDOM) + .build()) + .setSchemaUrl(SCHEMA_URL) + .build()); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java new file mode 100644 index 00000000000..11fcbad4013 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.common.internal.OtelVersion; +import java.util.Collection; +import java.util.Collections; + +/** + * Detection for {@code telemetry.sdk} entity. + * + *

See: teleemtry.sdk entity + */ +public final class TelemetrySdkEntityDetector implements EntityDetector { + private static final String SCHEMA_URL = "https://opentelemetry.io/schemas/1.40.0"; + private static final String ENTITY_TYPE = "telemetry.sdk"; + private static final AttributeKey TELEMETRY_SDK_LANGUAGE = + AttributeKey.stringKey("telemetry.sdk.language"); + private static final AttributeKey TELEMETRY_SDK_NAME = + AttributeKey.stringKey("telemetry.sdk.name"); + private static final AttributeKey TELEMETRY_SDK_VERSION = + AttributeKey.stringKey("telemetry.sdk.version"); + + @Override + public Collection detect(ConfigProperties config) { + return Collections.singletonList( + Entity.builder(ENTITY_TYPE) + .setSchemaUrl(SCHEMA_URL) + .setIdentity( + Attributes.builder() + .put(TELEMETRY_SDK_NAME, "opentelemetry") + .put(TELEMETRY_SDK_LANGUAGE, "java") + .build()) + .setDescription( + Attributes.builder().put(TELEMETRY_SDK_VERSION, OtelVersion.VERSION).build()) + .build()); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java index f856983cded..d3ae43da500 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java @@ -5,12 +5,14 @@ package io.opentelemetry.sdk.extension.incubator.resources.internal; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.extension.incubator.resources.Entity; +import io.opentelemetry.sdk.extension.incubator.resources.EntityDetector; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.ResourceBuilder; import io.opentelemetry.sdk.resources.internal.EntityBuilder; import io.opentelemetry.sdk.resources.internal.EntityUtil; import java.util.Collection; -import java.util.stream.Collectors; /** * This class is internal and experimental. Its APIs are unstable and can change at any time. Its @@ -21,7 +23,7 @@ public final class ExtendedEntityUtil { private ExtendedEntityUtil() {} /** Convert between the incubator API entity and the internal-implementation SDK entity. */ - public static io.opentelemetry.sdk.resources.internal.Entity convertEntity(Entity entity) { + static io.opentelemetry.sdk.resources.internal.Entity convertEntity(Entity entity) { SdkEntity api = (SdkEntity) entity; EntityBuilder builder = io.opentelemetry.sdk.resources.internal.Entity.builder(api.getType()); if (api.getSchemaUrl() != null) { @@ -32,9 +34,17 @@ public static io.opentelemetry.sdk.resources.internal.Entity convertEntity(Entit return builder.build(); } - /** Constructs a resource from a prioritized list of entities. */ - public static Resource createResource(Collection entities) { - return EntityUtil.createResource( - entities.stream().map(ExtendedEntityUtil::convertEntity).collect(Collectors.toList())); + /** Runs a set of EntityDetectors (in priority order) and merges the results into a Resource. */ + public static Resource runDetection( + Collection detectors, ConfigProperties config) { + ResourceBuilder builder = Resource.builder(); + for (EntityDetector detector : detectors) { + for (Entity entity : detector.detect(config)) { + if (entity != null) { + EntityUtil.addEntity(builder, convertEntity(entity)); + } + } + } + return builder.build(); } } diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java new file mode 100644 index 00000000000..40018d77fb7 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.extension.incubator.resources.internal.SdkEntity; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ServiceEntityDetectorTest { + + @Test + void testDetect() { + ServiceEntityDetector detector = new ServiceEntityDetector(); + Collection entities = + detector.detect( + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.service.name", "my-service"))); + + assertThat(entities).hasSize(2); + List list = new ArrayList<>(entities); + + SdkEntity serviceEntity = (SdkEntity) list.get(0); + assertThat(serviceEntity.getType()).isEqualTo("service"); + assertThat(serviceEntity.getIdentity()) + .isEqualTo(Attributes.builder().put("service.name", "my-service").build()); + assertThat(serviceEntity.getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.40.0"); + + SdkEntity serviceInstanceEntity = (SdkEntity) list.get(1); + assertThat(serviceInstanceEntity.getType()).isEqualTo("service.instance"); + assertThat(serviceInstanceEntity.getIdentity().get(ServiceEntityDetector.SERVICE_INSTANCE_ID)) + .isNotNull() + .isNotEmpty(); + assertThat(serviceInstanceEntity.getSchemaUrl()) + .isEqualTo("https://opentelemetry.io/schemas/1.40.0"); + + // Verify that another call returns the same instance ID (static final RANDOM) + Collection entities2 = + detector.detect( + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.service.name", "my-service"))); + List list2 = new ArrayList<>(entities2); + SdkEntity serviceInstanceEntity2 = (SdkEntity) list2.get(1); + assertThat(serviceInstanceEntity2.getIdentity().get(ServiceEntityDetector.SERVICE_INSTANCE_ID)) + .isEqualTo( + serviceInstanceEntity.getIdentity().get(ServiceEntityDetector.SERVICE_INSTANCE_ID)); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java new file mode 100644 index 00000000000..8e74bfc8065 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.common.internal.OtelVersion; +import io.opentelemetry.sdk.extension.incubator.resources.internal.SdkEntity; +import java.util.Collection; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class TelemetrySdkEntityDetectorTest { + + @Test + void testDetect() { + TelemetrySdkEntityDetector detector = new TelemetrySdkEntityDetector(); + Collection entities = + detector.detect(DefaultConfigProperties.createFromMap(Collections.emptyMap())); + + assertThat(entities).hasSize(1); + SdkEntity entity = (SdkEntity) entities.iterator().next(); + + assertThat(entity.getType()).isEqualTo("telemetry.sdk"); + assertThat(entity.getIdentity()) + .isEqualTo( + Attributes.builder() + .put("telemetry.sdk.name", "opentelemetry") + .put("telemetry.sdk.language", "java") + .build()); + assertThat(entity.getDescription()) + .isEqualTo(Attributes.builder().put("telemetry.sdk.version", OtelVersion.VERSION).build()); + assertThat(entity.getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.40.0"); + } +} From 509a7b91c59a486984826ba5f06deebca818ff67 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 9 Jun 2026 14:19:11 +0000 Subject: [PATCH 04/22] First attempt to unify Entities with SDK Configuration. - Create new Entity Detectors that are completely separate from existing reosurce detectors so entity must be FULL opt-in before behavior changes, even if OTLP is non-breaking. - Wire "either/or" scenarios into resource factory and declarative config. TODOs we need to sort out: - Features of ResourceConfiguration spec that interact poorly with entities, e.g. attribute-specific filters that aren't entity aware, raw attribute definitions. - Adding `env` as a supported detector for reading in Entity env variable propagation. - Dealing with oddities of ResourceBuilder/Factory wanting to only use attributes in Java - we worked around it for now. --- .../autoconfigure/IncubatingEntityUtil.java | 50 ++++++++++++++ .../autoconfigure/ResourceConfiguration.java | 56 +++++++++++---- .../DeclarativeConfigurationTest.java | 55 +++++++++++++++ .../fileconfig/DeclarativeConfigContext.java | 12 ++++ .../incubator/fileconfig/ResourceFactory.java | 68 +++++++++++++------ .../incubator/resources/EntityDetector.java | 8 +++ .../resources/EnvEntityDetector.java | 7 +- .../resources/ServiceEntityDetector.java | 5 ++ .../resources/TelemetrySdkEntityDetector.java | 5 ++ ...tension.incubator.resources.EntityDetector | 3 + .../DeclarativeConfigurationCreateTest.java | 2 +- 11 files changed, 236 insertions(+), 35 deletions(-) create mode 100644 sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java create mode 100644 sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.resources.EntityDetector diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java new file mode 100644 index 00000000000..fc4e08c6f6c --- /dev/null +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.extension.incubator.resources.EntityDetector; +import io.opentelemetry.sdk.extension.incubator.resources.internal.ExtendedEntityUtil; +import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; + +final class IncubatingEntityUtil { + + private IncubatingEntityUtil() {} + + @Nullable + static Resource configureEntityResource( + ConfigProperties config, + SpiHelper spiHelper, + Set enabledProviders, + Set disabledProviders) { + + List detectors = new ArrayList<>(); + for (EntityDetector detector : spiHelper.loadOrdered(EntityDetector.class)) { + String fqcn = detector.getClass().getName(); + String shortName = detector.getName(); + if (!enabledProviders.isEmpty() + && !enabledProviders.contains(fqcn) + && !enabledProviders.contains(shortName)) { + continue; + } + if (disabledProviders.contains(fqcn) || disabledProviders.contains(shortName)) { + continue; + } + detectors.add(detector); + } + + if (detectors.isEmpty()) { + return null; + } + + return ExtendedEntityUtil.runDetection(detectors, config); + } +} diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java index 9f1a1c4d03c..840af3b9807 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java @@ -26,6 +26,22 @@ */ public final class ResourceConfiguration { + private static final boolean ENTITY_INCUBATOR_AVAILABLE; + + static { + boolean incubatorAvailable = false; + try { + Class.forName( + "io.opentelemetry.sdk.extension.incubator.resources.EntityDetector", + false, + ResourceConfiguration.class.getClassLoader()); + incubatorAvailable = true; + } catch (ClassNotFoundException e) { + // Not available + } + ENTITY_INCUBATOR_AVAILABLE = incubatorAvailable; + } + // Visible for testing static final String DISABLED_ATTRIBUTE_KEYS = "otel.resource.disabled.keys"; static final String ENABLED_RESOURCE_PROVIDERS = "otel.java.enabled.resource.providers"; @@ -66,23 +82,35 @@ static Resource configureResource( Set enabledProviders = new HashSet<>(config.getList(ENABLED_RESOURCE_PROVIDERS)); Set disabledProviders = new HashSet<>(config.getList(DISABLED_RESOURCE_PROVIDERS)); - for (ResourceProvider resourceProvider : spiHelper.loadOrdered(ResourceProvider.class)) { - if (!enabledProviders.isEmpty() - && !enabledProviders.contains(resourceProvider.getClass().getName())) { - continue; - } - if (disabledProviders.contains(resourceProvider.getClass().getName())) { - continue; + // If Entity experiment is enabled, we use a new flow to instantiate resources. + boolean entitiesEnabled = config.getBoolean("otel.experimental.entities.enabled", false); + if (entitiesEnabled && ENTITY_INCUBATOR_AVAILABLE) { + Resource entityResource = + IncubatingEntityUtil.configureEntityResource( + config, spiHelper, enabledProviders, disabledProviders); + if (entityResource != null) { + result = entityResource; } - if (resourceProvider instanceof ConditionalResourceProvider - && !((ConditionalResourceProvider) resourceProvider).shouldApply(config, result)) { - continue; + } else { + + for (ResourceProvider resourceProvider : spiHelper.loadOrdered(ResourceProvider.class)) { + if (!enabledProviders.isEmpty() + && !enabledProviders.contains(resourceProvider.getClass().getName())) { + continue; + } + if (disabledProviders.contains(resourceProvider.getClass().getName())) { + continue; + } + if (resourceProvider instanceof ConditionalResourceProvider + && !((ConditionalResourceProvider) resourceProvider).shouldApply(config, result)) { + continue; + } + result = result.merge(resourceProvider.createResource(config)); } - result = result.merge(resourceProvider.createResource(config)); - } - - result = filterAttributes(result, config); + // TODO(jsuereth): Should filter attributes be used with entities? + result = filterAttributes(result, config); + } return resourceCustomizer.apply(result, config); } diff --git a/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java b/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java index 64d3b68ef15..2caffc05dea 100644 --- a/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java +++ b/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java @@ -18,6 +18,7 @@ import io.github.netmikey.logunit.api.LogCapturer; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.incubator.ExtendedOpenTelemetry; import io.opentelemetry.api.incubator.config.ConfigProvider; import io.opentelemetry.api.incubator.config.InstrumentationConfigUtil; @@ -34,6 +35,8 @@ import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.Entity; +import io.opentelemetry.sdk.resources.internal.EntityUtil; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import java.io.IOException; @@ -41,7 +44,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -243,4 +249,53 @@ void configFile_ConfigProvider() { .getScalarList("request_captured_headers", String.class)) .isEqualTo(Arrays.asList("Content-Type", "Accept")); } + + @Test + void entitiesEnabled() { + ConfigProperties config = + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.experimental.entities.enabled", "true")); + + AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk = + AutoConfiguredOpenTelemetrySdk.builder().setConfig(config).build(); + OpenTelemetrySdk openTelemetrySdk = autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk(); + cleanup.addCloseable(openTelemetrySdk); + + Resource resource = autoConfiguredOpenTelemetrySdk.getResource(); + + Collection entities = EntityUtil.getEntities(resource); + assertThat(entities) + .anyMatch( + e -> + e.getType().equals("telemetry.sdk") + && "opentelemetry" + .equals(e.getId().get(AttributeKey.stringKey("telemetry.sdk.name"))) + && "java" + .equals(e.getId().get(AttributeKey.stringKey("telemetry.sdk.language")))); + } + + @Test + void entitiesEnabled_WithEnabledProviders() { + Map props = new HashMap<>(); + props.put("otel.experimental.entities.enabled", "true"); + props.put("otel.service.name", "my-filtered-service"); + props.put("otel.java.enabled.resource.providers", "service"); + ConfigProperties config = DefaultConfigProperties.createFromMap(props); + + AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk = + AutoConfiguredOpenTelemetrySdk.builder().setConfig(config).build(); + OpenTelemetrySdk openTelemetrySdk = autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk(); + cleanup.addCloseable(openTelemetrySdk); + + Resource resource = autoConfiguredOpenTelemetrySdk.getResource(); + + Collection entities = EntityUtil.getEntities(resource); + assertThat(entities) + .anyMatch( + e -> + e.getType().equals("service") + && "my-filtered-service" + .equals(e.getId().get(AttributeKey.stringKey("service.name")))); + assertThat(entities).noneMatch(e -> e.getType().equals("telemetry.sdk")); + } } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigContext.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigContext.java index f6136a240f4..b246f3d1812 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigContext.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigContext.java @@ -131,6 +131,18 @@ private InternalTelemetryVersion getInternalTelemetryVersion() { } } + boolean isEntitiesEnabled() { + if (configProvider == null) { + return false; + } + // TODO - Check with Jack if this is the right thing to do for config flags, + // of if we want to put this in the experimental resource config space. + return Boolean.TRUE.equals( + configProvider + .getInstrumentationConfig("otel_sdk") + .getBoolean("experimental_entities_enabled")); + } + /** * Find a registered {@link ComponentProvider} with {@link ComponentProvider#getType()} matching * {@code type}, {@link ComponentProvider#getName()} matching {@code name}, and call {@link diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ResourceFactory.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ResourceFactory.java index 1a46ce5af8a..5c7af1cbe46 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ResourceFactory.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ResourceFactory.java @@ -15,8 +15,11 @@ import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ExperimentalResourceDetectorModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.IncludeExcludeModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ResourceModel; +import io.opentelemetry.sdk.extension.incubator.resources.EntityDetector; +import io.opentelemetry.sdk.extension.incubator.resources.internal.ExtendedEntityUtil; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.ResourceBuilder; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Predicate; @@ -37,27 +40,54 @@ public Resource create(ResourceModel model, DeclarativeConfigContext context) { ExperimentalResourceDetectionModel detectionModel = model.getDetectionDevelopment(); if (detectionModel != null) { - ResourceBuilder detectedResourceBuilder = Resource.builder(); - - List detectorModels = detectionModel.getDetectors(); - if (detectorModels != null) { - for (ExperimentalResourceDetectorModel detectorModel : detectorModels) { - detectedResourceBuilder.putAll( - ResourceDetectorFactory.getInstance().create(detectorModel, context)); + if (context.isEntitiesEnabled()) { + List detectorModels = detectionModel.getDetectors(); + if (detectorModels != null) { + List detectors = new ArrayList<>(); + for (ExperimentalResourceDetectorModel detectorModel : detectorModels) { + ConfigKeyValue detectorKeyValue = + FileConfigUtil.validateSingleKeyValue(context, detectorModel, "resource detector"); + String detectorName = detectorKeyValue.getKey(); + + for (EntityDetector detector : context.load(EntityDetector.class)) { + if (detector.getName().equals(detectorName) + || detector.getClass().getName().equals(detectorName)) { + detectors.add(detector); + } + } + } + if (!detectors.isEmpty()) { + Resource detectedEntityResource = + ExtendedEntityUtil.runDetection( + detectors, DefaultConfigProperties.createFromMap(Collections.emptyMap())); + builder = detectedEntityResource.toBuilder(); + } + } + } else { + ResourceBuilder detectedResourceBuilder = Resource.builder(); + + List detectorModels = detectionModel.getDetectors(); + if (detectorModels != null) { + for (ExperimentalResourceDetectorModel detectorModel : detectorModels) { + detectedResourceBuilder.putAll( + ResourceDetectorFactory.getInstance().create(detectorModel, context)); + } } - } - IncludeExcludeModel attributesIncludeExcludeModel = detectionModel.getAttributes(); - Predicate detectorAttributeFilter = - attributesIncludeExcludeModel == null - ? ResourceFactory::matchAll - : IncludeExcludeFactory.getInstance().create(attributesIncludeExcludeModel, context); - Attributes filteredDetectedAttributes = - detectedResourceBuilder.build().getAttributes().toBuilder() - .removeIf(attributeKey -> !detectorAttributeFilter.test(attributeKey.getKey())) - .build(); - - builder.putAll(filteredDetectedAttributes); + IncludeExcludeModel attributesIncludeExcludeModel = detectionModel.getAttributes(); + Predicate detectorAttributeFilter = + attributesIncludeExcludeModel == null + ? ResourceFactory::matchAll + : IncludeExcludeFactory.getInstance() + .create(attributesIncludeExcludeModel, context); + + Attributes filteredDetectedAttributes = + detectedResourceBuilder.build().getAttributes().toBuilder() + .removeIf(attributeKey -> !detectorAttributeFilter.test(attributeKey.getKey())) + .build(); + + builder.putAll(filteredDetectedAttributes); + } } String attributeList = model.getAttributesList(); diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java index d6696d6643a..d2558a64468 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java @@ -21,4 +21,12 @@ public interface EntityDetector extends Ordered { * @param config the configuration to use for detection */ Collection detect(ConfigProperties config); + + /** + * Returns the name of the detector (e.g., "service", "env") or the fully qualified class name. + * Used for configuration filtering by-name. + */ + default String getName() { + return getClass().getName(); + } } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java index 6a4ea541af6..b866eb2434c 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java @@ -22,11 +22,16 @@ * See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/entities/entity-propagation.md * for more information about the OTEL_ENTITIES environment variable. */ -class EnvEntityDetector implements EntityDetector { +public class EnvEntityDetector implements EntityDetector { private static final Logger logger = Logger.getLogger(EnvEntityDetector.class.getName()); private static final String PROPERTY_KEY = "otel.entities"; + @Override + public String getName() { + return "env"; + } + @Override public Collection detect(ConfigProperties config) { String entitiesStr = config.getString(PROPERTY_KEY); diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java index f2381dee739..77494e23586 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java @@ -26,6 +26,11 @@ public class ServiceEntityDetector implements EntityDetector { // multiple calls to this detector provider should return the same value private static final String RANDOM = UUID.randomUUID().toString(); + @Override + public String getName() { + return "service"; + } + @Override public Collection detect(ConfigProperties config) { String serviceName = config.getString("otel.service.name"); diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java index 11fcbad4013..d1d7a3b5580 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java @@ -28,6 +28,11 @@ public final class TelemetrySdkEntityDetector implements EntityDetector { private static final AttributeKey TELEMETRY_SDK_VERSION = AttributeKey.stringKey("telemetry.sdk.version"); + @Override + public String getName() { + return "telemetry.sdk"; + } + @Override public Collection detect(ConfigProperties config) { return Collections.singletonList( diff --git a/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.resources.EntityDetector b/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.resources.EntityDetector new file mode 100644 index 00000000000..5941d4c812b --- /dev/null +++ b/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.resources.EntityDetector @@ -0,0 +1,3 @@ +io.opentelemetry.sdk.extension.incubator.resources.ServiceEntityDetector +io.opentelemetry.sdk.extension.incubator.resources.EnvEntityDetector +io.opentelemetry.sdk.extension.incubator.resources.TelemetrySdkEntityDetector diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java index f1eb6a2e82b..9abf22ff39f 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java @@ -182,7 +182,7 @@ void create_ModelCustomizer() { .getSdk(); assertThat(sdk.toString()) .contains( - "resource=Resource{schemaUrl=null, attributes={" + "resource=Resource{schemaUrl=null, rawAttributes={" + "color=\"blue\", " + "foo=\"bar\", " + "service.name=\"unknown_service:java\", " From bdcc5d7d1fe2d7bb999f93f7fcd35a250fde470e Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 25 Jun 2026 01:15:47 +0000 Subject: [PATCH 05/22] Merge Jack's change to not expose two Entity APIs. --- .../autoconfigure/IncubatingEntityUtil.java | 14 +++- .../declarativeconfig/ResourceFactory.java | 20 ++++- .../extension/incubator/resources/Entity.java | 19 ----- .../incubator/resources/EntityBuilder.java | 24 ------ .../incubator/resources/EntityDetector.java | 1 + .../resources/EnvEntityDetector.java | 6 +- .../resources/ServiceEntityDetector.java | 5 +- .../resources/TelemetrySdkEntityDetector.java | 5 +- .../internal/ExtendedEntityUtil.java | 50 ------------- .../resources/internal/SdkEntity.java | 74 ------------------- .../resources/internal/SdkEntityBuilder.java | 60 --------------- .../resources/EnvEntityDetectorTest.java | 27 +++---- .../resources/ServiceEntityDetectorTest.java | 17 ++--- .../TelemetrySdkEntityDetectorTest.java | 6 +- 14 files changed, 64 insertions(+), 264 deletions(-) delete mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/Entity.java delete mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityBuilder.java delete mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java delete mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntity.java delete mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntityBuilder.java diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java index fc4e08c6f6c..80eebcd24ae 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java @@ -8,8 +8,10 @@ import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.extension.incubator.resources.EntityDetector; -import io.opentelemetry.sdk.extension.incubator.resources.internal.ExtendedEntityUtil; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.ResourceBuilder; +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; @@ -45,6 +47,14 @@ static Resource configureEntityResource( return null; } - return ExtendedEntityUtil.runDetection(detectors, config); + ResourceBuilder builder = Resource.builder(); + for (EntityDetector detector : detectors) { + for (Entity entity : detector.detect(config)) { + if (entity != null) { + EntityUtil.addEntity(builder, entity); + } + } + } + return builder.build(); } } 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 91a08414082..0b157d1ca41 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 @@ -16,9 +16,10 @@ import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.ResourceModel; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.extension.incubator.resources.EntityDetector; -import io.opentelemetry.sdk.extension.incubator.resources.internal.ExtendedEntityUtil; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.ResourceBuilder; +import io.opentelemetry.sdk.resources.internal.Entity; +import io.opentelemetry.sdk.resources.internal.EntityUtil; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -57,9 +58,7 @@ public Resource create(ResourceModel model, DeclarativeConfigContext context) { } } if (!detectors.isEmpty()) { - Resource detectedEntityResource = - ExtendedEntityUtil.runDetection( - detectors, DefaultConfigProperties.createFromMap(Collections.emptyMap())); + Resource detectedEntityResource = detectEntityResource(detectors); builder = detectedEntityResource.toBuilder(); } } @@ -112,6 +111,19 @@ public Resource create(ResourceModel model, DeclarativeConfigContext context) { return builder.build(); } + private static Resource detectEntityResource(List detectors) { + ResourceBuilder builder = Resource.builder(); + for (EntityDetector detector : detectors) { + for (Entity entity : + detector.detect(DefaultConfigProperties.createFromMap(Collections.emptyMap()))) { + if (entity != null) { + EntityUtil.addEntity(builder, entity); + } + } + } + return builder.build(); + } + private static boolean matchAll(String attributeKey) { return true; } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/Entity.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/Entity.java deleted file mode 100644 index b5468e14c7a..00000000000 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/Entity.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources; - -import io.opentelemetry.sdk.extension.incubator.resources.internal.SdkEntityBuilder; - -/** An instance of an Entity. */ -public interface Entity { - /** Constructs a new builder for creating Entities. */ - static EntityBuilder builder(String entityType) { - return new SdkEntityBuilder(entityType); - } - - /** Converts this entity to a builder. */ - EntityBuilder toBuilder(); -} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityBuilder.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityBuilder.java deleted file mode 100644 index fc95ec47210..00000000000 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityBuilder.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources; - -import io.opentelemetry.api.common.Attributes; - -/** A builder of {@link Entity}. */ -public interface EntityBuilder { - - /** Sets the schema_url of the Entity. */ - EntityBuilder setSchemaUrl(String schemaUrl); - - /** Sets the identity of the Entity. */ - EntityBuilder setIdentity(Attributes identity); - - /** Sets the description of the Entity. */ - EntityBuilder setDescription(Attributes description); - - /** Builds an entity. */ - Entity build(); -} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java index d2558a64468..a2b37854ef8 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java @@ -8,6 +8,7 @@ import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.Ordered; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.Entity; import java.util.Collection; /** diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java index b866eb2434c..561b7489210 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java @@ -8,6 +8,8 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.resources.internal.Entity; +import io.opentelemetry.sdk.resources.internal.EntityBuilder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; @@ -363,9 +365,9 @@ private void putAttr() { /** Finishes building the current entity and adds it to the parsed list. */ private void buildAndAddEntity() { if (currentType != null && !currentType.isEmpty() && !currentIdAttrs.isEmpty()) { - EntityBuilder builder = Entity.builder(currentType).setIdentity(currentIdAttrs); + EntityBuilder builder = Entity.builder(currentType).withId(currentIdAttrs); if (!currentDescAttrs.isEmpty()) { - builder.setDescription(currentDescAttrs); + builder.withDescription(currentDescAttrs); } if (currentSchemaUrl != null && !currentSchemaUrl.isEmpty()) { builder.setSchemaUrl(currentSchemaUrl); diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java index 77494e23586..f01d00f11dd 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java @@ -8,6 +8,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.resources.internal.Entity; import java.util.Arrays; import java.util.Collection; import java.util.UUID; @@ -37,12 +38,12 @@ public Collection detect(ConfigProperties config) { return Arrays.asList( Entity.builder(SERVICE_TYPE) - .setIdentity(Attributes.builder().put(SERVICE_NAME, serviceName).build()) + .withId(Attributes.builder().put(SERVICE_NAME, serviceName).build()) // TODO: Add other service descriptive attributes. .setSchemaUrl(SCHEMA_URL) .build(), Entity.builder(SERVICE_INSTANCE_TYPE) - .setIdentity( + .withId( Attributes.builder() // TODO: pull from env variable if needed. .put(SERVICE_INSTANCE_ID, RANDOM) diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java index d1d7a3b5580..6341c0bc1e3 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java @@ -9,6 +9,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.common.internal.OtelVersion; +import io.opentelemetry.sdk.resources.internal.Entity; import java.util.Collection; import java.util.Collections; @@ -38,12 +39,12 @@ public Collection detect(ConfigProperties config) { return Collections.singletonList( Entity.builder(ENTITY_TYPE) .setSchemaUrl(SCHEMA_URL) - .setIdentity( + .withId( Attributes.builder() .put(TELEMETRY_SDK_NAME, "opentelemetry") .put(TELEMETRY_SDK_LANGUAGE, "java") .build()) - .setDescription( + .withDescription( Attributes.builder().put(TELEMETRY_SDK_VERSION, OtelVersion.VERSION).build()) .build()); } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java deleted file mode 100644 index d3ae43da500..00000000000 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/ExtendedEntityUtil.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources.internal; - -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.extension.incubator.resources.Entity; -import io.opentelemetry.sdk.extension.incubator.resources.EntityDetector; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.resources.ResourceBuilder; -import io.opentelemetry.sdk.resources.internal.EntityBuilder; -import io.opentelemetry.sdk.resources.internal.EntityUtil; -import java.util.Collection; - -/** - * This class is internal and experimental. Its APIs are unstable and can change at any time. Its - * APIs (or a version of them) may be promoted to the public stable API in the future, but no - * guarantees are made. - */ -public final class ExtendedEntityUtil { - private ExtendedEntityUtil() {} - - /** Convert between the incubator API entity and the internal-implementation SDK entity. */ - static io.opentelemetry.sdk.resources.internal.Entity convertEntity(Entity entity) { - SdkEntity api = (SdkEntity) entity; - EntityBuilder builder = io.opentelemetry.sdk.resources.internal.Entity.builder(api.getType()); - if (api.getSchemaUrl() != null) { - builder.setSchemaUrl(api.getSchemaUrl()); - } - builder.withId(api.getIdentity()); - builder.withDescription(api.getDescription()); - return builder.build(); - } - - /** Runs a set of EntityDetectors (in priority order) and merges the results into a Resource. */ - public static Resource runDetection( - Collection detectors, ConfigProperties config) { - ResourceBuilder builder = Resource.builder(); - for (EntityDetector detector : detectors) { - for (Entity entity : detector.detect(config)) { - if (entity != null) { - EntityUtil.addEntity(builder, convertEntity(entity)); - } - } - } - return builder.build(); - } -} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntity.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntity.java deleted file mode 100644 index bf9d7cb36a6..00000000000 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntity.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources.internal; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.sdk.extension.incubator.resources.Entity; -import io.opentelemetry.sdk.extension.incubator.resources.EntityBuilder; -import javax.annotation.Nullable; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class SdkEntity implements Entity { - - private final String entityType; - @Nullable private final String schemaUrl; - private final Attributes identity; - private final Attributes description; - - SdkEntity( - String entityType, @Nullable String schemaUrl, Attributes identity, Attributes description) { - this.entityType = entityType; - this.schemaUrl = schemaUrl; - this.identity = identity; - this.description = description; - } - - /** - * Returns the entity type string of this entity. Must not be null. - * - * @return the entity type. - */ - public String getType() { - return entityType; - } - - /** - * Returns a map of attributes that identify the entity. - * - * @return the entity identity. - */ - public Attributes getIdentity() { - return this.identity; - } - - /** - * Returns a map of attributes that describe the entity. - * - * @return the entity description. - */ - public Attributes getDescription() { - return this.description; - } - - /** - * Returns the URL of the OpenTelemetry schema used by this resource. May be null if this entity - * does not abide by schema conventions (i.e. is custom). - * - * @return An OpenTelemetry schema URL. - */ - @Nullable - public String getSchemaUrl() { - return this.schemaUrl; - } - - @Override - public EntityBuilder toBuilder() { - return new SdkEntityBuilder(this); - } -} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntityBuilder.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntityBuilder.java deleted file mode 100644 index 35baf12cfe8..00000000000 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/internal/SdkEntityBuilder.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources.internal; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.sdk.extension.incubator.resources.Entity; -import io.opentelemetry.sdk.extension.incubator.resources.EntityBuilder; -import javax.annotation.Nullable; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class SdkEntityBuilder implements EntityBuilder { - private final String entityType; - @Nullable private String schemaUrl; - private Attributes identity; - private Attributes description; - - public SdkEntityBuilder(String entityType) { - this.entityType = entityType; - this.identity = Attributes.empty(); - this.description = Attributes.empty(); - } - - SdkEntityBuilder(SdkEntity entity) { - this.entityType = entity.getType(); - this.schemaUrl = entity.getSchemaUrl(); - this.identity = entity.getIdentity(); - this.description = entity.getDescription(); - } - - @Override - public EntityBuilder setSchemaUrl(String schemaUrl) { - this.schemaUrl = schemaUrl; - return this; - } - - @Override - public EntityBuilder setIdentity(Attributes identity) { - this.identity = identity; - return this; - } - - @Override - public EntityBuilder setDescription(Attributes description) { - this.description = description; - return this; - } - - @Override - public Entity build() { - // TODO - assertions around safe entity builds. - // TODO - identity is not empty. - return new SdkEntity(entityType, schemaUrl, identity, description); - } -} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java index ff209cef067..0974daf8d23 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java @@ -9,7 +9,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; -import io.opentelemetry.sdk.extension.incubator.resources.internal.SdkEntity; +import io.opentelemetry.sdk.resources.internal.Entity; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -37,9 +37,9 @@ void testSingleEntity() { Collections.singletonMap("otel.entities", value))); assertThat(entities).hasSize(1); - SdkEntity entity = (SdkEntity) entities.iterator().next(); + Entity entity = entities.iterator().next(); assertThat(entity.getType()).isEqualTo("service"); - assertThat(entity.getIdentity()) + assertThat(entity.getId()) .isEqualTo( Attributes.builder() .put("service.name", "my-app") @@ -63,16 +63,15 @@ void testMultipleEntitiesWithSchemaUrl() { assertThat(entities).hasSize(2); List list = new ArrayList<>(entities); - SdkEntity entity1 = (SdkEntity) list.get(0); + Entity entity1 = list.get(0); assertThat(entity1.getType()).isEqualTo("service"); - assertThat(entity1.getIdentity()) + assertThat(entity1.getId()) .isEqualTo(Attributes.builder().put("service.name", "my-app").build()); assertThat(entity1.getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.21.0"); - SdkEntity entity2 = (SdkEntity) list.get(1); + Entity entity2 = list.get(1); assertThat(entity2.getType()).isEqualTo("host"); - assertThat(entity2.getIdentity()) - .isEqualTo(Attributes.builder().put("host.id", "host-123").build()); + assertThat(entity2.getId()).isEqualTo(Attributes.builder().put("host.id", "host-123").build()); assertThat(entity2.getDescription()) .isEqualTo(Attributes.builder().put("host.name", "web-server-01").build()); assertThat(entity2.getSchemaUrl()).isNull(); @@ -88,9 +87,9 @@ void testPercentDecoding() { Collections.singletonMap("otel.entities", value))); assertThat(entities).hasSize(1); - SdkEntity entity = (SdkEntity) entities.iterator().next(); + Entity entity = entities.iterator().next(); assertThat(entity.getType()).isEqualTo("service"); - assertThat(entity.getIdentity()) + assertThat(entity.getId()) .isEqualTo(Attributes.builder().put("service.name", "my,app").build()); assertThat(entity.getDescription()) .isEqualTo(Attributes.builder().put("config", "key=value[prod]").build()); @@ -119,7 +118,7 @@ void testMalformedSyntax_MissingBrace() { // Should skip the malformed one and process the valid one assertThat(entities).hasSize(1); - SdkEntity entity = (SdkEntity) entities.iterator().next(); + Entity entity = entities.iterator().next(); assertThat(entity.getType()).isEqualTo("host"); } @@ -133,8 +132,9 @@ void testMalformedSyntax_MissingBraceEnd() { Collections.singletonMap("otel.entities", value))); assertThat(entities).hasSize(1); - SdkEntity entity = (SdkEntity) entities.iterator().next(); + Entity entity = entities.iterator().next(); assertThat(entity.getType()).isEqualTo("host"); + // TODO: why no assert against id? } @Test @@ -147,7 +147,8 @@ void testMissingRequiredFields_EmptyIdentity() { Collections.singletonMap("otel.entities", value))); assertThat(entities).hasSize(1); - SdkEntity entity = (SdkEntity) entities.iterator().next(); + Entity entity = entities.iterator().next(); assertThat(entity.getType()).isEqualTo("host"); + // TODO: why no assert against id? } } diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java index 40018d77fb7..5fded820253 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java @@ -9,7 +9,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; -import io.opentelemetry.sdk.extension.incubator.resources.internal.SdkEntity; +import io.opentelemetry.sdk.resources.internal.Entity; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -29,15 +29,15 @@ void testDetect() { assertThat(entities).hasSize(2); List list = new ArrayList<>(entities); - SdkEntity serviceEntity = (SdkEntity) list.get(0); + Entity serviceEntity = list.get(0); assertThat(serviceEntity.getType()).isEqualTo("service"); - assertThat(serviceEntity.getIdentity()) + assertThat(serviceEntity.getId()) .isEqualTo(Attributes.builder().put("service.name", "my-service").build()); assertThat(serviceEntity.getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.40.0"); - SdkEntity serviceInstanceEntity = (SdkEntity) list.get(1); + Entity serviceInstanceEntity = list.get(1); assertThat(serviceInstanceEntity.getType()).isEqualTo("service.instance"); - assertThat(serviceInstanceEntity.getIdentity().get(ServiceEntityDetector.SERVICE_INSTANCE_ID)) + assertThat(serviceInstanceEntity.getId().get(ServiceEntityDetector.SERVICE_INSTANCE_ID)) .isNotNull() .isNotEmpty(); assertThat(serviceInstanceEntity.getSchemaUrl()) @@ -49,9 +49,8 @@ void testDetect() { DefaultConfigProperties.createFromMap( Collections.singletonMap("otel.service.name", "my-service"))); List list2 = new ArrayList<>(entities2); - SdkEntity serviceInstanceEntity2 = (SdkEntity) list2.get(1); - assertThat(serviceInstanceEntity2.getIdentity().get(ServiceEntityDetector.SERVICE_INSTANCE_ID)) - .isEqualTo( - serviceInstanceEntity.getIdentity().get(ServiceEntityDetector.SERVICE_INSTANCE_ID)); + Entity serviceInstanceEntity2 = list2.get(1); + assertThat(serviceInstanceEntity2.getId().get(ServiceEntityDetector.SERVICE_INSTANCE_ID)) + .isEqualTo(serviceInstanceEntity.getId().get(ServiceEntityDetector.SERVICE_INSTANCE_ID)); } } diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java index 8e74bfc8065..20c0b24d532 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java @@ -10,7 +10,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.common.internal.OtelVersion; -import io.opentelemetry.sdk.extension.incubator.resources.internal.SdkEntity; +import io.opentelemetry.sdk.resources.internal.Entity; import java.util.Collection; import java.util.Collections; import org.junit.jupiter.api.Test; @@ -24,10 +24,10 @@ void testDetect() { detector.detect(DefaultConfigProperties.createFromMap(Collections.emptyMap())); assertThat(entities).hasSize(1); - SdkEntity entity = (SdkEntity) entities.iterator().next(); + Entity entity = entities.iterator().next(); assertThat(entity.getType()).isEqualTo("telemetry.sdk"); - assertThat(entity.getIdentity()) + assertThat(entity.getId()) .isEqualTo( Attributes.builder() .put("telemetry.sdk.name", "opentelemetry") From 279b1ea3bb4d0fc34c671ebe0aaf0da1ded1ca40 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 25 Jun 2026 01:47:46 +0000 Subject: [PATCH 06/22] Fix some PR feedback. - Simplify API for using entities. - Update so we can use current resource detection unchanged with entities. - Memoize full attributes manually when creating a resource. --- .../opentelemetry-sdk-common.txt | 4 +- .../internal/otlp/EntityRefMarshalerTest.java | 4 +- .../resources/EnvEntityDetector.java | 4 +- .../resources/ServiceEntityDetector.java | 4 +- .../resources/TelemetrySdkEntityDetector.java | 4 +- .../opentelemetry/sdk/resources/Resource.java | 25 ++++----- .../sdk/resources/ResourceBuilder.java | 15 +++--- .../sdk/resources/internal/Entity.java | 2 +- .../sdk/resources/internal/EntityBuilder.java | 4 +- .../sdk/resources/internal/EntityUtil.java | 18 +------ .../sdk/resources/internal/SdkEntity.java | 2 +- .../resources/internal/SdkEntityBuilder.java | 4 +- .../sdk/resources/ResourceTest.java | 10 ++-- .../resources/internal/EntityUtilTest.java | 53 +++++++------------ .../sdk/logs/SdkLoggerProviderTest.java | 2 +- .../sdk/trace/SdkSpanBuilderTest.java | 8 ++- 16 files changed, 65 insertions(+), 98 deletions(-) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt index ac672a06bbf..bb89e1f7638 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt @@ -1,4 +1,2 @@ Comparing source compatibility of opentelemetry-sdk-common-1.64.0-SNAPSHOT.jar against opentelemetry-sdk-common-1.63.0.jar -*** MODIFIED CLASS: PUBLIC ABSTRACT io.opentelemetry.sdk.resources.Resource (not serializable) - === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 - *** MODIFIED METHOD: PUBLIC NON_ABSTRACT (<- ABSTRACT) io.opentelemetry.api.common.Attributes getAttributes() +No changes. \ No newline at end of file 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 index b5e8cad39f5..f2f7af4fc08 100644 --- 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 @@ -26,8 +26,8 @@ void toEntityRefs() { Entity e = Entity.builder("test") .setSchemaUrl("test-url") - .withDescription(Attributes.builder().put("desc.key", "desc.value").build()) - .withId(Attributes.builder().put("id.key", "id.value").build()) + .setDescription(Attributes.builder().put("desc.key", "desc.value").build()) + .setId(Attributes.builder().put("id.key", "id.value").build()) .build(); EntityRef proto = parse(EntityRef.getDefaultInstance(), EntityRefMarshaler.createForEntity(e)); assertThat(proto.getType()).isEqualTo("test"); diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java index 561b7489210..ace30b3b0d4 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java @@ -365,9 +365,9 @@ private void putAttr() { /** Finishes building the current entity and adds it to the parsed list. */ private void buildAndAddEntity() { if (currentType != null && !currentType.isEmpty() && !currentIdAttrs.isEmpty()) { - EntityBuilder builder = Entity.builder(currentType).withId(currentIdAttrs); + EntityBuilder builder = Entity.builder(currentType).setId(currentIdAttrs); if (!currentDescAttrs.isEmpty()) { - builder.withDescription(currentDescAttrs); + builder.setDescription(currentDescAttrs); } if (currentSchemaUrl != null && !currentSchemaUrl.isEmpty()) { builder.setSchemaUrl(currentSchemaUrl); diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java index f01d00f11dd..2ef1384474b 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java @@ -38,12 +38,12 @@ public Collection detect(ConfigProperties config) { return Arrays.asList( Entity.builder(SERVICE_TYPE) - .withId(Attributes.builder().put(SERVICE_NAME, serviceName).build()) + .setId(Attributes.builder().put(SERVICE_NAME, serviceName).build()) // TODO: Add other service descriptive attributes. .setSchemaUrl(SCHEMA_URL) .build(), Entity.builder(SERVICE_INSTANCE_TYPE) - .withId( + .setId( Attributes.builder() // TODO: pull from env variable if needed. .put(SERVICE_INSTANCE_ID, RANDOM) diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java index 6341c0bc1e3..91b9128e166 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java @@ -39,12 +39,12 @@ public Collection detect(ConfigProperties config) { return Collections.singletonList( Entity.builder(ENTITY_TYPE) .setSchemaUrl(SCHEMA_URL) - .withId( + .setId( Attributes.builder() .put(TELEMETRY_SDK_NAME, "opentelemetry") .put(TELEMETRY_SDK_LANGUAGE, "java") .build()) - .withDescription( + .setDescription( Attributes.builder().put(TELEMETRY_SDK_VERSION, OtelVersion.VERSION).build()) .build()); } 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 163104bd8ff..91fba3b6361 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 @@ -117,7 +117,16 @@ public static Resource create(Attributes attributes, @Nullable String schemaUrl) static Resource create( Attributes attributes, @Nullable String schemaUrl, Collection entities) { AttributeCheckUtil.checkAttributes(Objects.requireNonNull(attributes, "attributes")); - return new AutoValue_Resource(schemaUrl, attributes, entities); + // 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, attributes, entities, fullAttributes.build()); } /** @@ -148,19 +157,7 @@ static Resource create( * * @return a map of attributes. */ - // @Memoized - This breaks nullaway. - public Attributes getAttributes() { - AttributesBuilder result = Attributes.builder(); - getEntities() - .forEach( - e -> { - result.putAll(e.getId()); - result.putAll(e.getDescription()); - }); - // In merge rules, raw comes last, so we return these last. - result.putAll(getRawAttributes()); - return result.build(); - } + public abstract Attributes getAttributes(); /** * Returns the value for a given resource attribute key. 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 2c47b3a927b..5d54944390d 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 @@ -11,7 +11,6 @@ import io.opentelemetry.sdk.resources.internal.Entity; import io.opentelemetry.sdk.resources.internal.EntityUtil; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Set; import java.util.function.Predicate; @@ -177,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.getRawAttributes()); } return this; } @@ -218,14 +221,8 @@ public Resource build() { } /** Appends a new entity on to the end of the list of entities. */ - ResourceBuilder add(Entity e) { + ResourceBuilder addEntity(Entity e) { this.entities.add(e); return this; } - - /** Appends a new collection of entities on to the end of the list of entities. */ - ResourceBuilder addAll(Collection entities) { - this.entities.addAll(entities); - return this; - } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/Entity.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/Entity.java index 40cf63e126b..c088c61979e 100644 --- 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 @@ -67,7 +67,7 @@ public interface Entity { * * @param entityType the entity type string of this entity. */ - public static EntityBuilder builder(String entityType) { + static EntityBuilder builder(String entityType) { return SdkEntity.builder(entityType); } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityBuilder.java index bb2b21c998b..52a910aec9d 100644 --- 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 @@ -29,7 +29,7 @@ public interface EntityBuilder { * @param description The attributes that describe the Entity. * @return this */ - EntityBuilder withDescription(Attributes description); + EntityBuilder setDescription(Attributes description); /** * Modify the identifying attributes of this Entity. @@ -37,7 +37,7 @@ public interface EntityBuilder { * @param id The identifying attributes. * @return this */ - EntityBuilder withId(Attributes id); + EntityBuilder setId(Attributes id); /** Create the {@link Entity} from this. */ Entity build(); diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java index edd307095a3..e6f16934031 100644 --- 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 @@ -77,21 +77,7 @@ static Resource createResourceRaw( /** Appends a new entity on to the end of the list of entities. */ public static ResourceBuilder addEntity(ResourceBuilder rb, Entity e) { try { - Method method = ResourceBuilder.class.getDeclaredMethod("add", Entity.class); - if (method != null) { - method.setAccessible(true); - method.invoke(rb, e); - } - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { - logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", ex); - } - return rb; - } - - /** Appends a new collection of entities on to the end of the list of entities. */ - public static ResourceBuilder addAllEntity(ResourceBuilder rb, Collection e) { - try { - Method method = ResourceBuilder.class.getDeclaredMethod("addAll", Collection.class); + Method method = ResourceBuilder.class.getDeclaredMethod("addEntity", Entity.class); if (method != null) { method.setAccessible(true); method.invoke(rb, e); @@ -270,7 +256,7 @@ static Collection mergeEntities(Collection base, Collection added = Arrays.asList( Entity.builder("a") .setSchemaUrl("one") - .withId(Attributes.builder().put("a.id", "a").build()) - .withDescription(Attributes.builder().put("a.desc2", "b").build()) + .setId(Attributes.builder().put("a.id", "a").build()) + .setDescription(Attributes.builder().put("a.desc2", "b").build()) .build()); Collection merged = EntityUtil.mergeEntities(base, added); assertThat(merged).hasSize(1); @@ -53,15 +53,15 @@ void testMerge_entities_same_types_and_id_different_schema() { Arrays.asList( Entity.builder("a") .setSchemaUrl("one") - .withId(Attributes.builder().put("a.id", "a").build()) - .withDescription(Attributes.builder().put("a.desc1", "a").build()) + .setId(Attributes.builder().put("a.id", "a").build()) + .setDescription(Attributes.builder().put("a.desc1", "a").build()) .build()); Collection added = Arrays.asList( Entity.builder("a") .setSchemaUrl("two") - .withId(Attributes.builder().put("a.id", "a").build()) - .withDescription(Attributes.builder().put("a.desc2", "b").build()) + .setId(Attributes.builder().put("a.id", "a").build()) + .setDescription(Attributes.builder().put("a.desc2", "b").build()) .build()); Collection merged = EntityUtil.mergeEntities(base, added); assertThat(merged).hasSize(1); @@ -84,15 +84,15 @@ void testMerge_entities_same_types_different_id() { Arrays.asList( Entity.builder("a") .setSchemaUrl("one") - .withId(Attributes.builder().put("a.id", "a").build()) - .withDescription(Attributes.builder().put("a.desc1", "a").build()) + .setId(Attributes.builder().put("a.id", "a").build()) + .setDescription(Attributes.builder().put("a.desc1", "a").build()) .build()); Collection added = Arrays.asList( Entity.builder("a") .setSchemaUrl("one") - .withId(Attributes.builder().put("a.id", "b").build()) - .withDescription(Attributes.builder().put("a.desc2", "b").build()) + .setId(Attributes.builder().put("a.id", "b").build()) + .setDescription(Attributes.builder().put("a.desc2", "b").build()) .build()); Collection merged = EntityUtil.mergeEntities(base, added); assertThat(merged).hasSize(1); @@ -115,13 +115,13 @@ void testMerge_entities_separate_types_and_schema() { Arrays.asList( Entity.builder("a") .setSchemaUrl("one") - .withId(Attributes.builder().put("a.id", "a").build()) + .setId(Attributes.builder().put("a.id", "a").build()) .build()); Collection added = Arrays.asList( Entity.builder("b") .setSchemaUrl("two") - .withId(Attributes.builder().put("b.id", "b").build()) + .setId(Attributes.builder().put("b.id", "b").build()) .build()); Collection merged = EntityUtil.mergeEntities(base, added); // Make sure we keep both entities when no conflict. @@ -164,7 +164,7 @@ void testSchemaUrlMerge_entities_same_url() { Arrays.asList( Entity.builder("t") .setSchemaUrl("one") - .withId(Attributes.builder().put("id", 1).build()) + .setId(Attributes.builder().put("id", 1).build()) .build()), "one", null); @@ -180,11 +180,11 @@ void testSchemaUrlMerge_entities_different_url() { Arrays.asList( Entity.builder("t") .setSchemaUrl("one") - .withId(Attributes.builder().put("id", 1).build()) + .setId(Attributes.builder().put("id", 1).build()) .build(), Entity.builder("t2") .setSchemaUrl("two") - .withId(Attributes.builder().put("id2", 1).build()) + .setId(Attributes.builder().put("id2", 1).build()) .build()), "one", "one"); @@ -216,7 +216,7 @@ void testRawAttributeMerge_entity_with_conflict() { Attributes.builder().put("a", 1).put("b", 1).build(), Attributes.builder().put("b", 2).put("c", 2).build(), Arrays.asList( - Entity.builder("c").withId(Attributes.builder().put("c", 1).build()).build())); + Entity.builder("c").setId(Attributes.builder().put("c", 1).build()).build())); assertThat(result.getConflicts()).satisfiesExactly(e -> assertThat(e.getType()).isEqualTo("c")); assertThat(result.getAttributes()) .hasSize(3) @@ -230,24 +230,9 @@ void testAddEntity_reflection() { Resource result = EntityUtil.addEntity( Resource.builder(), - Entity.builder("a").withId(Attributes.builder().put("a", 1).build()).build()) + Entity.builder("a").setId(Attributes.builder().put("a", 1).build()).build()) .build(); assertThat(EntityUtil.getEntities(result)) .satisfiesExactlyInAnyOrder(e -> assertThat(e.getType()).isEqualTo("a")); } - - @Test - void testAddAllEntity_reflection() { - Resource result = - EntityUtil.addAllEntity( - Resource.builder(), - Arrays.asList( - Entity.builder("a").withId(Attributes.builder().put("a", 1).build()).build(), - Entity.builder("b").withId(Attributes.builder().put("b", 1).build()).build())) - .build(); - assertThat(EntityUtil.getEntities(result)) - .satisfiesExactlyInAnyOrder( - e -> assertThat(e.getType()).isEqualTo("a"), - e -> assertThat(e.getType()).isEqualTo("b")); - } } diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java index 9ae3db2631c..28774268598 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java @@ -346,7 +346,7 @@ void toString_Valid() { assertThat(sdkLoggerProvider.toString()) .matches( "SdkLoggerProvider\\{clock=SystemClock\\{\\}, " - + "resource=Resource\\{schemaUrl=null, (attributes|rawAttributes)=\\{key=\"value\"\\}(, entities=\\[\\])?\\}, " + + "resource=Resource\\{schemaUrl=null, rawAttributes=\\{key=\"value\"\\}, entities=\\[\\], attributes=\\{key=\"value\"\\}\\}, " + "logLimits=LogLimits\\{maxNumberOfAttributes=128, maxAttributeValueLength=2147483647\\}, " + "logRecordProcessor=MockLogRecordProcessor, " + "loggerConfigurator=ScopeConfiguratorImpl\\{conditions=\\[\\]\\}\\}"); diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java index 7d8ac827974..26b9bf8967f 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java @@ -1109,9 +1109,13 @@ void spanDataToString() { + "traceFlags=00, " + "traceState=ArrayBasedTraceState\\{entries=\\[]}, remote=false, valid=false}, " + "resource=Resource\\{schemaUrl=null, " - + "(attributes|rawAttributes)=\\{service.name=\"unknown_service:java\", " + + "rawAttributes=\\{service.name=\"unknown_service:java\", " + "telemetry.sdk.language=\"java\", telemetry.sdk.name=\"opentelemetry\", " - + "telemetry.sdk.version=\"\\d+.\\d+.\\d+(-rc.\\d+)?(-SNAPSHOT)?\"\\}(, entities=\\[\\])?\\}, " + + "telemetry.sdk.version=\"\\d+.\\d+.\\d+(-rc.\\d+)?(-SNAPSHOT)?\"\\}, " + + "entities=\\[\\], " + + "attributes=\\{service.name=\"unknown_service:java\", " + + "telemetry.sdk.language=\"java\", telemetry.sdk.name=\"opentelemetry\", " + + "telemetry.sdk.version=\"\\d+.\\d+.\\d+(-rc.\\d+)?(-SNAPSHOT)?\"\\}\\}, " + "instrumentationScopeInfo=InstrumentationScopeInfo\\{" + "name=SpanBuilderSdkTest, version=null, schemaUrl=null, attributes=\\{}}, " + "name=span_name, " From da0b3fb1f627ec6a53a6590467998dd2359e4660 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 25 Jun 2026 02:19:56 +0000 Subject: [PATCH 07/22] Fixes from review - Move to using existing resource detectors and a flag to enable entities. --- .../autoconfigure/EnvironmentResource.java | 325 +++++++++++++++ .../autoconfigure/IncubatingEntityUtil.java | 60 --- .../autoconfigure/ResourceConfiguration.java | 56 +-- .../ResourceConfigurationTest.java | 73 ++++ .../DeclarativeConfigurationTest.java | 55 --- .../DeclarativeConfigContext.java | 12 - .../declarativeconfig/ResourceFactory.java | 80 +--- .../ServiceResourceDetector.java | 33 +- .../ServiceResourceDetectorTest.java | 54 +++ .../incubator/resources/EntityDetector.java | 33 -- .../resources/EnvEntityDetector.java | 394 ------------------ .../resources/ServiceEntityDetector.java | 54 --- .../resources/TelemetrySdkEntityDetector.java | 51 --- ...tension.incubator.resources.EntityDetector | 3 - .../resources/EnvEntityDetectorTest.java | 154 ------- .../resources/ServiceEntityDetectorTest.java | 56 --- .../TelemetrySdkEntityDetectorTest.java | 40 -- 17 files changed, 514 insertions(+), 1019 deletions(-) delete mode 100644 sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java delete mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java delete mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java delete mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java delete mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java delete mode 100644 sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.resources.EntityDetector delete mode 100644 sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java delete mode 100644 sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java delete mode 100644 sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java 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..d45ca4b44ae 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 @@ -10,8 +10,17 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; 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. @@ -38,6 +47,32 @@ final class EnvironmentResource { */ @SuppressWarnings("JdkObsolete") // Recommended alternative was introduced in java 10 static Resource createEnvironmentResource(ConfigProperties config) { + boolean entitiesEnabled = config.getBoolean("otel.experimental.entities.enabled", false); + if (entitiesEnabled) { + ResourceBuilder builder = Resource.builder(); + + String entitiesStr = config.getString("otel.entities"); + if (entitiesStr != null && !entitiesStr.isEmpty()) { + List parsedEntities = new EntityParser(entitiesStr).parse(); + for (Entity entity : parsedEntities) { + EntityUtil.addEntity(builder, entity); + } + } + + String serviceName = config.getString(SERVICE_NAME_PROPERTY); + if (serviceName != null) { + Entity serviceEntity = + Entity.builder("service").setId(Attributes.of(SERVICE_NAME, serviceName)).build(); + EntityUtil.addEntity(builder, serviceEntity); + } + + for (Map.Entry entry : config.getMap(ATTRIBUTE_PROPERTY).entrySet()) { + builder.put(entry.getKey(), decodeResourceAttributes(entry.getValue())); + } + + return builder.build(); + } + AttributesBuilder resourceAttributes = Attributes.builder(); for (Map.Entry entry : config.getMap(ATTRIBUTE_PROPERTY).entrySet()) { resourceAttributes.put( @@ -103,4 +138,294 @@ private static String decodeResourceAttributes(String value) { } private EnvironmentResource() {} + + 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()); + + /** + * The current state of parsing. + * + *

The format is TYPE{KEY1=VAL1,KEY2=VAL2}[ATTR1=VAL1,ATTR2=VAL2]@SCHEMA_URL; + * + *

The parser state machine transitions between the following states: - TYPE: Parsing an + * entity type - ID_KEY: Parsing a "key" of an identity attribute - ID_VAL: Parsing a "value" of + * an identity attribute - DESC_KEY: Parsing a "key" of a description attribute - DESC_VAL: + * Parsing a "value" of a description attribute - SCHEMA_URL: Parsing the schema URL of a + * specific entity - SKIP_TO_NEXT: Skip to the next entity + */ + private enum State { + TYPE, + ID_KEY, + ID_VAL, + DESC_KEY, + DESC_VAL, + SCHEMA_URL, + SKIP_TO_NEXT + } + + /** The input entity string. */ + private final String input; + + /** The current state of parsing. (i.e. where we are in the grammar) */ + private State state = State.TYPE; + + /** The segment of the input string that we are currently parsing. */ + private final Segment currentSegment; + + /** The list of entities we've parsed. */ + private final List entities = new ArrayList<>(); + + // Temporary state for building an entity. + + /** The parsed entity type. */ + @Nullable private String currentType; + + /** Parsed attributes denoting the entity identity. */ + private Attributes currentIdAttrs = Attributes.empty(); + + /** Parsed attributes denoting the entity description. */ + private Attributes currentDescAttrs = Attributes.empty(); + + /** Parsed schema URL for the entity. */ + @Nullable private String currentSchemaUrl; + + /** A temporary builder we use when parsing key-value pairs for identity or description. */ + @Nullable private AttributesBuilder currentBuilder; + + /** The current key of a key-value pair that we are parsing. */ + @Nullable private String currentKey; + + EntityParser(String input) { + this.input = input; + this.currentSegment = new Segment(input); + } + + /** + * Parses the input string and returns a list of entities. + * + * @return the list of entities parsed from the input string. + */ + List parse() { + int n = input.length(); + for (int i = 0; i < n; i++) { + char c = input.charAt(i); + + // We finished the previous entity, or hit a syntax error. + // Skip to the next entity and try to parse it. + if (state == State.SKIP_TO_NEXT) { + if (c == ';') { + resetEntityState(i + 1); + state = State.TYPE; + } + continue; + } + + switch (c) { + case '{': + // Finish writing entity type, start identity parsing. + if (state == State.TYPE) { + currentSegment.markEnd(i); + currentType = currentSegment.getValue(); + if (currentType == null || currentType.isEmpty()) { + logger.log(Level.WARNING, "Malformed entity definition (empty type): " + input); + state = State.SKIP_TO_NEXT; + } else { + state = State.ID_KEY; + currentSegment.reset(i + 1); + currentBuilder = Attributes.builder(); + } + } + break; + case '}': + // End identity parsing. + if (state == State.ID_VAL || state == State.ID_KEY) { + currentSegment.markEnd(i); + if (state == State.ID_VAL) { + putAttr(); + } + if (currentBuilder != null) { + currentIdAttrs = currentBuilder.build(); + } + if (currentIdAttrs.isEmpty()) { + logger.log( + Level.WARNING, + "Malformed entity definition (missing identifying attributes): " + input); + state = State.SKIP_TO_NEXT; + } else { + state = State.TYPE; // Default next state, might change if [ or @ follows + currentSegment.reset(i + 1); + } + } + break; + case '[': + // We finished identity, we're moving to parse description. + if (state == State.TYPE) { + // After } we are in TYPE state again but expecting [ or @ or ; + state = State.DESC_KEY; + currentSegment.reset(i + 1); + currentBuilder = Attributes.builder(); + } + break; + case ']': + // We finished description, update attributes for description and move + // back to TYPE state. + if (state == State.DESC_VAL || state == State.DESC_KEY) { + currentSegment.markEnd(i); + if (state == State.DESC_VAL) { + putAttr(); + } + if (currentBuilder != null) { + currentDescAttrs = currentBuilder.build(); + } + state = State.TYPE; + currentSegment.reset(i + 1); + } + break; + case '=': + // Finish our "key" parsing and start looking for a value. + if (state == State.ID_KEY || state == State.DESC_KEY) { + currentSegment.markEnd(i); + currentKey = currentSegment.getValue(); + if (currentKey == null || currentKey.isEmpty()) { + logger.log(Level.WARNING, "Malformed key-value pair (empty key): " + input); + state = State.SKIP_TO_NEXT; + } else { + state = (state == State.ID_KEY) ? State.ID_VAL : State.DESC_VAL; + currentSegment.reset(i + 1); + } + } + break; + case ',': + // Finish our "value" parsing and start looking for the next key-value. + if (state == State.ID_VAL || state == State.DESC_VAL) { + currentSegment.markEnd(i); + putAttr(); + state = (state == State.ID_VAL) ? State.ID_KEY : State.DESC_KEY; + currentSegment.reset(i + 1); + } + break; + case '@': + // Start looking for schema url + if (state == State.TYPE) { + state = State.SCHEMA_URL; + currentSegment.reset(i + 1); + } + break; + case ';': + // Finish up the current entity, and get ready to parse the next. + if (state == State.TYPE || state == State.SCHEMA_URL) { + if (state == State.SCHEMA_URL) { + currentSegment.markEnd(i); + currentSchemaUrl = currentSegment.getValue(); + } + buildAndAddEntity(); + resetEntityState(i + 1); + state = State.TYPE; + } else if (state == State.ID_KEY + || state == State.ID_VAL + || state == State.DESC_KEY + || state == State.DESC_VAL) { + logger.log(Level.WARNING, "Malformed entity definition (unexpected ';'): " + input); + resetEntityState(i + 1); + state = State.TYPE; + } + break; + case '%': + // Found an escape character, mark the segment as needing decoding, which + // requires special handling. + currentSegment.markNeedsDecoding(); + break; + default: + break; + } + } + + // Handle end of string + if (state == State.TYPE || state == State.SCHEMA_URL) { + if (state == State.SCHEMA_URL) { + currentSegment.markEnd(input.length()); + currentSchemaUrl = currentSegment.getValue(); + } + buildAndAddEntity(); + } + + return entities; + } + + /** Adds the current attribute key-value pair into the current attribute builder. */ + private void putAttr() { + String val = currentSegment.getValue(); + if (currentKey != null && !currentKey.isEmpty() && currentBuilder != null) { + currentBuilder.put(currentKey, val); + } + } + + /** Finishes building the current entity and adds it to the parsed list. */ + private void buildAndAddEntity() { + if (currentType != null && !currentType.isEmpty() && !currentIdAttrs.isEmpty()) { + EntityBuilder builder = Entity.builder(currentType).setId(currentIdAttrs); + if (!currentDescAttrs.isEmpty()) { + builder.setDescription(currentDescAttrs); + } + if (currentSchemaUrl != null && !currentSchemaUrl.isEmpty()) { + builder.setSchemaUrl(currentSchemaUrl); + } + entities.add(builder.build()); + } + } + + /** + * Resets the state of the entity parser. + * + * @param nextStart the start index of the next entity (e.g. after the `;`). + */ + private void resetEntityState(int nextStart) { + currentType = null; + currentIdAttrs = Attributes.empty(); + currentDescAttrs = Attributes.empty(); + currentSchemaUrl = null; + currentBuilder = null; + currentKey = null; + currentSegment.reset(nextStart); + } + } } diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java deleted file mode 100644 index 80eebcd24ae..00000000000 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingEntityUtil.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.autoconfigure; - -import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.extension.incubator.resources.EntityDetector; -import io.opentelemetry.sdk.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.ArrayList; -import java.util.List; -import java.util.Set; -import javax.annotation.Nullable; - -final class IncubatingEntityUtil { - - private IncubatingEntityUtil() {} - - @Nullable - static Resource configureEntityResource( - ConfigProperties config, - SpiHelper spiHelper, - Set enabledProviders, - Set disabledProviders) { - - List detectors = new ArrayList<>(); - for (EntityDetector detector : spiHelper.loadOrdered(EntityDetector.class)) { - String fqcn = detector.getClass().getName(); - String shortName = detector.getName(); - if (!enabledProviders.isEmpty() - && !enabledProviders.contains(fqcn) - && !enabledProviders.contains(shortName)) { - continue; - } - if (disabledProviders.contains(fqcn) || disabledProviders.contains(shortName)) { - continue; - } - detectors.add(detector); - } - - if (detectors.isEmpty()) { - return null; - } - - ResourceBuilder builder = Resource.builder(); - for (EntityDetector detector : detectors) { - for (Entity entity : detector.detect(config)) { - if (entity != null) { - EntityUtil.addEntity(builder, entity); - } - } - } - return builder.build(); - } -} 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 840af3b9807..9f1a1c4d03c 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java @@ -26,22 +26,6 @@ */ public final class ResourceConfiguration { - private static final boolean ENTITY_INCUBATOR_AVAILABLE; - - static { - boolean incubatorAvailable = false; - try { - Class.forName( - "io.opentelemetry.sdk.extension.incubator.resources.EntityDetector", - false, - ResourceConfiguration.class.getClassLoader()); - incubatorAvailable = true; - } catch (ClassNotFoundException e) { - // Not available - } - ENTITY_INCUBATOR_AVAILABLE = incubatorAvailable; - } - // Visible for testing static final String DISABLED_ATTRIBUTE_KEYS = "otel.resource.disabled.keys"; static final String ENABLED_RESOURCE_PROVIDERS = "otel.java.enabled.resource.providers"; @@ -82,35 +66,23 @@ static Resource configureResource( Set enabledProviders = new HashSet<>(config.getList(ENABLED_RESOURCE_PROVIDERS)); Set disabledProviders = new HashSet<>(config.getList(DISABLED_RESOURCE_PROVIDERS)); - // If Entity experiment is enabled, we use a new flow to instantiate resources. - boolean entitiesEnabled = config.getBoolean("otel.experimental.entities.enabled", false); - if (entitiesEnabled && ENTITY_INCUBATOR_AVAILABLE) { - Resource entityResource = - IncubatingEntityUtil.configureEntityResource( - config, spiHelper, enabledProviders, disabledProviders); - if (entityResource != null) { - result = entityResource; + for (ResourceProvider resourceProvider : spiHelper.loadOrdered(ResourceProvider.class)) { + if (!enabledProviders.isEmpty() + && !enabledProviders.contains(resourceProvider.getClass().getName())) { + continue; } - } else { - - for (ResourceProvider resourceProvider : spiHelper.loadOrdered(ResourceProvider.class)) { - if (!enabledProviders.isEmpty() - && !enabledProviders.contains(resourceProvider.getClass().getName())) { - continue; - } - if (disabledProviders.contains(resourceProvider.getClass().getName())) { - continue; - } - if (resourceProvider instanceof ConditionalResourceProvider - && !((ConditionalResourceProvider) resourceProvider).shouldApply(config, result)) { - continue; - } - result = result.merge(resourceProvider.createResource(config)); + if (disabledProviders.contains(resourceProvider.getClass().getName())) { + continue; } - - // TODO(jsuereth): Should filter attributes be used with entities? - result = filterAttributes(result, config); + if (resourceProvider instanceof ConditionalResourceProvider + && !((ConditionalResourceProvider) resourceProvider).shouldApply(config, result)) { + continue; + } + result = result.merge(resourceProvider.createResource(config)); } + + result = filterAttributes(result, config); + return resourceCustomizer.apply(result, config); } 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..6cd4f4d86cc 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,9 @@ 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.Collection; import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; @@ -166,4 +169,74 @@ void filterAttributes() { assertThat(resource.getAttributes().get(stringKey("bar"))).isNull(); }); } + + @Test + void createEnvironmentResource_EntitiesEnabled() { + Map props = new HashMap<>(); + props.put("otel.experimental.entities.enabled", "true"); + props.put( + "otel.entities", + "process{process.pid=1234}[process.executable.name=java]@http://schema;host{host.id=myhost}"); + props.put("otel.service.name", "my-service"); + props.put("otel.resource.attributes", "flat.attr=flat-val"); + + Resource resource = + ResourceConfiguration.createEnvironmentResource( + DefaultConfigProperties.createFromMap(props)); + + Collection entities = EntityUtil.getEntities(resource); + assertThat(entities).hasSize(3); + + assertThat(entities) + .anyMatch( + e -> + e.getType().equals("process") + && e.getSchemaUrl().equals("http://schema") + && e.getId().equals(Attributes.of(stringKey("process.pid"), "1234")) + && e.getDescription() + .equals(Attributes.of(stringKey("process.executable.name"), "java"))); + + assertThat(entities) + .anyMatch( + e -> + e.getType().equals("host") + && e.getSchemaUrl() == null + && e.getId().equals(Attributes.of(stringKey("host.id"), "myhost"))); + + assertThat(entities) + .anyMatch( + e -> + e.getType().equals("service") + && e.getId().equals(Attributes.of(stringKey("service.name"), "my-service"))); + + // The attributes of the entities should also be flat attributes on the Resource + assertThat(resource.getAttributes()) + .containsEntry(stringKey("service.name"), "my-service") + .containsEntry("process.pid", "1234") + .containsEntry("process.executable.name", "java") + .containsEntry("host.id", "myhost") + .containsEntry("flat.attr", "flat-val"); + } + + @Test + void createEnvironmentResource_EntitiesDisabled() { + Map props = new HashMap<>(); + props.put("otel.experimental.entities.enabled", "false"); + props.put("otel.entities", "process{process.pid=1234}"); + props.put("otel.service.name", "my-service"); + props.put("otel.resource.attributes", "flat.attr=flat-val"); + + Resource resource = + ResourceConfiguration.createEnvironmentResource( + DefaultConfigProperties.createFromMap(props)); + + Collection entities = EntityUtil.getEntities(resource); + assertThat(entities).isEmpty(); + + assertThat(resource.getAttributes()) + .containsEntry(stringKey("service.name"), "my-service") + .containsEntry("flat.attr", "flat-val") + // otel.entities is ignored when entities are disabled + .doesNotContainKey(stringKey("process.pid")); + } } diff --git a/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java b/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java index 2caffc05dea..64d3b68ef15 100644 --- a/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java +++ b/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java @@ -18,7 +18,6 @@ import io.github.netmikey.logunit.api.LogCapturer; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.incubator.ExtendedOpenTelemetry; import io.opentelemetry.api.incubator.config.ConfigProvider; import io.opentelemetry.api.incubator.config.InstrumentationConfigUtil; @@ -35,8 +34,6 @@ import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.resources.internal.Entity; -import io.opentelemetry.sdk.resources.internal.EntityUtil; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import java.io.IOException; @@ -44,10 +41,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -249,53 +243,4 @@ void configFile_ConfigProvider() { .getScalarList("request_captured_headers", String.class)) .isEqualTo(Arrays.asList("Content-Type", "Accept")); } - - @Test - void entitiesEnabled() { - ConfigProperties config = - DefaultConfigProperties.createFromMap( - Collections.singletonMap("otel.experimental.entities.enabled", "true")); - - AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk = - AutoConfiguredOpenTelemetrySdk.builder().setConfig(config).build(); - OpenTelemetrySdk openTelemetrySdk = autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk(); - cleanup.addCloseable(openTelemetrySdk); - - Resource resource = autoConfiguredOpenTelemetrySdk.getResource(); - - Collection entities = EntityUtil.getEntities(resource); - assertThat(entities) - .anyMatch( - e -> - e.getType().equals("telemetry.sdk") - && "opentelemetry" - .equals(e.getId().get(AttributeKey.stringKey("telemetry.sdk.name"))) - && "java" - .equals(e.getId().get(AttributeKey.stringKey("telemetry.sdk.language")))); - } - - @Test - void entitiesEnabled_WithEnabledProviders() { - Map props = new HashMap<>(); - props.put("otel.experimental.entities.enabled", "true"); - props.put("otel.service.name", "my-filtered-service"); - props.put("otel.java.enabled.resource.providers", "service"); - ConfigProperties config = DefaultConfigProperties.createFromMap(props); - - AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk = - AutoConfiguredOpenTelemetrySdk.builder().setConfig(config).build(); - OpenTelemetrySdk openTelemetrySdk = autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk(); - cleanup.addCloseable(openTelemetrySdk); - - Resource resource = autoConfiguredOpenTelemetrySdk.getResource(); - - Collection entities = EntityUtil.getEntities(resource); - assertThat(entities) - .anyMatch( - e -> - e.getType().equals("service") - && "my-filtered-service" - .equals(e.getId().get(AttributeKey.stringKey("service.name")))); - assertThat(entities).noneMatch(e -> e.getType().equals("telemetry.sdk")); - } } diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigContext.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigContext.java index 80a32528947..a93c0999bea 100644 --- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigContext.java +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigContext.java @@ -131,18 +131,6 @@ private InternalTelemetryVersion getInternalTelemetryVersion() { } } - boolean isEntitiesEnabled() { - if (configProvider == null) { - return false; - } - // TODO - Check with Jack if this is the right thing to do for config flags, - // of if we want to put this in the experimental resource config space. - return Boolean.TRUE.equals( - configProvider - .getInstrumentationConfig("otel_sdk") - .getBoolean("experimental_entities_enabled")); - } - /** * Find a registered {@link ComponentProvider} with {@link ComponentProvider#getType()} matching * {@code type}, {@link ComponentProvider#getName()} matching {@code name}, and call {@link diff --git a/sdk-extensions/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 0b157d1ca41..f1d1783aa03 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 @@ -15,12 +15,8 @@ import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.IncludeExcludeModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.ResourceModel; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; -import io.opentelemetry.sdk.extension.incubator.resources.EntityDetector; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.ResourceBuilder; -import io.opentelemetry.sdk.resources.internal.Entity; -import io.opentelemetry.sdk.resources.internal.EntityUtil; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Predicate; @@ -41,52 +37,27 @@ public Resource create(ResourceModel model, DeclarativeConfigContext context) { ExperimentalResourceDetectionModel detectionModel = model.getDetectionDevelopment(); if (detectionModel != null) { - if (context.isEntitiesEnabled()) { - List detectorModels = detectionModel.getDetectors(); - if (detectorModels != null) { - List detectors = new ArrayList<>(); - for (ExperimentalResourceDetectorModel detectorModel : detectorModels) { - ConfigKeyValue detectorKeyValue = - FileConfigUtil.validateSingleKeyValue(context, detectorModel, "resource detector"); - String detectorName = detectorKeyValue.getKey(); - - for (EntityDetector detector : context.load(EntityDetector.class)) { - if (detector.getName().equals(detectorName) - || detector.getClass().getName().equals(detectorName)) { - detectors.add(detector); - } - } - } - if (!detectors.isEmpty()) { - Resource detectedEntityResource = detectEntityResource(detectors); - builder = detectedEntityResource.toBuilder(); - } - } - } else { - ResourceBuilder detectedResourceBuilder = Resource.builder(); - - List detectorModels = detectionModel.getDetectors(); - if (detectorModels != null) { - for (ExperimentalResourceDetectorModel detectorModel : detectorModels) { - detectedResourceBuilder.putAll( - ResourceDetectorFactory.getInstance().create(detectorModel, context)); - } - } - - IncludeExcludeModel attributesIncludeExcludeModel = detectionModel.getAttributes(); - Predicate detectorAttributeFilter = - attributesIncludeExcludeModel == null - ? ResourceFactory::matchAll - : IncludeExcludeFactory.getInstance() - .create(attributesIncludeExcludeModel, context); + ResourceBuilder detectedResourceBuilder = Resource.builder(); - Attributes filteredDetectedAttributes = - detectedResourceBuilder.build().getAttributes().toBuilder() - .removeIf(attributeKey -> !detectorAttributeFilter.test(attributeKey.getKey())) - .build(); - - builder.putAll(filteredDetectedAttributes); + List detectorModels = detectionModel.getDetectors(); + if (detectorModels != null) { + for (ExperimentalResourceDetectorModel detectorModel : detectorModels) { + detectedResourceBuilder.putAll( + ResourceDetectorFactory.getInstance().create(detectorModel, context)); + } } + + IncludeExcludeModel attributesIncludeExcludeModel = detectionModel.getAttributes(); + Predicate detectorAttributeFilter = + attributesIncludeExcludeModel == null + ? ResourceFactory::matchAll + : IncludeExcludeFactory.getInstance().create(attributesIncludeExcludeModel, context); + Attributes filteredDetectedAttributes = + detectedResourceBuilder.build().getAttributes().toBuilder() + .removeIf(attributeKey -> !detectorAttributeFilter.test(attributeKey.getKey())) + .build(); + + builder.putAll(filteredDetectedAttributes); } String attributeList = model.getAttributesList(); @@ -111,19 +82,6 @@ public Resource create(ResourceModel model, DeclarativeConfigContext context) { return builder.build(); } - private static Resource detectEntityResource(List detectors) { - ResourceBuilder builder = Resource.builder(); - for (EntityDetector detector : detectors) { - for (Entity entity : - detector.detect(DefaultConfigProperties.createFromMap(Collections.emptyMap()))) { - if (entity != null) { - EntityUtil.addEntity(builder, entity); - } - } - } - return builder.build(); - } - private static boolean matchAll(String attributeKey) { return true; } 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..cd1335e8a49 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 @@ -6,12 +6,15 @@ 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.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; @@ -21,6 +24,10 @@ public class ServiceResourceDetector implements ComponentProvider { private static final AttributeKey SERVICE_INSTANCE_ID = AttributeKey.stringKey("service.instance.id"); + private static final String SCHEMA_URL = "https://opentelemetry.io/schemas/1.40.0"; + private static final String SERVICE_TYPE = "service"; + private static final String SERVICE_INSTANCE_TYPE = "service.instance"; + // multiple calls to this resource provider should return the same value private static final String RANDOM_SERVICE_INSTANCE_ID = UUID.randomUUID().toString(); @@ -40,13 +47,31 @@ public Resource create(DeclarativeConfigProperties config) { ConfigProperties properties = DefaultConfigProperties.create(Collections.emptyMap(), config.getComponentLoader()); + boolean entitiesEnabled = properties.getBoolean("otel.experimental.entities.enabled", false); + String serviceName = properties.getString("otel.service.name"); - if (serviceName != null) { - builder.put(SERVICE_NAME, serviceName).build(); + if (entitiesEnabled) { + if (serviceName != null) { + Entity serviceEntity = + Entity.builder(SERVICE_TYPE) + .setId(Attributes.of(SERVICE_NAME, serviceName)) + .setSchemaUrl(SCHEMA_URL) + .build(); + EntityUtil.addEntity(builder, serviceEntity); + } + Entity serviceInstanceEntity = + Entity.builder(SERVICE_INSTANCE_TYPE) + .setId(Attributes.of(SERVICE_INSTANCE_ID, RANDOM_SERVICE_INSTANCE_ID)) + .setSchemaUrl(SCHEMA_URL) + .build(); + EntityUtil.addEntity(builder, serviceInstanceEntity); + } else { + if (serviceName != null) { + builder.put(SERVICE_NAME, serviceName); + } + builder.put(SERVICE_INSTANCE_ID, RANDOM_SERVICE_INSTANCE_ID); } - builder.put(SERVICE_INSTANCE_ID, RANDOM_SERVICE_INSTANCE_ID); - return builder.build(); } } 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 index 8513423a0a9..1ecc7da40e6 100644 --- 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 @@ -12,6 +12,9 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.Entity; +import io.opentelemetry.sdk.resources.internal.EntityUtil; +import java.util.Collection; import java.util.Objects; import java.util.UUID; import org.junit.jupiter.api.Test; @@ -61,4 +64,55 @@ void create_NoSystemProperty() { .doesNotThrowAnyException(); }); } + + @Test + @ClearSystemProperty(key = "otel.service.name") + @ClearSystemProperty(key = "otel.experimental.entities.enabled") + void create_EntitiesEnabled() { + System.setProperty("otel.service.name", "my-service"); + System.setProperty("otel.experimental.entities.enabled", "true"); + + Resource resource = new ServiceResourceDetector().create(DeclarativeConfigProperties.empty()); + + Collection entities = EntityUtil.getEntities(resource); + assertThat(entities).hasSize(2); + + assertThat(entities) + .anyMatch( + e -> + e.getType().equals("service") + && e.getSchemaUrl().equals("https://opentelemetry.io/schemas/1.40.0") + && e.getId() + .equals( + Attributes.of(AttributeKey.stringKey("service.name"), "my-service"))); + + assertThat(entities) + .anyMatch( + e -> + e.getType().equals("service.instance") + && e.getSchemaUrl().equals("https://opentelemetry.io/schemas/1.40.0") + && e.getId().get(AttributeKey.stringKey("service.instance.id")) != null); + + // Flat attributes should also be present + Attributes attributes = resource.getAttributes(); + assertThat(attributes.get(AttributeKey.stringKey("service.name"))).isEqualTo("my-service"); + assertThat(attributes.get(AttributeKey.stringKey("service.instance.id"))).isNotNull(); + } + + @Test + @ClearSystemProperty(key = "otel.service.name") + @ClearSystemProperty(key = "otel.experimental.entities.enabled") + void create_EntitiesDisabled() { + System.setProperty("otel.service.name", "my-service"); + System.setProperty("otel.experimental.entities.enabled", "false"); + + Resource resource = new ServiceResourceDetector().create(DeclarativeConfigProperties.empty()); + + Collection entities = EntityUtil.getEntities(resource); + assertThat(entities).isEmpty(); + + Attributes attributes = resource.getAttributes(); + assertThat(attributes.get(AttributeKey.stringKey("service.name"))).isEqualTo("my-service"); + assertThat(attributes.get(AttributeKey.stringKey("service.instance.id"))).isNotNull(); + } } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java deleted file mode 100644 index a2b37854ef8..00000000000 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EntityDetector.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources; - -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.autoconfigure.spi.Ordered; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.resources.internal.Entity; -import java.util.Collection; - -/** - * A service provider interface (SPI) for providing a collection of {@link Entity} that are merged - * into the {@link Resource#getDefault() default resource}. - */ -public interface EntityDetector extends Ordered { - /** - * Detects entities based on the configuration. - * - * @param config the configuration to use for detection - */ - Collection detect(ConfigProperties config); - - /** - * Returns the name of the detector (e.g., "service", "env") or the fully qualified class name. - * Used for configuration filtering by-name. - */ - default String getName() { - return getClass().getName(); - } -} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java deleted file mode 100644 index ace30b3b0d4..00000000000 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetector.java +++ /dev/null @@ -1,394 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.resources.internal.Entity; -import io.opentelemetry.sdk.resources.internal.EntityBuilder; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.annotation.Nullable; - -/* - * An EntityDetector that parses the OTEL_ENTITIES environment variable. - * - * See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/entities/entity-propagation.md - * for more information about the OTEL_ENTITIES environment variable. - */ -public class EnvEntityDetector implements EntityDetector { - - private static final Logger logger = Logger.getLogger(EnvEntityDetector.class.getName()); - private static final String PROPERTY_KEY = "otel.entities"; - - @Override - public String getName() { - return "env"; - } - - @Override - public Collection detect(ConfigProperties config) { - String entitiesStr = config.getString(PROPERTY_KEY); - if (entitiesStr == null || entitiesStr.isEmpty()) { - return new ArrayList<>(); - } - - return new EntityParser(entitiesStr).parse(); - } - - /** - * Segment class represents a start/stop endpoint within a source String. - * - *

A segment can be used to extract a substring from the source string *without interning* the - * string into the JDK's string tables. This can dramatically reduce allocations when parsing. - * Segment is intended to provide a similar interface to using {@code substring} on {@code - * String}. - * - *

Additionally, a Segment can be denoted as URL-encoded (e.g. using '%20' to denote a - * character.) In this case, the segment will be decoded when extracting its String value. - */ - private static final class Segment { - private final String source; - private int start; - private int end; - private boolean needsDecoding; - - Segment(String source) { - this.source = source; - reset(0); - } - - /** Reset the segment for the next use, starting from the given start index. */ - void reset(int start) { - this.start = start; - this.end = start; - this.needsDecoding = false; - } - - /** Update the end of the segment (non-inclusive). */ - void markEnd(int end) { - this.end = end; - } - - /** - * Denotes that the segment is URL encoded, and should be decoded when calling {@code - * getValue()}. - */ - void markNeedsDecoding() { - this.needsDecoding = true; - } - - /** Return true if the segment is empty. */ - boolean isEmpty() { - return start >= end; - } - - /** - * Returns the string represented by the bounds of the segment *and* decodes it if {@code - * markNeedsDecoding} has been called. - * - *

Note: This will trim whitespace from the segment before returning it. - */ - String getValue() { - if (isEmpty()) { - return ""; - } - // TODO - avoid using substring and then triming to avoid interning more than - // one string. - String substring = source.substring(start, end).trim(); - return needsDecoding ? decode(substring) : substring; - } - - // Percent decoding logic moved here - private static String decode(String value) { - if (value.indexOf('%') < 0) { - return value; - } - - int n = value.length(); - byte[] bytes = new byte[n]; - int pos = 0; - - for (int i = 0; i < n; i++) { - char c = value.charAt(i); - if (c == '%' && i + 2 < n) { - int d1 = Character.digit(value.charAt(i + 1), 16); - int d2 = Character.digit(value.charAt(i + 2), 16); - if (d1 != -1 && d2 != -1) { - bytes[pos++] = (byte) ((d1 << 4) + d2); - i += 2; - continue; - } - } - bytes[pos++] = (byte) c; - } - return new String(bytes, 0, pos, StandardCharsets.UTF_8); - } - } - - // State machine parser - private static final class EntityParser { - /** - * The current state of parsing. - * - *

The format is TYPE{KEY1=VAL1,KEY2=VAL2}[ATTR1=VAL1,ATTR2=VAL2]@SCHEMA_URL; - * - *

The parser state machine transitions between the following states: - TYPE: Parsing an - * entity type - ID_KEY: Parsing a "key" of an identity attribute - ID_VAL: Parsing a "value" of - * an identity attribute - DESC_KEY: Parsing a "key" of a description attribute - DESC_VAL: - * Parsing a "value" of a description attribute - SCHEMA_URL: Parsing the schema URL of a - * specific entity - SKIP_TO_NEXT: Skip to the next entity - */ - private enum State { - TYPE, - ID_KEY, - ID_VAL, - DESC_KEY, - DESC_VAL, - SCHEMA_URL, - SKIP_TO_NEXT - // TODO - do we need specific states to represent "TYPE_COMPLETE", - // "ID_COMPLETE", "DESC_COMPLETE"? - } - - /** The input entity string. */ - private final String input; - - /** The current state of parsing. (i.e. where we are in the grammar) */ - private State state = State.TYPE; - - /** The segment of the input string that we are currently parsing. */ - private final Segment currentSegment; - - /** The list of entities we've parsed. */ - private final List entities = new ArrayList<>(); - - // Temporary state for building an entity. - - /** The parsed entity type. */ - @Nullable private String currentType; - - /** Parsed attributes denoting the entity identity. */ - private Attributes currentIdAttrs = Attributes.empty(); - - /** Parsed attributes denoting the entity description. */ - private Attributes currentDescAttrs = Attributes.empty(); - - /** Parsed schema URL for the entity. */ - @Nullable private String currentSchemaUrl; - - /** A temporary builder we use when parsing key-value pairs for identity or description. */ - @Nullable private AttributesBuilder currentBuilder; - - /** The current key of a key-value pair that we are parsing. */ - @Nullable private String currentKey; - - EntityParser(String input) { - this.input = input; - this.currentSegment = new Segment(input); - } - - /** - * Parses the input string and returns a list of entities. - * - * @return the list of entities parsed from the input string. - */ - List parse() { - int n = input.length(); - for (int i = 0; i < n; i++) { - char c = input.charAt(i); - - // We finished the previous entity, or hit a syntax error. - // Skip to the next entity and try to parse it. - if (state == State.SKIP_TO_NEXT) { - if (c == ';') { - resetEntityState(i + 1); - state = State.TYPE; - } - continue; - } - - switch (c) { - case '{': - // Finish writing entity type, start identity parsing. - if (state == State.TYPE) { - currentSegment.markEnd(i); - currentType = currentSegment.getValue(); - if (currentType == null || currentType.isEmpty()) { - logger.log(Level.WARNING, "Malformed entity definition (empty type): " + input); - state = State.SKIP_TO_NEXT; - } else { - state = State.ID_KEY; - currentSegment.reset(i + 1); - currentBuilder = Attributes.builder(); - } - } - break; - case '}': - // End identity parsing. - if (state == State.ID_VAL || state == State.ID_KEY) { - currentSegment.markEnd(i); - if (state == State.ID_VAL) { - putAttr(); - } - if (currentBuilder != null) { - currentIdAttrs = currentBuilder.build(); - } - if (currentIdAttrs.isEmpty()) { - logger.log( - Level.WARNING, - "Malformed entity definition (missing identifying attributes): " + input); - state = State.SKIP_TO_NEXT; - } else { - state = State.TYPE; // Default next state, might change if [ or @ follows - currentSegment.reset(i + 1); - } - } - break; - case '[': - // We finished identity, we're moving to parse description. - if (state == State.TYPE) { - // After } we are in TYPE state again but expecting [ or @ or ; - // TODO - Should we create new state to denote "ID_COMPLETE" for this? - state = State.DESC_KEY; - currentSegment.reset(i + 1); - currentBuilder = Attributes.builder(); - } - break; - case ']': - // We finished description, update attributes for description and move - // back to TYPE state. - // TODO - should we create a new state to denote "DESC_COMPLETE"? - // Since DESC is optional, we would would transition to the same state as - // ID_COMPLETE, but - // not allowing DESC to show up again. - if (state == State.DESC_VAL || state == State.DESC_KEY) { - currentSegment.markEnd(i); - if (state == State.DESC_VAL) { - putAttr(); - } - if (currentBuilder != null) { - currentDescAttrs = currentBuilder.build(); - } - state = State.TYPE; - currentSegment.reset(i + 1); - } - break; - case '=': - // Finish our "key" parsing and start looking for a value. - if (state == State.ID_KEY || state == State.DESC_KEY) { - currentSegment.markEnd(i); - currentKey = currentSegment.getValue(); - if (currentKey == null || currentKey.isEmpty()) { - logger.log(Level.WARNING, "Malformed key-value pair (empty key): " + input); - state = State.SKIP_TO_NEXT; - } else { - state = (state == State.ID_KEY) ? State.ID_VAL : State.DESC_VAL; - currentSegment.reset(i + 1); - } - } - break; - case ',': - // Finish our "value" parsing and start looking for the next key-value. - if (state == State.ID_VAL || state == State.DESC_VAL) { - currentSegment.markEnd(i); - putAttr(); - state = (state == State.ID_VAL) ? State.ID_KEY : State.DESC_KEY; - currentSegment.reset(i + 1); - } - break; - case '@': - // Start looking for schema url - if (state == State.TYPE) { // After } or ] we are in TYPE state - state = State.SCHEMA_URL; - currentSegment.reset(i + 1); - } - break; - case ';': - // Finish up the current entity, and get ready to parse the next. - if (state == State.TYPE || state == State.SCHEMA_URL) { - if (state == State.SCHEMA_URL) { - currentSegment.markEnd(i); - currentSchemaUrl = currentSegment.getValue(); - } - buildAndAddEntity(); - resetEntityState(i + 1); - state = State.TYPE; - } else if (state == State.ID_KEY - || state == State.ID_VAL - || state == State.DESC_KEY - || state == State.DESC_VAL) { - logger.log(Level.WARNING, "Malformed entity definition (unexpected ';'): " + input); - resetEntityState(i + 1); - state = State.TYPE; - } - break; - case '%': - // Found an escape character, mark the segment as needing decoding, which - // requires special handling. - currentSegment.markNeedsDecoding(); - break; - default: - // Keep scanning - break; - } - } - - // Handle end of string - if (state == State.TYPE || state == State.SCHEMA_URL) { - if (state == State.SCHEMA_URL) { - currentSegment.markEnd(input.length()); - currentSchemaUrl = currentSegment.getValue(); - } - buildAndAddEntity(); - } - - return entities; - } - - /** Adds the current attribute key-value pair into the current attribute builder. */ - private void putAttr() { - String val = currentSegment.getValue(); - if (currentKey != null && !currentKey.isEmpty() && currentBuilder != null) { - currentBuilder.put(currentKey, val); - } - } - - /** Finishes building the current entity and adds it to the parsed list. */ - private void buildAndAddEntity() { - if (currentType != null && !currentType.isEmpty() && !currentIdAttrs.isEmpty()) { - EntityBuilder builder = Entity.builder(currentType).setId(currentIdAttrs); - if (!currentDescAttrs.isEmpty()) { - builder.setDescription(currentDescAttrs); - } - if (currentSchemaUrl != null && !currentSchemaUrl.isEmpty()) { - builder.setSchemaUrl(currentSchemaUrl); - } - entities.add(builder.build()); - } - } - - /** - * Resets the state of the entity parser. - * - * @param nextStart the start index of the next entity (e.g. after the `;`). - */ - private void resetEntityState(int nextStart) { - currentType = null; - currentIdAttrs = Attributes.empty(); - currentDescAttrs = Attributes.empty(); - currentSchemaUrl = null; - currentBuilder = null; - currentKey = null; - currentSegment.reset(nextStart); - } - } -} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java deleted file mode 100644 index 2ef1384474b..00000000000 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetector.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.resources.internal.Entity; -import java.util.Arrays; -import java.util.Collection; -import java.util.UUID; - -/** Detects `service` and `service.instance` entities. */ -public class ServiceEntityDetector implements EntityDetector { - // TODO - Pull this in from semconv. - private static final String SCHEMA_URL = "https://opentelemetry.io/schemas/1.40.0"; - private static final String SERVICE_TYPE = "service"; - private static final String SERVICE_INSTANCE_TYPE = "service.instance"; - - private static final AttributeKey SERVICE_NAME = AttributeKey.stringKey("service.name"); - public static final AttributeKey SERVICE_INSTANCE_ID = - AttributeKey.stringKey("service.instance.id"); - - // multiple calls to this detector provider should return the same value - private static final String RANDOM = UUID.randomUUID().toString(); - - @Override - public String getName() { - return "service"; - } - - @Override - public Collection detect(ConfigProperties config) { - String serviceName = config.getString("otel.service.name"); - - return Arrays.asList( - Entity.builder(SERVICE_TYPE) - .setId(Attributes.builder().put(SERVICE_NAME, serviceName).build()) - // TODO: Add other service descriptive attributes. - .setSchemaUrl(SCHEMA_URL) - .build(), - Entity.builder(SERVICE_INSTANCE_TYPE) - .setId( - Attributes.builder() - // TODO: pull from env variable if needed. - .put(SERVICE_INSTANCE_ID, RANDOM) - .build()) - .setSchemaUrl(SCHEMA_URL) - .build()); - } -} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java deleted file mode 100644 index 91b9128e166..00000000000 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetector.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.common.internal.OtelVersion; -import io.opentelemetry.sdk.resources.internal.Entity; -import java.util.Collection; -import java.util.Collections; - -/** - * Detection for {@code telemetry.sdk} entity. - * - *

See: teleemtry.sdk entity - */ -public final class TelemetrySdkEntityDetector implements EntityDetector { - private static final String SCHEMA_URL = "https://opentelemetry.io/schemas/1.40.0"; - private static final String ENTITY_TYPE = "telemetry.sdk"; - private static final AttributeKey TELEMETRY_SDK_LANGUAGE = - AttributeKey.stringKey("telemetry.sdk.language"); - private static final AttributeKey TELEMETRY_SDK_NAME = - AttributeKey.stringKey("telemetry.sdk.name"); - private static final AttributeKey TELEMETRY_SDK_VERSION = - AttributeKey.stringKey("telemetry.sdk.version"); - - @Override - public String getName() { - return "telemetry.sdk"; - } - - @Override - public Collection detect(ConfigProperties config) { - return Collections.singletonList( - Entity.builder(ENTITY_TYPE) - .setSchemaUrl(SCHEMA_URL) - .setId( - Attributes.builder() - .put(TELEMETRY_SDK_NAME, "opentelemetry") - .put(TELEMETRY_SDK_LANGUAGE, "java") - .build()) - .setDescription( - Attributes.builder().put(TELEMETRY_SDK_VERSION, OtelVersion.VERSION).build()) - .build()); - } -} diff --git a/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.resources.EntityDetector b/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.resources.EntityDetector deleted file mode 100644 index 5941d4c812b..00000000000 --- a/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.resources.EntityDetector +++ /dev/null @@ -1,3 +0,0 @@ -io.opentelemetry.sdk.extension.incubator.resources.ServiceEntityDetector -io.opentelemetry.sdk.extension.incubator.resources.EnvEntityDetector -io.opentelemetry.sdk.extension.incubator.resources.TelemetrySdkEntityDetector diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java deleted file mode 100644 index 0974daf8d23..00000000000 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvEntityDetectorTest.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; -import io.opentelemetry.sdk.resources.internal.Entity; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import org.junit.jupiter.api.Test; - -class EnvEntityDetectorTest { - - @Test - void testEmpty() { - EnvEntityDetector detector = new EnvEntityDetector(); - Collection entities = - detector.detect(DefaultConfigProperties.createFromMap(Collections.emptyMap())); - assertThat(entities).isEmpty(); - } - - @Test - void testSingleEntity() { - EnvEntityDetector detector = new EnvEntityDetector(); - String value = - "service{service.name=my-app,service.instance.id=instance-1}[service.version=1.0.0]"; - Collection entities = - detector.detect( - DefaultConfigProperties.createFromMap( - Collections.singletonMap("otel.entities", value))); - - assertThat(entities).hasSize(1); - Entity entity = entities.iterator().next(); - assertThat(entity.getType()).isEqualTo("service"); - assertThat(entity.getId()) - .isEqualTo( - Attributes.builder() - .put("service.name", "my-app") - .put("service.instance.id", "instance-1") - .build()); - assertThat(entity.getDescription()) - .isEqualTo(Attributes.builder().put("service.version", "1.0.0").build()); - assertThat(entity.getSchemaUrl()).isNull(); - } - - @Test - void testMultipleEntitiesWithSchemaUrl() { - EnvEntityDetector detector = new EnvEntityDetector(); - String value = - "service{service.name=my-app}@https://opentelemetry.io/schemas/1.21.0;host{host.id=host-123}[host.name=web-server-01]"; - Collection entities = - detector.detect( - DefaultConfigProperties.createFromMap( - Collections.singletonMap("otel.entities", value))); - - assertThat(entities).hasSize(2); - List list = new ArrayList<>(entities); - - Entity entity1 = list.get(0); - assertThat(entity1.getType()).isEqualTo("service"); - assertThat(entity1.getId()) - .isEqualTo(Attributes.builder().put("service.name", "my-app").build()); - assertThat(entity1.getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.21.0"); - - Entity entity2 = list.get(1); - assertThat(entity2.getType()).isEqualTo("host"); - assertThat(entity2.getId()).isEqualTo(Attributes.builder().put("host.id", "host-123").build()); - assertThat(entity2.getDescription()) - .isEqualTo(Attributes.builder().put("host.name", "web-server-01").build()); - assertThat(entity2.getSchemaUrl()).isNull(); - } - - @Test - void testPercentDecoding() { - EnvEntityDetector detector = new EnvEntityDetector(); - String value = "service{service.name=my%2Capp}[config=key%3Dvalue%5Bprod%5D]"; - Collection entities = - detector.detect( - DefaultConfigProperties.createFromMap( - Collections.singletonMap("otel.entities", value))); - - assertThat(entities).hasSize(1); - Entity entity = entities.iterator().next(); - assertThat(entity.getType()).isEqualTo("service"); - assertThat(entity.getId()) - .isEqualTo(Attributes.builder().put("service.name", "my,app").build()); - assertThat(entity.getDescription()) - .isEqualTo(Attributes.builder().put("config", "key=value[prod]").build()); - } - - @Test - void testEmptyStringsIgnored() { - EnvEntityDetector detector = new EnvEntityDetector(); - String value = ";service{service.name=app1};;host{host.id=host-123};"; - Collection entities = - detector.detect( - DefaultConfigProperties.createFromMap( - Collections.singletonMap("otel.entities", value))); - - assertThat(entities).hasSize(2); - } - - @Test - void testMalformedSyntax_MissingBrace() { - EnvEntityDetector detector = new EnvEntityDetector(); - String value = "service service.name=app1};host{host.id=host-123}"; - Collection entities = - detector.detect( - DefaultConfigProperties.createFromMap( - Collections.singletonMap("otel.entities", value))); - - // Should skip the malformed one and process the valid one - assertThat(entities).hasSize(1); - Entity entity = entities.iterator().next(); - assertThat(entity.getType()).isEqualTo("host"); - } - - @Test - void testMalformedSyntax_MissingBraceEnd() { - EnvEntityDetector detector = new EnvEntityDetector(); - String value = "service{service.name=app1;host{host.id=host-123}"; - Collection entities = - detector.detect( - DefaultConfigProperties.createFromMap( - Collections.singletonMap("otel.entities", value))); - - assertThat(entities).hasSize(1); - Entity entity = entities.iterator().next(); - assertThat(entity.getType()).isEqualTo("host"); - // TODO: why no assert against id? - } - - @Test - void testMissingRequiredFields_EmptyIdentity() { - EnvEntityDetector detector = new EnvEntityDetector(); - String value = "service{};host{host.id=host-123}"; - Collection entities = - detector.detect( - DefaultConfigProperties.createFromMap( - Collections.singletonMap("otel.entities", value))); - - assertThat(entities).hasSize(1); - Entity entity = entities.iterator().next(); - assertThat(entity.getType()).isEqualTo("host"); - // TODO: why no assert against id? - } -} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java deleted file mode 100644 index 5fded820253..00000000000 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceEntityDetectorTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; -import io.opentelemetry.sdk.resources.internal.Entity; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import org.junit.jupiter.api.Test; - -class ServiceEntityDetectorTest { - - @Test - void testDetect() { - ServiceEntityDetector detector = new ServiceEntityDetector(); - Collection entities = - detector.detect( - DefaultConfigProperties.createFromMap( - Collections.singletonMap("otel.service.name", "my-service"))); - - assertThat(entities).hasSize(2); - List list = new ArrayList<>(entities); - - Entity serviceEntity = list.get(0); - assertThat(serviceEntity.getType()).isEqualTo("service"); - assertThat(serviceEntity.getId()) - .isEqualTo(Attributes.builder().put("service.name", "my-service").build()); - assertThat(serviceEntity.getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.40.0"); - - Entity serviceInstanceEntity = list.get(1); - assertThat(serviceInstanceEntity.getType()).isEqualTo("service.instance"); - assertThat(serviceInstanceEntity.getId().get(ServiceEntityDetector.SERVICE_INSTANCE_ID)) - .isNotNull() - .isNotEmpty(); - assertThat(serviceInstanceEntity.getSchemaUrl()) - .isEqualTo("https://opentelemetry.io/schemas/1.40.0"); - - // Verify that another call returns the same instance ID (static final RANDOM) - Collection entities2 = - detector.detect( - DefaultConfigProperties.createFromMap( - Collections.singletonMap("otel.service.name", "my-service"))); - List list2 = new ArrayList<>(entities2); - Entity serviceInstanceEntity2 = list2.get(1); - assertThat(serviceInstanceEntity2.getId().get(ServiceEntityDetector.SERVICE_INSTANCE_ID)) - .isEqualTo(serviceInstanceEntity.getId().get(ServiceEntityDetector.SERVICE_INSTANCE_ID)); - } -} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java deleted file mode 100644 index 20c0b24d532..00000000000 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/TelemetrySdkEntityDetectorTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; -import io.opentelemetry.sdk.common.internal.OtelVersion; -import io.opentelemetry.sdk.resources.internal.Entity; -import java.util.Collection; -import java.util.Collections; -import org.junit.jupiter.api.Test; - -class TelemetrySdkEntityDetectorTest { - - @Test - void testDetect() { - TelemetrySdkEntityDetector detector = new TelemetrySdkEntityDetector(); - Collection entities = - detector.detect(DefaultConfigProperties.createFromMap(Collections.emptyMap())); - - assertThat(entities).hasSize(1); - Entity entity = entities.iterator().next(); - - assertThat(entity.getType()).isEqualTo("telemetry.sdk"); - assertThat(entity.getId()) - .isEqualTo( - Attributes.builder() - .put("telemetry.sdk.name", "opentelemetry") - .put("telemetry.sdk.language", "java") - .build()); - assertThat(entity.getDescription()) - .isEqualTo(Attributes.builder().put("telemetry.sdk.version", OtelVersion.VERSION).build()); - assertThat(entity.getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.40.0"); - } -} From 25e35e6f762fabca01e495d532cb3c3151b06fa7 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 25 Jun 2026 02:31:58 +0000 Subject: [PATCH 08/22] Move experiment flag to common location. --- .../autoconfigure/EntityExperimentConstants.java | 14 ++++++++++++++ .../sdk/autoconfigure/EnvironmentResource.java | 5 +++-- .../autoconfigure/ResourceConfigurationTest.java | 10 +++++----- sdk-extensions/declarative-config/build.gradle.kts | 3 +++ .../declarativeconfig/ServiceResourceDetector.java | 3 ++- .../ServiceResourceDetectorTest.java | 8 ++++---- 6 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EntityExperimentConstants.java diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EntityExperimentConstants.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EntityExperimentConstants.java new file mode 100644 index 00000000000..8bef4950031 --- /dev/null +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EntityExperimentConstants.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +/** Constants for experimental entity SDK features. */ +final class EntityExperimentConstants { + + static final String EXPERIMENTAL_ENTITIES_ENABLED = "otel.experimental.entities.enabled"; + + private EntityExperimentConstants() {} +} 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 d45ca4b44ae..1fc91379e18 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 @@ -36,6 +36,7 @@ final class EnvironmentResource { // Visible for testing static final String ATTRIBUTE_PROPERTY = "otel.resource.attributes"; static final String SERVICE_NAME_PROPERTY = "otel.service.name"; + static final String ENTITIES_PROPERTY = "otel.entities"; /** * Create a {@link Resource} from the environment. The resource contains attributes parsed from @@ -47,11 +48,11 @@ final class EnvironmentResource { */ @SuppressWarnings("JdkObsolete") // Recommended alternative was introduced in java 10 static Resource createEnvironmentResource(ConfigProperties config) { - boolean entitiesEnabled = config.getBoolean("otel.experimental.entities.enabled", false); + boolean entitiesEnabled = config.getBoolean(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, false); if (entitiesEnabled) { ResourceBuilder builder = Resource.builder(); - String entitiesStr = config.getString("otel.entities"); + String entitiesStr = config.getString(ENTITIES_PROPERTY); if (entitiesStr != null && !entitiesStr.isEmpty()) { List parsedEntities = new EntityParser(entitiesStr).parse(); for (Entity entity : parsedEntities) { 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 6cd4f4d86cc..7dab3eeb665 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 @@ -173,10 +173,10 @@ void filterAttributes() { @Test void createEnvironmentResource_EntitiesEnabled() { Map props = new HashMap<>(); - props.put("otel.experimental.entities.enabled", "true"); + props.put(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, "true"); props.put( - "otel.entities", - "process{process.pid=1234}[process.executable.name=java]@http://schema;host{host.id=myhost}"); + EnvironmentResource.ENTITIES_PROPERTY, + "process{process.pid=1234}[process.executable.name=java]@http://schema;host{host.id=myhost}"); props.put("otel.service.name", "my-service"); props.put("otel.resource.attributes", "flat.attr=flat-val"); @@ -221,8 +221,8 @@ void createEnvironmentResource_EntitiesEnabled() { @Test void createEnvironmentResource_EntitiesDisabled() { Map props = new HashMap<>(); - props.put("otel.experimental.entities.enabled", "false"); - props.put("otel.entities", "process{process.pid=1234}"); + props.put(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, "false"); + props.put(EnvironmentResource.ENTITIES_PROPERTY, "process{process.pid=1234}"); props.put("otel.service.name", "my-service"); props.put("otel.resource.attributes", "flat.attr=flat-val"); diff --git a/sdk-extensions/declarative-config/build.gradle.kts b/sdk-extensions/declarative-config/build.gradle.kts index dc6db434c18..b4b6b6ad43c 100644 --- a/sdk-extensions/declarative-config/build.gradle.kts +++ b/sdk-extensions/declarative-config/build.gradle.kts @@ -172,6 +172,9 @@ val copyResourceConfiguration by tasks.registering(Copy::class) { from( project(":sdk-extensions:autoconfigure").file( "src/main/java/io/opentelemetry/sdk/autoconfigure/EnvironmentResource.java" + ), + project(":sdk-extensions:autoconfigure").file( + "src/main/java/io/opentelemetry/sdk/autoconfigure/EntityExperimentConstants.java" ) ) into(generatedResourceConfigDir.map { it.dir("io/opentelemetry/sdk/autoconfigure/declarativeconfig") }) 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 cd1335e8a49..2e90e29a815 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 @@ -47,7 +47,8 @@ public Resource create(DeclarativeConfigProperties config) { ConfigProperties properties = DefaultConfigProperties.create(Collections.emptyMap(), config.getComponentLoader()); - boolean entitiesEnabled = properties.getBoolean("otel.experimental.entities.enabled", false); + boolean entitiesEnabled = + properties.getBoolean(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, false); String serviceName = properties.getString("otel.service.name"); if (entitiesEnabled) { 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 index 1ecc7da40e6..096a76c997b 100644 --- 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 @@ -67,10 +67,10 @@ void create_NoSystemProperty() { @Test @ClearSystemProperty(key = "otel.service.name") - @ClearSystemProperty(key = "otel.experimental.entities.enabled") + @ClearSystemProperty(key = EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED) void create_EntitiesEnabled() { System.setProperty("otel.service.name", "my-service"); - System.setProperty("otel.experimental.entities.enabled", "true"); + System.setProperty(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, "true"); Resource resource = new ServiceResourceDetector().create(DeclarativeConfigProperties.empty()); @@ -101,10 +101,10 @@ void create_EntitiesEnabled() { @Test @ClearSystemProperty(key = "otel.service.name") - @ClearSystemProperty(key = "otel.experimental.entities.enabled") + @ClearSystemProperty(key = EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED) void create_EntitiesDisabled() { System.setProperty("otel.service.name", "my-service"); - System.setProperty("otel.experimental.entities.enabled", "false"); + System.setProperty(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, "false"); Resource resource = new ServiceResourceDetector().create(DeclarativeConfigProperties.empty()); From 6d786022f4bf23d1a0abe4059db8dab7572c4f5c Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 25 Jun 2026 11:54:33 +0000 Subject: [PATCH 09/22] Spotless fixes. --- .../opentelemetry/sdk/autoconfigure/EnvironmentResource.java | 3 ++- .../sdk/autoconfigure/ResourceConfigurationTest.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) 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 1fc91379e18..4b70aca9f49 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 @@ -48,7 +48,8 @@ final class EnvironmentResource { */ @SuppressWarnings("JdkObsolete") // Recommended alternative was introduced in java 10 static Resource createEnvironmentResource(ConfigProperties config) { - boolean entitiesEnabled = config.getBoolean(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, false); + boolean entitiesEnabled = + config.getBoolean(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, false); if (entitiesEnabled) { ResourceBuilder builder = Resource.builder(); 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 7dab3eeb665..74c8fc41187 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 @@ -175,8 +175,8 @@ void createEnvironmentResource_EntitiesEnabled() { Map props = new HashMap<>(); props.put(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, "true"); props.put( - EnvironmentResource.ENTITIES_PROPERTY, - "process{process.pid=1234}[process.executable.name=java]@http://schema;host{host.id=myhost}"); + EnvironmentResource.ENTITIES_PROPERTY, + "process{process.pid=1234}[process.executable.name=java]@http://schema;host{host.id=myhost}"); props.put("otel.service.name", "my-service"); props.put("otel.resource.attributes", "flat.attr=flat-val"); From d200481e12f11b3dd30350174da6c33e5af6d988 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Fri, 26 Jun 2026 16:43:35 +0000 Subject: [PATCH 10/22] minor fixes from review, tests to follow --- .../internal/otlp/ResourceMarshaler.java | 21 ++++++++++++++----- sdk-extensions/incubator/build.gradle.kts | 1 - 2 files changed, 16 insertions(+), 6 deletions(-) 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/sdk-extensions/incubator/build.gradle.kts b/sdk-extensions/incubator/build.gradle.kts index 8e12aacecd0..af02be590af 100644 --- a/sdk-extensions/incubator/build.gradle.kts +++ b/sdk-extensions/incubator/build.gradle.kts @@ -13,7 +13,6 @@ otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator", "io.o otelJava.osgiServiceLoaderProvides.set(listOf( "io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider", "io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider", - "io.opentelemetry.sdk.extension.incubator.resources.EntityDetector", )) dependencies { From c6fc930454d6eb1c27b34d05e97d7d8551692b59 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Fri, 26 Jun 2026 16:46:53 +0000 Subject: [PATCH 11/22] Add tests. --- .../internal/otlp/EntityRefMarshaler.java | 4 +- .../internal/otlp/ResourceMarshalerTest.java | 100 ++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/ResourceMarshalerTest.java 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 index e77ab7f8308..b464c810a59 100644 --- 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 @@ -76,8 +76,8 @@ private static int calculateSize( size += MarshalerUtil.sizeBytes(EntityRef.SCHEMA_URL, schemaUrlUtf8); } size += MarshalerUtil.sizeBytes(EntityRef.TYPE, typeUtf8); - MarshalerUtil.sizeRepeatedString(EntityRef.ID_KEYS, idKeysUtf8); - MarshalerUtil.sizeRepeatedString(EntityRef.DESCRIPTION_KEYS, descriptionKeysUtf8); + size += MarshalerUtil.sizeRepeatedString(EntityRef.ID_KEYS, idKeysUtf8); + size += MarshalerUtil.sizeRepeatedString(EntityRef.DESCRIPTION_KEYS, descriptionKeysUtf8); return size; } } 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..f75aed04781 --- /dev/null +++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/ResourceMarshalerTest.java @@ -0,0 +1,100 @@ +/* + * 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") + .setId(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); + } +} From 09f0c57e68c4b85c90de2234609df54c1ddcc65d Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 29 Jun 2026 12:48:51 +0000 Subject: [PATCH 12/22] Simplify Resource, fix tests. --- .../io/opentelemetry/sdk/resources/Resource.java | 14 ++++++++++++-- .../opentelemetry/sdk/resources/ResourceTest.java | 3 +-- .../sdk/logs/SdkLoggerProviderTest.java | 2 +- .../sdk/trace/SdkSpanBuilderTest.java | 3 --- 4 files changed, 14 insertions(+), 8 deletions(-) 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 91fba3b6361..d1f6ad15441 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 @@ -126,7 +126,7 @@ static Resource create( }); // In merge rules, raw comes last, so we return these last. fullAttributes.putAll(attributes); - return new AutoValue_Resource(schemaUrl, attributes, entities, fullAttributes.build()); + return new AutoValue_Resource(schemaUrl, entities, fullAttributes.build()); } /** @@ -143,7 +143,17 @@ static Resource create( * * @return a map of attributes. */ - abstract Attributes getRawAttributes(); + final Attributes getRawAttributes() { + AttributesBuilder rawAttributes = getAttributes().toBuilder(); + rawAttributes.removeIf( + key -> + getEntities().stream() + .anyMatch( + entity -> + entity.getId().get(key) != null + || entity.getDescription().get(key) != null)); + return rawAttributes.build(); + } /** * Returns a collection of associated entities. diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java index d00e6154747..f171c5d9ff7 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java @@ -208,8 +208,7 @@ void testToString() { Attributes attribute1 = Attributes.of(stringKey("a"), "1", stringKey("b"), "2"); Resource resource = Resource.create(attribute1, "http://schema"); assertThat(resource.toString()) - .isEqualTo( - "Resource{schemaUrl=http://schema, rawAttributes={a=\"1\", b=\"2\"}, entities=[], attributes={a=\"1\", b=\"2\"}}"); + .isEqualTo("Resource{schemaUrl=http://schema, entities=[], attributes={a=\"1\", b=\"2\"}}"); } @Test diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java index 28774268598..fe46e763fab 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java @@ -346,7 +346,7 @@ void toString_Valid() { assertThat(sdkLoggerProvider.toString()) .matches( "SdkLoggerProvider\\{clock=SystemClock\\{\\}, " - + "resource=Resource\\{schemaUrl=null, rawAttributes=\\{key=\"value\"\\}, entities=\\[\\], attributes=\\{key=\"value\"\\}\\}, " + + "resource=Resource\\{schemaUrl=null, entities=\\[\\], attributes=\\{key=\"value\"\\}\\}, " + "logLimits=LogLimits\\{maxNumberOfAttributes=128, maxAttributeValueLength=2147483647\\}, " + "logRecordProcessor=MockLogRecordProcessor, " + "loggerConfigurator=ScopeConfiguratorImpl\\{conditions=\\[\\]\\}\\}"); diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java index 26b9bf8967f..d9371c1c827 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java @@ -1109,9 +1109,6 @@ void spanDataToString() { + "traceFlags=00, " + "traceState=ArrayBasedTraceState\\{entries=\\[]}, remote=false, valid=false}, " + "resource=Resource\\{schemaUrl=null, " - + "rawAttributes=\\{service.name=\"unknown_service:java\", " - + "telemetry.sdk.language=\"java\", telemetry.sdk.name=\"opentelemetry\", " - + "telemetry.sdk.version=\"\\d+.\\d+.\\d+(-rc.\\d+)?(-SNAPSHOT)?\"\\}, " + "entities=\\[\\], " + "attributes=\\{service.name=\"unknown_service:java\", " + "telemetry.sdk.language=\"java\", telemetry.sdk.name=\"opentelemetry\", " From e2aec2a72714c551171473dbea151d090baf61fe Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 29 Jun 2026 12:54:07 +0000 Subject: [PATCH 13/22] Fix nit about builder method. --- .../io/opentelemetry/sdk/resources/internal/Entity.java | 2 +- .../opentelemetry/sdk/resources/internal/SdkEntity.java | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) 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 index c088c61979e..2e647e4fc0e 100644 --- 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 @@ -68,6 +68,6 @@ public interface Entity { * @param entityType the entity type string of this entity. */ static EntityBuilder builder(String entityType) { - return SdkEntity.builder(entityType); + return new SdkEntityBuilder(entityType); } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntity.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntity.java index dc54d47ee88..4aa65d2942b 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntity.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntity.java @@ -36,13 +36,4 @@ static Entity create( public final EntityBuilder toBuilder() { return new SdkEntityBuilder(this); } - - /** - * Returns a new {@link EntityBuilder} instance for creating arbitrary {@link Entity}. - * - * @param entityType the entity type string of this entity. - */ - public static final EntityBuilder builder(String entityType) { - return new SdkEntityBuilder(entityType); - } } From c83a242acc8b3204abb6ecb746a9ef064b6f0a82 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 29 Jun 2026 14:04:17 +0000 Subject: [PATCH 14/22] Remove entities from EnvironmentResource. Create new SHELL=/bin/bash QT_ACCESSIBILITY=1 COLORTERM=truecolor HISTCONTROL=ignoredups XDG_MENU_PREFIX=gnome- TERM_PROGRAM_VERSION=1.107.0 GNOME_DESKTOP_SESSION_ID=this-is-deprecated GTK_IM_MODULE=ibus QT_IM_MODULES=wayland;ibus PULSE_RUNTIME_PATH=/run/user/110055/crd_audio#_Bm5JrFQNq CHROME_REMOTE_DESKTOP_HOST_EXTRA_PARAMS=--enable-utempter P4CONFIG=.p4config SSH_AUTH_SOCK=/run/user/110055/crd_ssh_auth_sock MEMORY_PRESSURE_WRITE=c29tZSAyMDAwMDAgMjAwMDAwMAA= XMODIFIERS=@im=ibus X20_HOME=/google/data/rw/users/jo/joshuasuereth ANTIGRAVITY_CLI_ALIAS=jetski-ide GTK_MODULES=gail:atk-bridge PWD=/usr/local/google/home/joshuasuereth/projects/open-telemetry/opentelemetry-java RSYNC_RSH=ssh LOGNAME=joshuasuereth XDG_SESSION_TYPE=x11 CHROME_CONFIG_HOME=/usr/local/google/home/joshuasuereth/.config/chrome-remote-desktop/chrome-config GPG_AGENT_INFO=/run/user/110055/gnupg/S.gpg-agent:0:1 SYSTEMD_EXEC_PID=4924 XAUTHORITY=/usr/local/google/home/joshuasuereth/.Xauthority VSCODE_GIT_ASKPASS_NODE=/opt/jetski-ide/jetski GOOGLE_API_CERTIFICATE_CONFIG=/etc/gcloud/certificate_config.json P4MERGE=/google/src/files/head/depot/eng/perforce/mergep4.tcl PULSE_SINK=chrome_remote_desktop_session HOME=/usr/local/google/home/joshuasuereth LANG=en_US.UTF-8 CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE=true LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=00:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.7z=01;31:*.ace=01;31:*.alz=01;31:*.apk=01;31:*.arc=01;31:*.arj=01;31:*.bz=01;31:*.bz2=01;31:*.cab=01;31:*.cpio=01;31:*.crate=01;31:*.deb=01;31:*.drpm=01;31:*.dwm=01;31:*.dz=01;31:*.ear=01;31:*.egg=01;31:*.esd=01;31:*.gz=01;31:*.jar=01;31:*.lha=01;31:*.lrz=01;31:*.lz=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.lzo=01;31:*.pyz=01;31:*.rar=01;31:*.rpm=01;31:*.rz=01;31:*.sar=01;31:*.swm=01;31:*.t7z=01;31:*.tar=01;31:*.taz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tgz=01;31:*.tlz=01;31:*.txz=01;31:*.tz=01;31:*.tzo=01;31:*.tzst=01;31:*.udeb=01;31:*.war=01;31:*.whl=01;31:*.wim=01;31:*.xz=01;31:*.z=01;31:*.zip=01;31:*.zoo=01;31:*.zst=01;31:*.avif=01;35:*.jpg=01;35:*.jpeg=01;35:*.jxl=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:*~=00;90:*#=00;90:*.bak=00;90:*.crdownload=00;90:*.dpkg-dist=00;90:*.dpkg-new=00;90:*.dpkg-old=00;90:*.dpkg-tmp=00;90:*.old=00;90:*.orig=00;90:*.part=00;90:*.rej=00;90:*.rpmnew=00;90:*.rpmorig=00;90:*.rpmsave=00;90:*.swp=00;90:*.tmp=00;90:*.ucf-dist=00;90:*.ucf-new=00;90:*.ucf-old=00;90: XDG_CURRENT_DESKTOP=GNOME PYTHONSTARTUP=/usr/local/google/home/joshuasuereth/.config/Jetski/User/workspaceStorage/cbb7ec0eec555b4d06671311926a9b6f/ms-python.python/pythonrc.py MEMORY_PRESSURE_WATCH=/sys/fs/cgroup/user.slice/user-110055.slice/user@110055.service/session.slice/org.gnome.Shell@x11.service/memory.pressure VTE_VERSION=8390 VSCODE_JAVA_EXEC=/usr/lib/jvm/java-21-openjdk-amd64/bin/java GIT_ASKPASS=/opt/jetski-ide/resources/app/extensions/git/dist/askpass.sh GNOME_TERMINAL_SCREEN=/org/gnome/Terminal/screen/1538e342_19f1_4bdb_9b1d_20e770cebc04 STREAMZ_SERVERS=[2001:4860:f802::78]:9530 CHROME_DESKTOP=jetski.desktop CLUTTER_IM_MODULE=ibus INFOPATH=/usr/share/emacs-google-config/info: CHROME_REMOTE_DESKTOP_SESSION=1 VSCODE_GIT_ASKPASS_EXTRA_ARGS= VSCODE_PYTHON_AUTOACTIVATE_GUARD=1 CLOUDSDK_CONTAINER_USE_APPLICATION_DEFAULT_CREDENTIALS=true LESSCLOSE=/usr/bin/lesspipe %s %s XDG_SESSION_CLASS=user PYTHONPATH=/usr/local/buildtools/current/sitecustomize TERM=xterm-256color PYTHON_BASIC_REPL=1 LESSOPEN=| /usr/bin/lesspipe %s SK_SIGNING_PLUGIN=gnubbyagent USER=joshuasuereth VSCODE_GIT_IPC_HANDLE=/run/user/110055/vscode-git-e5a8f99bf6.sock GNOME_TERMINAL_SERVICE=:1.94 CHROME_REMOTE_DESKTOP_DEFAULT_DESKTOP_SIZES=1600x1200,3840x2160,3840x2560,5120x1440,2160x3840 CLOUDSDK_CONTEXT_AWARE_USE_MTLS_FOR_GRPC=true DISPLAY=:20 SHLVL=1 PARINIT=rTbgqR B=.?_A_a Q=_s>|: CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH=/etc/gcloud/certificate_config.json GOOGLE_AUTH_WEBAUTHN_PLUGIN=gcloudwebauthn QT_IM_MODULE=ibus CVS_RSH=ssh MANAGERPIDFDID=2490 FC_FONTATIONS=1 LD_LIBRARY_PATH=/usr/lib/mesa-diverted/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu/mesa:/usr/lib/x86_64-linux-gnu/dri:/usr/lib/x86_64-linux-gnu/gallium-pipe XDG_RUNTIME_DIR=/run/user/110055 PIPEWIRE_REMOTE=crd_audio#_Bm5JrFQNq/pipewire VSCODE_GIT_ASKPASS_MAIN=/opt/jetski-ide/resources/app/extensions/git/dist/askpass-main.js GTK3_MODULES=xapp-gtk3-module VSCODE_JDWP_ADAPTER_ENDPOINTS=/usr/local/google/home/joshuasuereth/.jetski/extensions/vscjava.vscode-java-debug-0.59.0-universal/.noConfigDebugAdapterEndpoints/endpoint-c1594771e7136dbf.txt XDG_DATA_DIRS=/usr/share/gnome:/usr/local/share/:/usr/share/ GDK_BACKEND=x11 PATH=/usr/local/google/home/joshuasuereth/.local/bin:/usr/local/google/home/joshuasuereth/bin:/usr/local/google/home/joshuasuereth/.cargo/bin:/usr/local/google/home/joshuasuereth/.local/bin:/usr/local/google/home/joshuasuereth/bin:/usr/lib/google-golang/bin:/usr/local/buildtools/java/jdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/google/home/joshuasuereth/.jetski/extensions/vscjava.vscode-java-debug-0.59.0-universal/bundled/scripts/noConfigScripts GOOGLE_CLOUD_DISABLE_DIRECT_PATH=true CLOUDSDK_INTERNAL_USER=true DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/110055/bus CLOUDSDK_CONTEXT_AWARE_USE_ECP_HTTP_PROXY=true DATA_CLOUD_CURR_IDE_NAME=jetski TERM_PROGRAM=vscode BASH_FUNC_poetry%%=() { /usr/bin/gpkg poetry "$@" } BASH_FUNC_yarn%%=() { /usr/bin/gpkg yarn "$@" } BASH_FUNC_pip%%=() { /usr/bin/gpkg pip "$@" } BASH_FUNC_pdm%%=() { /usr/bin/gpkg pdm "$@" } BASH_FUNC_uvx%%=() { /usr/bin/gpkg uvx "$@" } BASH_FUNC_npm%%=() { /usr/bin/gpkg npm "$@" } BASH_FUNC_npx%%=() { /usr/bin/gpkg npx "$@" } BASH_FUNC_pip3%%=() { /usr/bin/gpkg pip3 "$@" } BASH_FUNC_python3%%=() { /usr/bin/gpkg python3 "$@" } BASH_FUNC_pipx%%=() { /usr/bin/gpkg pipx "$@" } BASH_FUNC_pnpx%%=() { /usr/bin/gpkg pnpx "$@" } BASH_FUNC_pnpm%%=() { /usr/bin/gpkg pnpm "$@" } BASH_FUNC_yarnpkg%%=() { /usr/bin/gpkg yarnpkg "$@" } BASH_FUNC_uv%%=() { /usr/bin/gpkg uv "$@" } BASH_FUNC_python%%=() { /usr/bin/gpkg python "$@" } BASH_FUNC_hatch%%=() { /usr/bin/gpkg hatch "$@" } _=/usr/bin/env resource providerr. --- .../autoconfigure/EnvironmentResource.java | 327 ---------------- .../ResourceConfigurationTest.java | 73 ---- .../resources/EnvResourceProvider.java | 356 ++++++++++++++++++ ...try.sdk.autoconfigure.spi.ResourceProvider | 1 + ...toconfigure.spi.internal.ComponentProvider | 1 + .../resources/EnvResourceProviderTest.java | 69 ++++ 6 files changed, 427 insertions(+), 400 deletions(-) create mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProvider.java create mode 100644 sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider create mode 100644 sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java 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 4b70aca9f49..67372de748b 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 @@ -10,17 +10,8 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; 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. @@ -36,7 +27,6 @@ final class EnvironmentResource { // Visible for testing static final String ATTRIBUTE_PROPERTY = "otel.resource.attributes"; static final String SERVICE_NAME_PROPERTY = "otel.service.name"; - static final String ENTITIES_PROPERTY = "otel.entities"; /** * Create a {@link Resource} from the environment. The resource contains attributes parsed from @@ -48,33 +38,6 @@ final class EnvironmentResource { */ @SuppressWarnings("JdkObsolete") // Recommended alternative was introduced in java 10 static Resource createEnvironmentResource(ConfigProperties config) { - boolean entitiesEnabled = - config.getBoolean(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, false); - if (entitiesEnabled) { - ResourceBuilder builder = Resource.builder(); - - String entitiesStr = config.getString(ENTITIES_PROPERTY); - if (entitiesStr != null && !entitiesStr.isEmpty()) { - List parsedEntities = new EntityParser(entitiesStr).parse(); - for (Entity entity : parsedEntities) { - EntityUtil.addEntity(builder, entity); - } - } - - String serviceName = config.getString(SERVICE_NAME_PROPERTY); - if (serviceName != null) { - Entity serviceEntity = - Entity.builder("service").setId(Attributes.of(SERVICE_NAME, serviceName)).build(); - EntityUtil.addEntity(builder, serviceEntity); - } - - for (Map.Entry entry : config.getMap(ATTRIBUTE_PROPERTY).entrySet()) { - builder.put(entry.getKey(), decodeResourceAttributes(entry.getValue())); - } - - return builder.build(); - } - AttributesBuilder resourceAttributes = Attributes.builder(); for (Map.Entry entry : config.getMap(ATTRIBUTE_PROPERTY).entrySet()) { resourceAttributes.put( @@ -140,294 +103,4 @@ private static String decodeResourceAttributes(String value) { } private EnvironmentResource() {} - - 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()); - - /** - * The current state of parsing. - * - *

The format is TYPE{KEY1=VAL1,KEY2=VAL2}[ATTR1=VAL1,ATTR2=VAL2]@SCHEMA_URL; - * - *

The parser state machine transitions between the following states: - TYPE: Parsing an - * entity type - ID_KEY: Parsing a "key" of an identity attribute - ID_VAL: Parsing a "value" of - * an identity attribute - DESC_KEY: Parsing a "key" of a description attribute - DESC_VAL: - * Parsing a "value" of a description attribute - SCHEMA_URL: Parsing the schema URL of a - * specific entity - SKIP_TO_NEXT: Skip to the next entity - */ - private enum State { - TYPE, - ID_KEY, - ID_VAL, - DESC_KEY, - DESC_VAL, - SCHEMA_URL, - SKIP_TO_NEXT - } - - /** The input entity string. */ - private final String input; - - /** The current state of parsing. (i.e. where we are in the grammar) */ - private State state = State.TYPE; - - /** The segment of the input string that we are currently parsing. */ - private final Segment currentSegment; - - /** The list of entities we've parsed. */ - private final List entities = new ArrayList<>(); - - // Temporary state for building an entity. - - /** The parsed entity type. */ - @Nullable private String currentType; - - /** Parsed attributes denoting the entity identity. */ - private Attributes currentIdAttrs = Attributes.empty(); - - /** Parsed attributes denoting the entity description. */ - private Attributes currentDescAttrs = Attributes.empty(); - - /** Parsed schema URL for the entity. */ - @Nullable private String currentSchemaUrl; - - /** A temporary builder we use when parsing key-value pairs for identity or description. */ - @Nullable private AttributesBuilder currentBuilder; - - /** The current key of a key-value pair that we are parsing. */ - @Nullable private String currentKey; - - EntityParser(String input) { - this.input = input; - this.currentSegment = new Segment(input); - } - - /** - * Parses the input string and returns a list of entities. - * - * @return the list of entities parsed from the input string. - */ - List parse() { - int n = input.length(); - for (int i = 0; i < n; i++) { - char c = input.charAt(i); - - // We finished the previous entity, or hit a syntax error. - // Skip to the next entity and try to parse it. - if (state == State.SKIP_TO_NEXT) { - if (c == ';') { - resetEntityState(i + 1); - state = State.TYPE; - } - continue; - } - - switch (c) { - case '{': - // Finish writing entity type, start identity parsing. - if (state == State.TYPE) { - currentSegment.markEnd(i); - currentType = currentSegment.getValue(); - if (currentType == null || currentType.isEmpty()) { - logger.log(Level.WARNING, "Malformed entity definition (empty type): " + input); - state = State.SKIP_TO_NEXT; - } else { - state = State.ID_KEY; - currentSegment.reset(i + 1); - currentBuilder = Attributes.builder(); - } - } - break; - case '}': - // End identity parsing. - if (state == State.ID_VAL || state == State.ID_KEY) { - currentSegment.markEnd(i); - if (state == State.ID_VAL) { - putAttr(); - } - if (currentBuilder != null) { - currentIdAttrs = currentBuilder.build(); - } - if (currentIdAttrs.isEmpty()) { - logger.log( - Level.WARNING, - "Malformed entity definition (missing identifying attributes): " + input); - state = State.SKIP_TO_NEXT; - } else { - state = State.TYPE; // Default next state, might change if [ or @ follows - currentSegment.reset(i + 1); - } - } - break; - case '[': - // We finished identity, we're moving to parse description. - if (state == State.TYPE) { - // After } we are in TYPE state again but expecting [ or @ or ; - state = State.DESC_KEY; - currentSegment.reset(i + 1); - currentBuilder = Attributes.builder(); - } - break; - case ']': - // We finished description, update attributes for description and move - // back to TYPE state. - if (state == State.DESC_VAL || state == State.DESC_KEY) { - currentSegment.markEnd(i); - if (state == State.DESC_VAL) { - putAttr(); - } - if (currentBuilder != null) { - currentDescAttrs = currentBuilder.build(); - } - state = State.TYPE; - currentSegment.reset(i + 1); - } - break; - case '=': - // Finish our "key" parsing and start looking for a value. - if (state == State.ID_KEY || state == State.DESC_KEY) { - currentSegment.markEnd(i); - currentKey = currentSegment.getValue(); - if (currentKey == null || currentKey.isEmpty()) { - logger.log(Level.WARNING, "Malformed key-value pair (empty key): " + input); - state = State.SKIP_TO_NEXT; - } else { - state = (state == State.ID_KEY) ? State.ID_VAL : State.DESC_VAL; - currentSegment.reset(i + 1); - } - } - break; - case ',': - // Finish our "value" parsing and start looking for the next key-value. - if (state == State.ID_VAL || state == State.DESC_VAL) { - currentSegment.markEnd(i); - putAttr(); - state = (state == State.ID_VAL) ? State.ID_KEY : State.DESC_KEY; - currentSegment.reset(i + 1); - } - break; - case '@': - // Start looking for schema url - if (state == State.TYPE) { - state = State.SCHEMA_URL; - currentSegment.reset(i + 1); - } - break; - case ';': - // Finish up the current entity, and get ready to parse the next. - if (state == State.TYPE || state == State.SCHEMA_URL) { - if (state == State.SCHEMA_URL) { - currentSegment.markEnd(i); - currentSchemaUrl = currentSegment.getValue(); - } - buildAndAddEntity(); - resetEntityState(i + 1); - state = State.TYPE; - } else if (state == State.ID_KEY - || state == State.ID_VAL - || state == State.DESC_KEY - || state == State.DESC_VAL) { - logger.log(Level.WARNING, "Malformed entity definition (unexpected ';'): " + input); - resetEntityState(i + 1); - state = State.TYPE; - } - break; - case '%': - // Found an escape character, mark the segment as needing decoding, which - // requires special handling. - currentSegment.markNeedsDecoding(); - break; - default: - break; - } - } - - // Handle end of string - if (state == State.TYPE || state == State.SCHEMA_URL) { - if (state == State.SCHEMA_URL) { - currentSegment.markEnd(input.length()); - currentSchemaUrl = currentSegment.getValue(); - } - buildAndAddEntity(); - } - - return entities; - } - - /** Adds the current attribute key-value pair into the current attribute builder. */ - private void putAttr() { - String val = currentSegment.getValue(); - if (currentKey != null && !currentKey.isEmpty() && currentBuilder != null) { - currentBuilder.put(currentKey, val); - } - } - - /** Finishes building the current entity and adds it to the parsed list. */ - private void buildAndAddEntity() { - if (currentType != null && !currentType.isEmpty() && !currentIdAttrs.isEmpty()) { - EntityBuilder builder = Entity.builder(currentType).setId(currentIdAttrs); - if (!currentDescAttrs.isEmpty()) { - builder.setDescription(currentDescAttrs); - } - if (currentSchemaUrl != null && !currentSchemaUrl.isEmpty()) { - builder.setSchemaUrl(currentSchemaUrl); - } - entities.add(builder.build()); - } - } - - /** - * Resets the state of the entity parser. - * - * @param nextStart the start index of the next entity (e.g. after the `;`). - */ - private void resetEntityState(int nextStart) { - currentType = null; - currentIdAttrs = Attributes.empty(); - currentDescAttrs = Attributes.empty(); - currentSchemaUrl = null; - currentBuilder = null; - currentKey = null; - currentSegment.reset(nextStart); - } - } } diff --git a/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceConfigurationTest.java b/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceConfigurationTest.java index 74c8fc41187..d8e97e382ca 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,9 +17,6 @@ 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.Collection; import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; @@ -169,74 +166,4 @@ void filterAttributes() { assertThat(resource.getAttributes().get(stringKey("bar"))).isNull(); }); } - - @Test - void createEnvironmentResource_EntitiesEnabled() { - Map props = new HashMap<>(); - props.put(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, "true"); - props.put( - EnvironmentResource.ENTITIES_PROPERTY, - "process{process.pid=1234}[process.executable.name=java]@http://schema;host{host.id=myhost}"); - props.put("otel.service.name", "my-service"); - props.put("otel.resource.attributes", "flat.attr=flat-val"); - - Resource resource = - ResourceConfiguration.createEnvironmentResource( - DefaultConfigProperties.createFromMap(props)); - - Collection entities = EntityUtil.getEntities(resource); - assertThat(entities).hasSize(3); - - assertThat(entities) - .anyMatch( - e -> - e.getType().equals("process") - && e.getSchemaUrl().equals("http://schema") - && e.getId().equals(Attributes.of(stringKey("process.pid"), "1234")) - && e.getDescription() - .equals(Attributes.of(stringKey("process.executable.name"), "java"))); - - assertThat(entities) - .anyMatch( - e -> - e.getType().equals("host") - && e.getSchemaUrl() == null - && e.getId().equals(Attributes.of(stringKey("host.id"), "myhost"))); - - assertThat(entities) - .anyMatch( - e -> - e.getType().equals("service") - && e.getId().equals(Attributes.of(stringKey("service.name"), "my-service"))); - - // The attributes of the entities should also be flat attributes on the Resource - assertThat(resource.getAttributes()) - .containsEntry(stringKey("service.name"), "my-service") - .containsEntry("process.pid", "1234") - .containsEntry("process.executable.name", "java") - .containsEntry("host.id", "myhost") - .containsEntry("flat.attr", "flat-val"); - } - - @Test - void createEnvironmentResource_EntitiesDisabled() { - Map props = new HashMap<>(); - props.put(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, "false"); - props.put(EnvironmentResource.ENTITIES_PROPERTY, "process{process.pid=1234}"); - props.put("otel.service.name", "my-service"); - props.put("otel.resource.attributes", "flat.attr=flat-val"); - - Resource resource = - ResourceConfiguration.createEnvironmentResource( - DefaultConfigProperties.createFromMap(props)); - - Collection entities = EntityUtil.getEntities(resource); - assertThat(entities).isEmpty(); - - assertThat(resource.getAttributes()) - .containsEntry(stringKey("service.name"), "my-service") - .containsEntry("flat.attr", "flat-val") - // otel.entities is ignored when entities are disabled - .doesNotContainKey(stringKey("process.pid")); - } } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProvider.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProvider.java new file mode 100644 index 00000000000..9a4f9174f45 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProvider.java @@ -0,0 +1,356 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +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.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * {@link ResourceProvider} and {@link ComponentProvider} for detecting resources via the {@code + * OTEL_ENTITIES} environment variable. + */ +public final class EnvResourceProvider implements ResourceProvider, ComponentProvider { + + private static final String ENTITIES_PROPERTY = "otel.entities"; + + @Override + public Resource createResource(ConfigProperties config) { + return createResourceInternal(config); + } + + @Override + public int order() { + return 0; + } + + @Override + public Class getType() { + return Resource.class; + } + + @Override + public String getName() { + return "env"; + } + + @Override + public Resource create(DeclarativeConfigProperties config) { + ConfigProperties properties = + DefaultConfigProperties.create(Collections.emptyMap(), config.getComponentLoader()); + return createResourceInternal(properties); + } + + private static Resource createResourceInternal(ConfigProperties config) { + String entitiesStr = config.getString(ENTITIES_PROPERTY); + if (entitiesStr == null || entitiesStr.isEmpty()) { + return Resource.empty(); + } + + ResourceBuilder builder = Resource.builder(); + List parsedEntities = new EntityParser(entitiesStr).parse(); + for (Entity entity : parsedEntities) { + EntityUtil.addEntity(builder, entity); + } + return builder.build(); + } + + /** + * Decodes percent-encoded characters in resource attribute values per W3C Baggage spec. + * + *

Unlike {@link java.net.URLDecoder}, this method: + * + *

    + *
  • Preserves '+' as a literal plus sign (URLDecoder decodes '+' as space) + *
  • Preserves invalid percent sequences as literals (e.g., "%2G", "%", "%2") + *
  • Supports multi-byte UTF-8 sequences (e.g., "%C3%A9" decodes to "é") + *
+ * + * @param value the percent-encoded string + * @return the decoded string + */ + private static String decodeResourceAttributes(String value) { + // no percent signs means nothing to decode + if (value.indexOf('%') < 0) { + return value; + } + + int n = value.length(); + // Use byte array to properly handle multi-byte UTF-8 sequences + byte[] bytes = new byte[n]; + int pos = 0; + + for (int i = 0; i < n; i++) { + char c = value.charAt(i); + // Check for percent-encoded sequence i.e. '%' followed by two hex digits + if (c == '%' && i + 2 < n) { + int d1 = Character.digit(value.charAt(i + 1), 16); + int d2 = Character.digit(value.charAt(i + 2), 16); + // Valid hex digits return 0-15, invalid returns -1 + if (d1 != -1 && d2 != -1) { + // Combine two hex digits into a single byte (e.g., "2F" becomes 0x2F) + bytes[pos++] = (byte) ((d1 << 4) + d2); + // Skip the two hex digits (loop will also do i++) + i += 2; + continue; + } + } + // Keep '+' as '+' (unlike URLDecoder) and preserve invalid percent sequences + // which will be + // treated as literals + bytes[pos++] = (byte) c; + } + 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).setId(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); + } + } +} diff --git a/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider b/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider index 189af738dcf..a39b8f1eecf 100644 --- a/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider +++ b/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider @@ -1 +1,2 @@ io.opentelemetry.sdk.extension.incubator.resources.ServiceInstanceIdResourceProvider +io.opentelemetry.sdk.extension.incubator.resources.EnvResourceProvider diff --git a/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider b/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider new file mode 100644 index 00000000000..7c5a45f4dae --- /dev/null +++ b/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider @@ -0,0 +1 @@ +io.opentelemetry.sdk.extension.incubator.resources.EnvResourceProvider diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java new file mode 100644 index 00000000000..9b90290609f --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.resources; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +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.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class EnvResourceProviderTest { + + @Test + void getTypeAndName() { + EnvResourceProvider provider = new EnvResourceProvider(); + assertThat(provider.getType()).isEqualTo(Resource.class); + assertThat(provider.getName()).isEqualTo("env"); + assertThat(provider.order()).isEqualTo(0); + } + + @Test + void createResource_Empty() { + EnvResourceProvider provider = new EnvResourceProvider(); + Resource resource = + provider.createResource(DefaultConfigProperties.createFromMap(Collections.emptyMap())); + assertThat(EntityUtil.getEntities(resource)).isEmpty(); + } + + @Test + void createResource_WithEntities() { + Map props = new HashMap<>(); + props.put( + "otel.entities", + "process{process.pid=1234}[process.executable.name=java]@http://schema;host{host.id=myhost}"); + + EnvResourceProvider provider = new EnvResourceProvider(); + Resource resource = provider.createResource(DefaultConfigProperties.createFromMap(props)); + + Collection entities = EntityUtil.getEntities(resource); + assertThat(entities).hasSize(2); + + assertThat(entities) + .anyMatch( + e -> + e.getType().equals("process") + && e.getSchemaUrl().equals("http://schema") + && e.getId().equals(Attributes.of(stringKey("process.pid"), "1234")) + && e.getDescription() + .equals(Attributes.of(stringKey("process.executable.name"), "java"))); + + assertThat(entities) + .anyMatch( + e -> + e.getType().equals("host") + && e.getSchemaUrl() == null + && e.getId().equals(Attributes.of(stringKey("host.id"), "myhost"))); + } +} From a92a7ba8b52ec8078df37c88aa2610d239bd60f0 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 29 Jun 2026 14:09:14 +0000 Subject: [PATCH 15/22] Flesh out tests. --- .../resources/EnvResourceProviderTest.java | 70 ++++++++++++++++++- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java index 9b90290609f..208f6bebb0f 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java @@ -9,6 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.internal.Entity; @@ -30,11 +31,17 @@ void getTypeAndName() { } @Test - void createResource_Empty() { + void createResource_EmptyOrNull() { EnvResourceProvider provider = new EnvResourceProvider(); - Resource resource = + + Resource emptyResource = provider.createResource(DefaultConfigProperties.createFromMap(Collections.emptyMap())); - assertThat(EntityUtil.getEntities(resource)).isEmpty(); + assertThat(EntityUtil.getEntities(emptyResource)).isEmpty(); + + Resource blankResource = + provider.createResource( + DefaultConfigProperties.createFromMap(Collections.singletonMap("otel.entities", ""))); + assertThat(EntityUtil.getEntities(blankResource)).isEmpty(); } @Test @@ -66,4 +73,61 @@ void createResource_WithEntities() { && e.getSchemaUrl() == null && e.getId().equals(Attributes.of(stringKey("host.id"), "myhost"))); } + + @Test + void createResource_PercentDecoding() { + Map props = new HashMap<>(); + props.put( + "otel.entities", + "service{service.name=my+app,space=hello%20world,utf8=%C3%A9,invalid=%2G,incomplete=%2,end=%}"); + + EnvResourceProvider provider = new EnvResourceProvider(); + Resource resource = provider.createResource(DefaultConfigProperties.createFromMap(props)); + + Collection entities = EntityUtil.getEntities(resource); + assertThat(entities).hasSize(1); + + Entity entity = entities.iterator().next(); + assertThat(entity.getId()) + .containsEntry(stringKey("service.name"), "my+app") + .containsEntry(stringKey("space"), "hello world") + .containsEntry(stringKey("utf8"), "é") + .containsEntry(stringKey("invalid"), "%2G") + .containsEntry(stringKey("incomplete"), "%2") + .containsEntry(stringKey("end"), "%"); + } + + @Test + void createResource_Malformed() { + Map props = new HashMap<>(); + props.put( + "otel.entities", + "{empty.type=val};process{};process{=val};process{key;=val};host{host.id=valid}"); + + EnvResourceProvider provider = new EnvResourceProvider(); + Resource resource = provider.createResource(DefaultConfigProperties.createFromMap(props)); + + Collection entities = EntityUtil.getEntities(resource); + // Only the last valid host entity should be parsed + assertThat(entities).hasSize(1); + assertThat(entities.iterator().next().getType()).isEqualTo("host"); + } + + @Test + void create_ComponentProvider() { + System.setProperty("otel.entities", "service{service.name=my-service}"); + try { + EnvResourceProvider provider = new EnvResourceProvider(); + Resource resource = provider.create(DeclarativeConfigProperties.empty()); + Collection entities = EntityUtil.getEntities(resource); + assertThat(entities).hasSize(1); + assertThat(entities) + .anyMatch( + e -> + e.getType().equals("service") + && e.getId().equals(Attributes.of(stringKey("service.name"), "my-service"))); + } finally { + System.clearProperty("otel.entities"); + } + } } From 169d186891768a105b9df56c018111b7109a1769 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 29 Jun 2026 14:16:17 +0000 Subject: [PATCH 16/22] Fix build time dependency issue my IDE missed. --- sdk-extensions/incubator/build.gradle.kts | 2 ++ .../extension/incubator/resources/EnvResourceProviderTest.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk-extensions/incubator/build.gradle.kts b/sdk-extensions/incubator/build.gradle.kts index af02be590af..1ede46662f2 100644 --- a/sdk-extensions/incubator/build.gradle.kts +++ b/sdk-extensions/incubator/build.gradle.kts @@ -25,6 +25,8 @@ dependencies { compileOnly(project(":sdk-extensions:autoconfigure-spi")) testImplementation(project(":sdk:testing")) + testImplementation(project(":api:incubator")) + testImplementation(project(":sdk-extensions:autoconfigure-spi")) testImplementation(project(":sdk-extensions:autoconfigure")) testImplementation("edu.berkeley.cs.jqf:jqf-fuzz") testImplementation("com.google.guava:guava-testlib") diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java index 208f6bebb0f..dabcb73d9d2 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java @@ -6,7 +6,7 @@ package io.opentelemetry.sdk.extension.incubator.resources; import static io.opentelemetry.api.common.AttributeKey.stringKey; -import static org.assertj.core.api.Assertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; From 632cf30453384632e1a9e242cf489281e4725e93 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 29 Jun 2026 15:19:19 +0000 Subject: [PATCH 17/22] Fix tests and move constants. --- .../internal/EntityExperimentConstants.java | 20 +++ .../EntityExperimentConstants.java | 14 --- .../declarative-config/build.gradle.kts | 3 - .../ServiceResourceDetector.java | 1 + .../DeclarativeConfigurationCreateTest.java | 16 ++- .../ServiceResourceDetectorTest.java | 119 +++++++++--------- 6 files changed, 91 insertions(+), 82 deletions(-) create mode 100644 sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/EntityExperimentConstants.java delete mode 100644 sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EntityExperimentConstants.java diff --git a/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/EntityExperimentConstants.java b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/EntityExperimentConstants.java new file mode 100644 index 00000000000..4083fb5409d --- /dev/null +++ b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/EntityExperimentConstants.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.spi.internal; + +/** + * Constants for experimental entity SDK features. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class EntityExperimentConstants { + + /** The configuration key for enabling experimental entity support in resource detectors. */ + public static final String EXPERIMENTAL_ENTITIES_ENABLED = "otel.experimental.entities.enabled"; + + private EntityExperimentConstants() {} +} diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EntityExperimentConstants.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EntityExperimentConstants.java deleted file mode 100644 index 8bef4950031..00000000000 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EntityExperimentConstants.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.autoconfigure; - -/** Constants for experimental entity SDK features. */ -final class EntityExperimentConstants { - - static final String EXPERIMENTAL_ENTITIES_ENABLED = "otel.experimental.entities.enabled"; - - private EntityExperimentConstants() {} -} diff --git a/sdk-extensions/declarative-config/build.gradle.kts b/sdk-extensions/declarative-config/build.gradle.kts index b4b6b6ad43c..dc6db434c18 100644 --- a/sdk-extensions/declarative-config/build.gradle.kts +++ b/sdk-extensions/declarative-config/build.gradle.kts @@ -172,9 +172,6 @@ val copyResourceConfiguration by tasks.registering(Copy::class) { from( project(":sdk-extensions:autoconfigure").file( "src/main/java/io/opentelemetry/sdk/autoconfigure/EnvironmentResource.java" - ), - project(":sdk-extensions:autoconfigure").file( - "src/main/java/io/opentelemetry/sdk/autoconfigure/EntityExperimentConstants.java" ) ) into(generatedResourceConfigDir.map { it.dir("io/opentelemetry/sdk/autoconfigure/declarativeconfig") }) 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 2e90e29a815..0496a687dc9 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 @@ -11,6 +11,7 @@ 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.autoconfigure.spi.internal.EntityExperimentConstants; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.ResourceBuilder; import io.opentelemetry.sdk.resources.internal.Entity; 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 ef530bb71ef..69b3ff4eb0e 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, rawAttributes={" - + "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/ServiceResourceDetectorTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ServiceResourceDetectorTest.java index 096a76c997b..cb8ce64e184 100644 --- 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 @@ -18,7 +18,6 @@ import java.util.Objects; import java.util.UUID; import org.junit.jupiter.api.Test; -import org.junitpioneer.jupiter.ClearSystemProperty; class ServiceResourceDetectorTest { @@ -31,26 +30,30 @@ void getTypeAndName() { } @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(); - }); + try { + 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(); + }); + } finally { + System.clearProperty("otel.service.name"); + } } @Test void create_NoSystemProperty() { + System.clearProperty("otel.service.name"); assertThat(new ServiceResourceDetector().create(DeclarativeConfigProperties.empty())) .satisfies( resource -> { @@ -66,53 +69,57 @@ void create_NoSystemProperty() { } @Test - @ClearSystemProperty(key = "otel.service.name") - @ClearSystemProperty(key = EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED) void create_EntitiesEnabled() { System.setProperty("otel.service.name", "my-service"); - System.setProperty(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, "true"); - - Resource resource = new ServiceResourceDetector().create(DeclarativeConfigProperties.empty()); - - Collection entities = EntityUtil.getEntities(resource); - assertThat(entities).hasSize(2); - - assertThat(entities) - .anyMatch( - e -> - e.getType().equals("service") - && e.getSchemaUrl().equals("https://opentelemetry.io/schemas/1.40.0") - && e.getId() - .equals( - Attributes.of(AttributeKey.stringKey("service.name"), "my-service"))); - - assertThat(entities) - .anyMatch( - e -> - e.getType().equals("service.instance") - && e.getSchemaUrl().equals("https://opentelemetry.io/schemas/1.40.0") - && e.getId().get(AttributeKey.stringKey("service.instance.id")) != null); - - // Flat attributes should also be present - Attributes attributes = resource.getAttributes(); - assertThat(attributes.get(AttributeKey.stringKey("service.name"))).isEqualTo("my-service"); - assertThat(attributes.get(AttributeKey.stringKey("service.instance.id"))).isNotNull(); + System.setProperty("otel.experimental.entities.enabled", "true"); + try { + Resource resource = new ServiceResourceDetector().create(DeclarativeConfigProperties.empty()); + + Collection entities = EntityUtil.getEntities(resource); + assertThat(entities).hasSize(2); + + assertThat(entities) + .anyMatch( + e -> + e.getType().equals("service") + && e.getSchemaUrl().equals("https://opentelemetry.io/schemas/1.40.0") + && e.getId() + .equals( + Attributes.of(AttributeKey.stringKey("service.name"), "my-service"))); + + assertThat(entities) + .anyMatch( + e -> + e.getType().equals("service.instance") + && e.getSchemaUrl().equals("https://opentelemetry.io/schemas/1.40.0") + && e.getId().get(AttributeKey.stringKey("service.instance.id")) != null); + + // Flat attributes should also be present + Attributes attributes = resource.getAttributes(); + assertThat(attributes.get(AttributeKey.stringKey("service.name"))).isEqualTo("my-service"); + assertThat(attributes.get(AttributeKey.stringKey("service.instance.id"))).isNotNull(); + } finally { + System.clearProperty("otel.service.name"); + System.clearProperty("otel.experimental.entities.enabled"); + } } @Test - @ClearSystemProperty(key = "otel.service.name") - @ClearSystemProperty(key = EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED) void create_EntitiesDisabled() { System.setProperty("otel.service.name", "my-service"); - System.setProperty(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, "false"); - - Resource resource = new ServiceResourceDetector().create(DeclarativeConfigProperties.empty()); - - Collection entities = EntityUtil.getEntities(resource); - assertThat(entities).isEmpty(); - - Attributes attributes = resource.getAttributes(); - assertThat(attributes.get(AttributeKey.stringKey("service.name"))).isEqualTo("my-service"); - assertThat(attributes.get(AttributeKey.stringKey("service.instance.id"))).isNotNull(); + System.setProperty("otel.experimental.entities.enabled", "false"); + try { + Resource resource = new ServiceResourceDetector().create(DeclarativeConfigProperties.empty()); + + Collection entities = EntityUtil.getEntities(resource); + assertThat(entities).isEmpty(); + + Attributes attributes = resource.getAttributes(); + assertThat(attributes.get(AttributeKey.stringKey("service.name"))).isEqualTo("my-service"); + assertThat(attributes.get(AttributeKey.stringKey("service.instance.id"))).isNotNull(); + } finally { + System.clearProperty("otel.service.name"); + System.clearProperty("otel.experimental.entities.enabled"); + } } } From 0dcab3ea4f2034aaa3dc92cff9b52c4870e777e0 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:54:29 -0500 Subject: [PATCH 18/22] Feedback for entities impl --- .../internal/otlp/EntityRefMarshaler.java | 2 +- .../internal/otlp/EntityRefMarshalerTest.java | 5 +- .../internal/otlp/ResourceMarshalerTest.java | 3 +- .../autoconfigure/EnvironmentResource.java | 262 ++++++++++++- .../ResourceConfigurationTest.java | 76 ++++ .../EntitiesEnvResourceDetector.java | 44 +++ .../declarativeconfig/ResourceFactory.java | 7 +- .../ServiceResourceDetector.java | 28 +- ...toconfigure.spi.internal.ComponentProvider | 1 + .../EntitiesEnvResourceDetectorTest.java | 73 ++++ sdk-extensions/incubator/build.gradle.kts | 2 - .../resources/EnvResourceProvider.java | 356 ------------------ ...try.sdk.autoconfigure.spi.ResourceProvider | 1 - ...toconfigure.spi.internal.ComponentProvider | 1 - .../resources/EnvResourceProviderTest.java | 133 ------- .../common/internal/SemConvAttributes.java | 16 + .../internal/AttributeCheckUtil.java | 4 +- .../sdk/resources/internal/Entity.java | 6 +- .../sdk/resources/internal/EntityBuilder.java | 8 - .../sdk/resources/internal/EntityUtil.java | 50 +-- .../internal/RawAttributeMergeResult.java | 2 +- .../resources/internal/SdkEntityBuilder.java | 14 +- .../internal/SemConvAttributesTest.java | 13 + .../sdk/resources/ResourceTest.java | 6 +- .../resources/internal/EntityUtilTest.java | 319 +++++++--------- 25 files changed, 670 insertions(+), 762 deletions(-) create mode 100644 sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetector.java create mode 100644 sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetectorTest.java delete mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProvider.java delete mode 100644 sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider delete mode 100644 sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java 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 index b464c810a59..d11ac55fa16 100644 --- 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 @@ -37,7 +37,7 @@ protected void writeTo(Serializer output) throws IOException { output.writeRepeatedString(EntityRef.DESCRIPTION_KEYS, descriptionKeysUtf8); } - /** Consttructs an entity reference marshaler from a full entity. */ + /** Constructs an entity reference marshaler from a full entity. */ static EntityRefMarshaler createForEntity(Entity e) { byte[] schemaUrlUtf8 = null; if (!StringUtils.isNullOrEmpty(e.getSchemaUrl())) { 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 index f2f7af4fc08..fcf9907dfb1 100644 --- 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 @@ -24,10 +24,9 @@ class EntityRefMarshalerTest { @Test void toEntityRefs() { Entity e = - Entity.builder("test") + Entity.builder("test", Attributes.builder().put("id.key", "id.value").build()) .setSchemaUrl("test-url") .setDescription(Attributes.builder().put("desc.key", "desc.value").build()) - .setId(Attributes.builder().put("id.key", "id.value").build()) .build(); EntityRef proto = parse(EntityRef.getDefaultInstance(), EntityRefMarshaler.createForEntity(e)); assertThat(proto.getType()).isEqualTo("test"); @@ -48,7 +47,7 @@ private static T parse(T prototype, Marshaler marshaler) { // Our marshaler should produce the exact same length of serialized output (for example, field // default values are not outputted), so we check that here. The output itself may have slightly // different ordering, mostly due to the way we don't output oneof values in field order all the - // tieme. If the lengths are equal and the resulting protos are equal, the marshaling is + // 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); 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 index f75aed04781..e55690adb37 100644 --- 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 @@ -28,8 +28,7 @@ class ResourceMarshalerTest { @Test void marshalResourceWithEntities() { Entity entity = - Entity.builder("process") - .setId(Attributes.of(stringKey("process.pid"), "1234")) + Entity.builder("process", Attributes.of(stringKey("process.pid"), "1234")) .setDescription(Attributes.of(stringKey("process.executable.name"), "java")) .setSchemaUrl("http://process.schema") .build(); 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..874a65beb55 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 @@ -9,9 +9,19 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.EntityExperimentConstants; 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. @@ -27,6 +37,7 @@ final class EnvironmentResource { // Visible for testing static final String ATTRIBUTE_PROPERTY = "otel.resource.attributes"; static final String SERVICE_NAME_PROPERTY = "otel.service.name"; + static final String ENTITIES_PROPERTY = "otel.entities"; /** * Create a {@link Resource} from the environment. The resource contains attributes parsed from @@ -38,9 +49,20 @@ final class EnvironmentResource { */ @SuppressWarnings("JdkObsolete") // Recommended alternative was introduced in java 10 static Resource createEnvironmentResource(ConfigProperties config) { - AttributesBuilder resourceAttributes = Attributes.builder(); + ResourceBuilder resourceBuilder = Resource.builder(); + + boolean entitiesEnabled = + config.getBoolean(EntityExperimentConstants.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); + } + } + for (Map.Entry entry : config.getMap(ATTRIBUTE_PROPERTY).entrySet()) { - resourceAttributes.put( + 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 @@ -49,10 +71,10 @@ static Resource createEnvironmentResource(ConfigProperties config) { } String serviceName = config.getString(SERVICE_NAME_PROPERTY); if (serviceName != null) { - resourceAttributes.put(SERVICE_NAME, serviceName); + resourceBuilder.put(SERVICE_NAME, serviceName); } - return Resource.create(resourceAttributes.build()); + return resourceBuilder.build(); } /** @@ -102,5 +124,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/test/java/io/opentelemetry/sdk/autoconfigure/ResourceConfigurationTest.java b/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceConfigurationTest.java index d8e97e382ca..d790ca3a97f 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,75 @@ 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.createEnvironmentResource(configProperties); + + 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/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..979d0654442 --- /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.spi.internal.EntityExperimentConstants.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.createEnvironmentResource( + 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 f1d1783aa03..ed62c4afd79 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 @@ -8,7 +8,6 @@ import static io.opentelemetry.sdk.autoconfigure.declarativeconfig.EnvironmentResource.ATTRIBUTE_PROPERTY; import static io.opentelemetry.sdk.autoconfigure.declarativeconfig.EnvironmentResource.createEnvironmentResource; -import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.AttributeNameValueModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.ExperimentalResourceDetectionModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.ExperimentalResourceDetectorModel; @@ -52,12 +51,12 @@ 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); } String attributeList = model.getAttributesList(); 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 0496a687dc9..469f286c263 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,13 +5,13 @@ 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.autoconfigure.spi.internal.EntityExperimentConstants; +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; @@ -21,14 +21,6 @@ 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"); - - private static final String SCHEMA_URL = "https://opentelemetry.io/schemas/1.40.0"; - private static final String SERVICE_TYPE = "service"; - private static final String SERVICE_INSTANCE_TYPE = "service.instance"; - // multiple calls to this resource provider should return the same value private static final String RANDOM_SERVICE_INSTANCE_ID = UUID.randomUUID().toString(); @@ -55,23 +47,25 @@ public Resource create(DeclarativeConfigProperties config) { if (entitiesEnabled) { if (serviceName != null) { Entity serviceEntity = - Entity.builder(SERVICE_TYPE) - .setId(Attributes.of(SERVICE_NAME, serviceName)) - .setSchemaUrl(SCHEMA_URL) + Entity.builder( + SemConvAttributes.SERVICE_TYPE, + Attributes.of(SemConvAttributes.SERVICE_NAME, serviceName)) + .setSchemaUrl(SemConvAttributes.SCHEMA_URL_V1_40_0) .build(); EntityUtil.addEntity(builder, serviceEntity); } Entity serviceInstanceEntity = - Entity.builder(SERVICE_INSTANCE_TYPE) - .setId(Attributes.of(SERVICE_INSTANCE_ID, RANDOM_SERVICE_INSTANCE_ID)) - .setSchemaUrl(SCHEMA_URL) + 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); } else { if (serviceName != null) { - builder.put(SERVICE_NAME, serviceName); + builder.put(SemConvAttributes.SERVICE_NAME, serviceName); } - builder.put(SERVICE_INSTANCE_ID, RANDOM_SERVICE_INSTANCE_ID); + builder.put(SemConvAttributes.SERVICE_INSTANCE_ID, RANDOM_SERVICE_INSTANCE_ID); } return builder.build(); 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/EntitiesEnvResourceDetectorTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetectorTest.java new file mode 100644 index 00000000000..eba1cd56ef9 --- /dev/null +++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetectorTest.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.declarativeconfig; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.common.ComponentLoader; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.ExperimentalResourceDetectionModel; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.ExperimentalResourceDetectorModel; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.ResourceModel; +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.Map; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class EntitiesEnvResourceDetectorTest { + + private static final DeclarativeConfigContext context = + new DeclarativeConfigContext( + ComponentLoader.forClassLoader(SpanProcessorFactoryTest.class.getClassLoader())); + + @ParameterizedTest + @MethodSource("createArgs") + void create(Map systemProperties, Collection expectedEntities) { + systemProperties.forEach(System::setProperty); + try { + ResourceModel resourceModel = + new ResourceModel() + .withDetectionDevelopment( + new ExperimentalResourceDetectionModel() + .withDetectors( + Collections.singletonList( + new ExperimentalResourceDetectorModel() + .withAdditionalProperty("env", null)))); + Resource resource = ResourceFactory.getInstance().create(resourceModel, context); + Collection entities = EntityUtil.getEntities(resource); + assertThat(entities).hasSize(expectedEntities.size()); + assertThat(entities).containsAll(expectedEntities); + } finally { + systemProperties.forEach((key, unused) -> System.clearProperty(key)); + } + } + + static Stream createArgs() { + return Stream.of( + Arguments.argumentSet( + "happy path", + ImmutableMap.of( + "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( + "empty", ImmutableMap.of("otel.entities", ""), Collections.emptyList())); + } +} diff --git a/sdk-extensions/incubator/build.gradle.kts b/sdk-extensions/incubator/build.gradle.kts index 1ede46662f2..af02be590af 100644 --- a/sdk-extensions/incubator/build.gradle.kts +++ b/sdk-extensions/incubator/build.gradle.kts @@ -25,8 +25,6 @@ dependencies { compileOnly(project(":sdk-extensions:autoconfigure-spi")) testImplementation(project(":sdk:testing")) - testImplementation(project(":api:incubator")) - testImplementation(project(":sdk-extensions:autoconfigure-spi")) testImplementation(project(":sdk-extensions:autoconfigure")) testImplementation("edu.berkeley.cs.jqf:jqf-fuzz") testImplementation("com.google.guava:guava-testlib") diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProvider.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProvider.java deleted file mode 100644 index 9a4f9174f45..00000000000 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProvider.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; -import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; -import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; -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.Collections; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.annotation.Nullable; - -/** - * {@link ResourceProvider} and {@link ComponentProvider} for detecting resources via the {@code - * OTEL_ENTITIES} environment variable. - */ -public final class EnvResourceProvider implements ResourceProvider, ComponentProvider { - - private static final String ENTITIES_PROPERTY = "otel.entities"; - - @Override - public Resource createResource(ConfigProperties config) { - return createResourceInternal(config); - } - - @Override - public int order() { - return 0; - } - - @Override - public Class getType() { - return Resource.class; - } - - @Override - public String getName() { - return "env"; - } - - @Override - public Resource create(DeclarativeConfigProperties config) { - ConfigProperties properties = - DefaultConfigProperties.create(Collections.emptyMap(), config.getComponentLoader()); - return createResourceInternal(properties); - } - - private static Resource createResourceInternal(ConfigProperties config) { - String entitiesStr = config.getString(ENTITIES_PROPERTY); - if (entitiesStr == null || entitiesStr.isEmpty()) { - return Resource.empty(); - } - - ResourceBuilder builder = Resource.builder(); - List parsedEntities = new EntityParser(entitiesStr).parse(); - for (Entity entity : parsedEntities) { - EntityUtil.addEntity(builder, entity); - } - return builder.build(); - } - - /** - * Decodes percent-encoded characters in resource attribute values per W3C Baggage spec. - * - *

Unlike {@link java.net.URLDecoder}, this method: - * - *

    - *
  • Preserves '+' as a literal plus sign (URLDecoder decodes '+' as space) - *
  • Preserves invalid percent sequences as literals (e.g., "%2G", "%", "%2") - *
  • Supports multi-byte UTF-8 sequences (e.g., "%C3%A9" decodes to "é") - *
- * - * @param value the percent-encoded string - * @return the decoded string - */ - private static String decodeResourceAttributes(String value) { - // no percent signs means nothing to decode - if (value.indexOf('%') < 0) { - return value; - } - - int n = value.length(); - // Use byte array to properly handle multi-byte UTF-8 sequences - byte[] bytes = new byte[n]; - int pos = 0; - - for (int i = 0; i < n; i++) { - char c = value.charAt(i); - // Check for percent-encoded sequence i.e. '%' followed by two hex digits - if (c == '%' && i + 2 < n) { - int d1 = Character.digit(value.charAt(i + 1), 16); - int d2 = Character.digit(value.charAt(i + 2), 16); - // Valid hex digits return 0-15, invalid returns -1 - if (d1 != -1 && d2 != -1) { - // Combine two hex digits into a single byte (e.g., "2F" becomes 0x2F) - bytes[pos++] = (byte) ((d1 << 4) + d2); - // Skip the two hex digits (loop will also do i++) - i += 2; - continue; - } - } - // Keep '+' as '+' (unlike URLDecoder) and preserve invalid percent sequences - // which will be - // treated as literals - bytes[pos++] = (byte) c; - } - 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).setId(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); - } - } -} diff --git a/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider b/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider index a39b8f1eecf..189af738dcf 100644 --- a/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider +++ b/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider @@ -1,2 +1 @@ io.opentelemetry.sdk.extension.incubator.resources.ServiceInstanceIdResourceProvider -io.opentelemetry.sdk.extension.incubator.resources.EnvResourceProvider diff --git a/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider b/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider deleted file mode 100644 index 7c5a45f4dae..00000000000 --- a/sdk-extensions/incubator/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider +++ /dev/null @@ -1 +0,0 @@ -io.opentelemetry.sdk.extension.incubator.resources.EnvResourceProvider diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java deleted file mode 100644 index dabcb73d9d2..00000000000 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/resources/EnvResourceProviderTest.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.resources; - -import static io.opentelemetry.api.common.AttributeKey.stringKey; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; -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.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import org.junit.jupiter.api.Test; - -class EnvResourceProviderTest { - - @Test - void getTypeAndName() { - EnvResourceProvider provider = new EnvResourceProvider(); - assertThat(provider.getType()).isEqualTo(Resource.class); - assertThat(provider.getName()).isEqualTo("env"); - assertThat(provider.order()).isEqualTo(0); - } - - @Test - void createResource_EmptyOrNull() { - EnvResourceProvider provider = new EnvResourceProvider(); - - Resource emptyResource = - provider.createResource(DefaultConfigProperties.createFromMap(Collections.emptyMap())); - assertThat(EntityUtil.getEntities(emptyResource)).isEmpty(); - - Resource blankResource = - provider.createResource( - DefaultConfigProperties.createFromMap(Collections.singletonMap("otel.entities", ""))); - assertThat(EntityUtil.getEntities(blankResource)).isEmpty(); - } - - @Test - void createResource_WithEntities() { - Map props = new HashMap<>(); - props.put( - "otel.entities", - "process{process.pid=1234}[process.executable.name=java]@http://schema;host{host.id=myhost}"); - - EnvResourceProvider provider = new EnvResourceProvider(); - Resource resource = provider.createResource(DefaultConfigProperties.createFromMap(props)); - - Collection entities = EntityUtil.getEntities(resource); - assertThat(entities).hasSize(2); - - assertThat(entities) - .anyMatch( - e -> - e.getType().equals("process") - && e.getSchemaUrl().equals("http://schema") - && e.getId().equals(Attributes.of(stringKey("process.pid"), "1234")) - && e.getDescription() - .equals(Attributes.of(stringKey("process.executable.name"), "java"))); - - assertThat(entities) - .anyMatch( - e -> - e.getType().equals("host") - && e.getSchemaUrl() == null - && e.getId().equals(Attributes.of(stringKey("host.id"), "myhost"))); - } - - @Test - void createResource_PercentDecoding() { - Map props = new HashMap<>(); - props.put( - "otel.entities", - "service{service.name=my+app,space=hello%20world,utf8=%C3%A9,invalid=%2G,incomplete=%2,end=%}"); - - EnvResourceProvider provider = new EnvResourceProvider(); - Resource resource = provider.createResource(DefaultConfigProperties.createFromMap(props)); - - Collection entities = EntityUtil.getEntities(resource); - assertThat(entities).hasSize(1); - - Entity entity = entities.iterator().next(); - assertThat(entity.getId()) - .containsEntry(stringKey("service.name"), "my+app") - .containsEntry(stringKey("space"), "hello world") - .containsEntry(stringKey("utf8"), "é") - .containsEntry(stringKey("invalid"), "%2G") - .containsEntry(stringKey("incomplete"), "%2") - .containsEntry(stringKey("end"), "%"); - } - - @Test - void createResource_Malformed() { - Map props = new HashMap<>(); - props.put( - "otel.entities", - "{empty.type=val};process{};process{=val};process{key;=val};host{host.id=valid}"); - - EnvResourceProvider provider = new EnvResourceProvider(); - Resource resource = provider.createResource(DefaultConfigProperties.createFromMap(props)); - - Collection entities = EntityUtil.getEntities(resource); - // Only the last valid host entity should be parsed - assertThat(entities).hasSize(1); - assertThat(entities.iterator().next().getType()).isEqualTo("host"); - } - - @Test - void create_ComponentProvider() { - System.setProperty("otel.entities", "service{service.name=my-service}"); - try { - EnvResourceProvider provider = new EnvResourceProvider(); - Resource resource = provider.create(DeclarativeConfigProperties.empty()); - Collection entities = EntityUtil.getEntities(resource); - assertThat(entities).hasSize(1); - assertThat(entities) - .anyMatch( - e -> - e.getType().equals("service") - && e.getId().equals(Attributes.of(stringKey("service.name"), "my-service"))); - } finally { - System.clearProperty("otel.entities"); - } - } -} 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/internal/AttributeCheckUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/AttributeCheckUtil.java index 966c5b58277..49eec3563c9 100644 --- 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 @@ -12,10 +12,12 @@ import java.util.Objects; /** - * Helpers to check resource attributes. + * 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() {} 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 index 2e647e4fc0e..694ebeac4bb 100644 --- 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 @@ -25,6 +25,7 @@ *

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 { /** @@ -66,8 +67,9 @@ public interface Entity { * 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) { - return new SdkEntityBuilder(entityType); + 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 index 52a910aec9d..55d90f01f5c 100644 --- 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 @@ -31,14 +31,6 @@ public interface EntityBuilder { */ EntityBuilder setDescription(Attributes description); - /** - * Modify the identifying attributes of this Entity. - * - * @param id The identifying attributes. - * @return this - */ - EntityBuilder setId(Attributes id); - /** Create the {@link Entity} from this. */ Entity build(); } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java index e6f16934031..a8e89b7c431 100644 --- 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 @@ -14,11 +14,10 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -59,37 +58,28 @@ static Resource createResourceRaw( Method method = Resource.class.getDeclaredMethod( "create", Attributes.class, String.class, Collection.class); - if (method != null) { - method.setAccessible(true); - Object result = method.invoke(null, attributes, schemaUrl, entities); - if (result instanceof Resource) { - return (Resource) result; - } - } + method.setAccessible(true); + Object result = method.invoke(null, attributes, schemaUrl, entities); + return (Resource) result; } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", e); + throw new IllegalStateException("Error calling create on Resource", e); } - // Fall back to non-entity behavior? - logger.log(Level.WARNING, "Attempting to use entities with unsupported resource"); - return Resource.empty(); } /** Appends a new entity on to the end of the list of entities. */ public static ResourceBuilder addEntity(ResourceBuilder rb, Entity e) { try { Method method = ResourceBuilder.class.getDeclaredMethod("addEntity", Entity.class); - if (method != null) { - method.setAccessible(true); - method.invoke(rb, e); - } + method.setAccessible(true); + method.invoke(rb, e); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { - logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", ex); + throw new IllegalStateException("Error calling addEntity on ResourceBuilder", ex); } return rb; } /** - * Returns a collectoion of associated entities. + * Returns a collection of associated entities. * * @return a collection of entities. */ @@ -97,14 +87,11 @@ public static ResourceBuilder addEntity(ResourceBuilder rb, Entity e) { public static Collection getEntities(Resource r) { try { Method method = Resource.class.getDeclaredMethod("getEntities"); - if (method != null) { - method.setAccessible(true); - return (Collection) method.invoke(r); - } + method.setAccessible(true); + return (Collection) method.invoke(r); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", e); + throw new IllegalStateException("Error calling getEntities on Resource", e); } - return Collections.emptyList(); } /** @@ -115,14 +102,11 @@ public static Collection getEntities(Resource r) { public static Attributes getRawAttributes(Resource r) { try { Method method = Resource.class.getDeclaredMethod("getRawAttributes"); - if (method != null) { - method.setAccessible(true); - return (Attributes) method.invoke(r); - } + method.setAccessible(true); + return (Attributes) method.invoke(r); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", e); + throw new IllegalStateException("Error calling getRawAttributes on Resource", e); } - return Attributes.empty(); } /** Returns true if any entity in the collection has the attribute key, in id or description. */ @@ -179,7 +163,7 @@ static String mergeResourceSchemaUrl( * MUST NOT be reported on OTLP resource. */ @SuppressWarnings("unchecked") - static final RawAttributeMergeResult mergeRawAttributes( + static RawAttributeMergeResult mergeRawAttributes( Attributes base, Attributes additional, Collection entities) { AttributesBuilder result = base.toBuilder(); // We know attribute conflicts were handled perviously on the resource, so @@ -188,7 +172,7 @@ static final RawAttributeMergeResult mergeRawAttributes( result.removeIf(key -> hasAttributeKey(entities, key)); // For every "raw" attribute on the other resource, we merge into the // resource, but check for entity conflicts from previous entities. - ArrayList conflicts = new ArrayList<>(); + List conflicts = new ArrayList<>(); if (!additional.isEmpty()) { additional.forEach( (key, value) -> { diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/RawAttributeMergeResult.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/RawAttributeMergeResult.java index f1cc081b035..67daaad1ba2 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/RawAttributeMergeResult.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/RawAttributeMergeResult.java @@ -26,7 +26,7 @@ abstract class RawAttributeMergeResult { */ abstract Collection getConflicts(); - static final RawAttributeMergeResult create(Attributes attributes, Collection conflicts) { + static RawAttributeMergeResult create(Attributes attributes, Collection conflicts) { return new AutoValue_RawAttributeMergeResult(attributes, conflicts); } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntityBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntityBuilder.java index f4f62024f12..2773130b88e 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntityBuilder.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntityBuilder.java @@ -17,15 +17,16 @@ */ final class SdkEntityBuilder implements EntityBuilder { private final String entityType; + private final Attributes id; private Attributes description; - private Attributes id; @Nullable private String schemaUrl; - SdkEntityBuilder(String entityType) { + SdkEntityBuilder(String entityType, Attributes id) { AttributeCheckUtil.isValid(entityType); + AttributeCheckUtil.checkAttributes(id); this.entityType = entityType; + this.id = id; this.description = Attributes.empty(); - this.id = Attributes.empty(); } SdkEntityBuilder(Entity seed) { @@ -48,13 +49,6 @@ public EntityBuilder setDescription(Attributes description) { return this; } - @Override - public EntityBuilder setId(Attributes id) { - AttributeCheckUtil.checkAttributes(id); - this.id = id; - return this; - } - @Override public Entity build() { return SdkEntity.create(entityType, id, description, schemaUrl); diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/SemConvAttributesTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/SemConvAttributesTest.java index 47632d16d15..25a30e29be3 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/SemConvAttributesTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/SemConvAttributesTest.java @@ -9,7 +9,9 @@ import io.opentelemetry.semconv.ErrorAttributes; import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.SchemaUrls; import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.ServiceAttributes; import io.opentelemetry.semconv.incubating.OtelIncubatingAttributes; import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; import org.junit.jupiter.api.Test; @@ -18,6 +20,17 @@ class SemConvAttributesTest { @Test void testAttributeKeys() { + assertThat(SemConvAttributes.SCHEMA_URL_V1_40_0).isEqualTo(SchemaUrls.V1_40_0); + + // TODO(jack-berg): assert against generated constants once we start generating for entities + // types + assertThat(SemConvAttributes.SERVICE_TYPE).isEqualTo("service"); + assertThat(SemConvAttributes.SERVICE_INSTANCE_TYPE).isEqualTo("service.instance"); + + assertThat(SemConvAttributes.SERVICE_NAME).isEqualTo(ServiceAttributes.SERVICE_NAME); + assertThat(SemConvAttributes.SERVICE_INSTANCE_ID) + .isEqualTo(ServiceAttributes.SERVICE_INSTANCE_ID); + assertThat(SemConvAttributes.OTEL_COMPONENT_NAME) .isEqualTo(OtelIncubatingAttributes.OTEL_COMPONENT_NAME); assertThat(SemConvAttributes.OTEL_COMPONENT_TYPE) diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java index f171c5d9ff7..1a2d6dee6c9 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java @@ -242,17 +242,15 @@ void testMergeResources_entities_separate_types_and_schema() { Resource resource1 = Resource.builder() .addEntity( - Entity.builder("a") + Entity.builder("a", Attributes.builder().put("a.id", "a").build()) .setSchemaUrl("one") - .setId(Attributes.builder().put("a.id", "a").build()) .build()) .build(); Resource resource2 = Resource.builder() .addEntity( - Entity.builder("b") + Entity.builder("b", Attributes.builder().put("b.id", "b").build()) .setSchemaUrl("two") - .setId(Attributes.builder().put("b.id", "b").build()) .build()) .build(); Resource merged = resource1.merge(resource2); diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/EntityUtilTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/EntityUtilTest.java index 332dd198401..86d016f911f 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/EntityUtilTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/EntityUtilTest.java @@ -13,193 +13,158 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.stream.Stream; +import javax.annotation.Nullable; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; /** Unit tests for {@link EntityUtil}. */ class EntityUtilTest { - @Test - void testMerge_entities_same_types_and_id() { - Collection base = - Arrays.asList( - Entity.builder("a") - .setSchemaUrl("one") - .setId(Attributes.builder().put("a.id", "a").build()) - .setDescription(Attributes.builder().put("a.desc1", "a").build()) - .build()); - Collection added = - Arrays.asList( - Entity.builder("a") - .setSchemaUrl("one") - .setId(Attributes.builder().put("a.id", "a").build()) - .setDescription(Attributes.builder().put("a.desc2", "b").build()) - .build()); - Collection merged = EntityUtil.mergeEntities(base, added); - assertThat(merged).hasSize(1); - assertThat(merged) - .anySatisfy( - entity -> { - assertThat(entity.getType()).isEqualTo("a"); - assertThat(entity.getSchemaUrl()).isEqualTo("one"); - assertThat(entity.getId()).containsEntry("a.id", "a"); - assertThat(entity.getDescription()) - .containsEntry("a.desc1", "a") - .containsEntry("a.desc2", "b"); - }); - } - - @Test - void testMerge_entities_same_types_and_id_different_schema() { - Collection base = - Arrays.asList( - Entity.builder("a") - .setSchemaUrl("one") - .setId(Attributes.builder().put("a.id", "a").build()) - .setDescription(Attributes.builder().put("a.desc1", "a").build()) - .build()); - Collection added = - Arrays.asList( - Entity.builder("a") - .setSchemaUrl("two") - .setId(Attributes.builder().put("a.id", "a").build()) - .setDescription(Attributes.builder().put("a.desc2", "b").build()) - .build()); - Collection merged = EntityUtil.mergeEntities(base, added); - assertThat(merged).hasSize(1); - assertThat(merged) - .anySatisfy( - entity -> { - assertThat(entity.getType()).isEqualTo("a"); - assertThat(entity.getSchemaUrl()).isEqualTo("one"); - assertThat(entity.getId()).containsEntry("a.id", "a"); - assertThat(entity.getDescription()) - .containsEntry("a.desc1", "a") - // Don't merge between versions. - .doesNotContainKey("a.desc2"); - }); - } - @Test - void testMerge_entities_same_types_different_id() { - Collection base = - Arrays.asList( - Entity.builder("a") - .setSchemaUrl("one") - .setId(Attributes.builder().put("a.id", "a").build()) - .setDescription(Attributes.builder().put("a.desc1", "a").build()) - .build()); - Collection added = - Arrays.asList( - Entity.builder("a") - .setSchemaUrl("one") - .setId(Attributes.builder().put("a.id", "b").build()) - .setDescription(Attributes.builder().put("a.desc2", "b").build()) - .build()); - Collection merged = EntityUtil.mergeEntities(base, added); - assertThat(merged).hasSize(1); - assertThat(merged) - .satisfiesExactly( - entity -> { - assertThat(entity.getType()).isEqualTo("a"); - assertThat(entity.getSchemaUrl()).isEqualTo("one"); - assertThat(entity.getId()).containsEntry("a.id", "a"); - assertThat(entity.getDescription()) - .containsEntry("a.desc1", "a") - // Don't merge between different ids. - .doesNotContainKey("a.desc2"); - }); + @ParameterizedTest + @MethodSource("mergeEntitiesTestCases") + void mergeEntities( + Collection base, Collection added, Collection expected) { + assertThat(EntityUtil.mergeEntities(base, added)).containsExactlyInAnyOrderElementsOf(expected); } - @Test - void testMerge_entities_separate_types_and_schema() { - Collection base = - Arrays.asList( - Entity.builder("a") - .setSchemaUrl("one") - .setId(Attributes.builder().put("a.id", "a").build()) - .build()); - Collection added = - Arrays.asList( - Entity.builder("b") - .setSchemaUrl("two") - .setId(Attributes.builder().put("b.id", "b").build()) - .build()); - Collection merged = EntityUtil.mergeEntities(base, added); - // Make sure we keep both entities when no conflict. - assertThat(merged) - .satisfiesExactlyInAnyOrder( - a -> assertThat(a.getType()).isEqualTo("a"), - b -> assertThat(b.getType()).isEqualTo("b")); - } - - @Test - void testSchemaUrlMerge_no_entities_differentUrls() { - // If the we find conflicting schema URLs in resource we must drop schema url (set to null). - String result = EntityUtil.mergeResourceSchemaUrl(Collections.emptyList(), "one", "two"); - assertThat(result).isNull(); - } - - @Test - void testSchemaUrlMerge_no_entities_base_null() { - // If the our resource had no schema url it abides by, we use the incoming schema url. - String result = EntityUtil.mergeResourceSchemaUrl(Collections.emptyList(), null, "two"); - assertThat(result).isEqualTo("two"); + static Stream mergeEntitiesTestCases() { + return Stream.of( + Arguments.argumentSet( + "same type and id - descriptions are merged", + Collections.singletonList( + Entity.builder("a", Attributes.builder().put("a.id", "a").build()) + .setSchemaUrl("one") + .setDescription(Attributes.builder().put("a.desc1", "a").build()) + .build()), + Collections.singletonList( + Entity.builder("a", Attributes.builder().put("a.id", "a").build()) + .setSchemaUrl("one") + .setDescription(Attributes.builder().put("a.desc2", "b").build()) + .build()), + Collections.singletonList( + Entity.builder("a", Attributes.builder().put("a.id", "a").build()) + .setSchemaUrl("one") + .setDescription( + Attributes.builder().put("a.desc1", "a").put("a.desc2", "b").build()) + .build())), + Arguments.argumentSet( + "same type and id different schema - don't merge between schema versions", + Collections.singletonList( + Entity.builder("a", Attributes.builder().put("a.id", "a").build()) + .setSchemaUrl("one") + .setDescription(Attributes.builder().put("a.desc1", "a").build()) + .build()), + Collections.singletonList( + Entity.builder("a", Attributes.builder().put("a.id", "a").build()) + .setSchemaUrl("two") + .setDescription(Attributes.builder().put("a.desc2", "b").build()) + .build()), + Collections.singletonList( + Entity.builder("a", Attributes.builder().put("a.id", "a").build()) + .setSchemaUrl("one") + .setDescription(Attributes.builder().put("a.desc1", "a").build()) + .build())), + Arguments.argumentSet( + "same type different id - don't merge between different identities", + Collections.singletonList( + Entity.builder("a", Attributes.builder().put("a.id", "a").build()) + .setSchemaUrl("one") + .setDescription(Attributes.builder().put("a.desc1", "a").build()) + .build()), + Collections.singletonList( + Entity.builder("a", Attributes.builder().put("a.id", "b").build()) + .setSchemaUrl("one") + .setDescription(Attributes.builder().put("a.desc2", "b").build()) + .build()), + Collections.singletonList( + Entity.builder("a", Attributes.builder().put("a.id", "a").build()) + .setSchemaUrl("one") + .setDescription(Attributes.builder().put("a.desc1", "a").build()) + .build())), + Arguments.argumentSet( + "separate types and schema - both entities are kept", + Collections.singletonList( + Entity.builder("a", Attributes.builder().put("a.id", "a").build()) + .setSchemaUrl("one") + .build()), + Collections.singletonList( + Entity.builder("b", Attributes.builder().put("b.id", "b").build()) + .setSchemaUrl("two") + .build()), + Arrays.asList( + Entity.builder("a", Attributes.builder().put("a.id", "a").build()) + .setSchemaUrl("one") + .build(), + Entity.builder("b", Attributes.builder().put("b.id", "b").build()) + .setSchemaUrl("two") + .build()))); } - @Test - void testSchemaUrlMerge_no_entities_next_null() { - // If the new resource had no schema url it abides by, we preserve ours. - // NOTE: this is by specification, but seems problematic if conflicts in merge - // cause violation of SchemaURL. - String result = EntityUtil.mergeResourceSchemaUrl(Collections.emptyList(), "one", null); - assertThat(result).isEqualTo("one"); + @ParameterizedTest + @MethodSource("mergeResourceSchemaUrlTestCases") + void mergeResourceSchemaUrl( + Collection entities, + @Nullable String baseUrl, + @Nullable String nextUrl, + @Nullable String expected) { + assertThat(EntityUtil.mergeResourceSchemaUrl(entities, baseUrl, nextUrl)).isEqualTo(expected); } - @Test - void testSchemaUrlMerge_entities_same_url() { - // If the new resource had no schema url it abides by, we preserve ours. - // NOTE: this is by specification, but seems problematic if conflicts in merge - // cause violation of SchemaURL. - String result = - EntityUtil.mergeResourceSchemaUrl( - Arrays.asList( - Entity.builder("t") + static Stream mergeResourceSchemaUrlTestCases() { + return Stream.of( + Arguments.argumentSet( + "no entities conflicting urls - drop schema url", + Collections.emptyList(), + "one", + "two", + null), + Arguments.argumentSet( + "no entities base null - use incoming url", + Collections.emptyList(), + null, + "two", + "two"), + Arguments.argumentSet( + "no entities next null - preserve base url", + Collections.emptyList(), + "one", + null, + "one"), + Arguments.argumentSet( + "entities with same url", + Collections.singletonList( + Entity.builder("t", Attributes.builder().put("id", 1).build()) .setSchemaUrl("one") - .setId(Attributes.builder().put("id", 1).build()) .build()), "one", - null); - assertThat(result).isEqualTo("one"); - } - - @Test - void testSchemaUrlMerge_entities_different_url() { - // When entities have conflicting schema urls, we cannot fill out resource schema url, - // no matter what. - String result = - EntityUtil.mergeResourceSchemaUrl( + null, + "one"), + Arguments.argumentSet( + "entities with conflicting urls - cannot fill resource schema url", Arrays.asList( - Entity.builder("t") + Entity.builder("t", Attributes.builder().put("id", 1).build()) .setSchemaUrl("one") - .setId(Attributes.builder().put("id", 1).build()) .build(), - Entity.builder("t2") + Entity.builder("t2", Attributes.builder().put("id2", 1).build()) .setSchemaUrl("two") - .setId(Attributes.builder().put("id2", 1).build()) .build()), "one", - "one"); - assertThat(result).isEqualTo(null); + "one", + null)); } - @Test - void testRawAttributeMerge_no_entities() { - // When no entities are present all attributes are merged. + @ParameterizedTest + @MethodSource("mergeRawAttributesTestCases") + void mergeRawAttributes(Collection entities, Collection expectedConflicts) { RawAttributeMergeResult result = EntityUtil.mergeRawAttributes( Attributes.builder().put("a", 1).put("b", 1).build(), Attributes.builder().put("b", 2).put("c", 2).build(), - Collections.emptyList()); - assertThat(result.getConflicts()).isEmpty(); + entities); + assertThat(result.getConflicts()).containsExactlyInAnyOrderElementsOf(expectedConflicts); assertThat(result.getAttributes()) .hasSize(3) .containsEntry("a", 1) @@ -207,30 +172,26 @@ void testRawAttributeMerge_no_entities() { .containsEntry("c", 2); } - @Test - void testRawAttributeMerge_entity_with_conflict() { - // When an entity conflicts with incoming raw attributes, we need to call out that conflict - // so resource merge logic can remove the entity from resource. - RawAttributeMergeResult result = - EntityUtil.mergeRawAttributes( - Attributes.builder().put("a", 1).put("b", 1).build(), - Attributes.builder().put("b", 2).put("c", 2).build(), - Arrays.asList( - Entity.builder("c").setId(Attributes.builder().put("c", 1).build()).build())); - assertThat(result.getConflicts()).satisfiesExactly(e -> assertThat(e.getType()).isEqualTo("c")); - assertThat(result.getAttributes()) - .hasSize(3) - .containsEntry("a", 1) - .containsEntry("b", 2) - .containsEntry("c", 2); + static Stream mergeRawAttributesTestCases() { + return Stream.of( + Arguments.argumentSet( + "no entities - all attributes merged", + Collections.emptyList(), + Collections.emptyList()), + Arguments.argumentSet( + "entity id attribute conflicts with incoming attribute", + Collections.singletonList( + Entity.builder("c", Attributes.builder().put("c", 1).build()).build()), + Collections.singletonList( + Entity.builder("c", Attributes.builder().put("c", 1).build()).build()))); } @Test - void testAddEntity_reflection() { + void addEntity_reflection() { Resource result = EntityUtil.addEntity( Resource.builder(), - Entity.builder("a").setId(Attributes.builder().put("a", 1).build()).build()) + Entity.builder("a", Attributes.builder().put("a", 1).build()).build()) .build(); assertThat(EntityUtil.getEntities(result)) .satisfiesExactlyInAnyOrder(e -> assertThat(e.getType()).isEqualTo("a")); From de5ea479e775edd539fd7e88d287ae0ff43fd4f9 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 14 Jul 2026 11:31:11 +0000 Subject: [PATCH 19/22] Update method name to match spec PR. --- .../java/io/opentelemetry/sdk/resources/Resource.java | 8 ++++---- .../opentelemetry/sdk/resources/ResourceBuilder.java | 2 +- .../sdk/resources/internal/EntityUtil.java | 11 ++++++----- 3 files changed, 11 insertions(+), 10 deletions(-) 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 70d01ea7c26..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 @@ -143,16 +143,16 @@ static Resource create( * * @return a map of attributes. */ - final Attributes getRawAttributes() { - AttributesBuilder rawAttributes = getAttributes().toBuilder(); - rawAttributes.removeIf( + 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 rawAttributes.build(); + return unassociatedAttributes.build(); } /** 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 5d54944390d..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 @@ -180,7 +180,7 @@ public ResourceBuilder putAll(Resource resource) { entities.addAll(resource.getEntities()); // Only pull "raw" attributes - we expect entities to carry some of the full // set. - attributesBuilder.putAll(resource.getRawAttributes()); + attributesBuilder.putAll(resource.getUnassociatedAttributes()); } return this; } 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 index a8e89b7c431..a370a62aa17 100644 --- 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 @@ -95,17 +95,18 @@ public static Collection getEntities(Resource r) { } /** - * Returns a map of attributes that describe the resource, not associated with entites. + * Returns a map of attributes that describe the resource, not associated with + * entities. * * @return a map of attributes. */ - public static Attributes getRawAttributes(Resource r) { + public static Attributes getUnassociatedAttributes(Resource r) { try { - Method method = Resource.class.getDeclaredMethod("getRawAttributes"); + Method method = Resource.class.getDeclaredMethod("getUnassociatedAttributes"); method.setAccessible(true); return (Attributes) method.invoke(r); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new IllegalStateException("Error calling getRawAttributes on Resource", e); + throw new IllegalStateException("Error calling getUnassociatedAttributes on Resource", e); } } @@ -270,7 +271,7 @@ public static Resource merge(Resource base, @Nullable Resource next) { // https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/entities/0264-resource-and-entities.md#entity-merging-and-resource Collection entities = EntityUtil.mergeEntities(getEntities(base), getEntities(next)); RawAttributeMergeResult attributeResult = - EntityUtil.mergeRawAttributes(getRawAttributes(base), getRawAttributes(next), entities); + EntityUtil.mergeRawAttributes(getUnassociatedAttributes(base), getUnassociatedAttributes(next), entities); // Remove entities that are conflicting with raw attributes, and therefore in an unknown state. entities.removeAll(attributeResult.getConflicts()); // Now figure out schema url for overall resource. From 82b4d1220d880d6503d0e65106299451deb59291 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 14 Jul 2026 11:39:51 +0000 Subject: [PATCH 20/22] Spotless Apply --- .../io/opentelemetry/sdk/resources/internal/EntityUtil.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index a370a62aa17..ecc6c0cc5bd 100644 --- 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 @@ -95,8 +95,7 @@ public static Collection getEntities(Resource r) { } /** - * Returns a map of attributes that describe the resource, not associated with - * entities. + * Returns a map of attributes that describe the resource, not associated with entities. * * @return a map of attributes. */ @@ -271,7 +270,8 @@ public static Resource merge(Resource base, @Nullable Resource next) { // https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/entities/0264-resource-and-entities.md#entity-merging-and-resource Collection entities = EntityUtil.mergeEntities(getEntities(base), getEntities(next)); RawAttributeMergeResult attributeResult = - EntityUtil.mergeRawAttributes(getUnassociatedAttributes(base), getUnassociatedAttributes(next), entities); + EntityUtil.mergeRawAttributes( + getUnassociatedAttributes(base), getUnassociatedAttributes(next), entities); // Remove entities that are conflicting with raw attributes, and therefore in an unknown state. entities.removeAll(attributeResult.getConflicts()); // Now figure out schema url for overall resource. From 74fd7ff3ce66ee8e9ddc6132ce15976347767407 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:17:59 -0500 Subject: [PATCH 21/22] declarative config model package change --- .../declarativeconfig/EntitiesEnvResourceDetectorTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetectorTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetectorTest.java index eba1cd56ef9..2e825900c60 100644 --- a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetectorTest.java +++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetectorTest.java @@ -11,9 +11,9 @@ import com.google.common.collect.ImmutableMap; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.common.ComponentLoader; -import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.ExperimentalResourceDetectionModel; -import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.ExperimentalResourceDetectorModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.ResourceModel; +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; From 54e31d8c4d524845c2056fa628f52a949c96ef48 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:01:11 -0500 Subject: [PATCH 22/22] Sketch out central entity erasure --- .../gradle/js2p/OtelDescriptionRule.java | 47 ++++++ .../gradle/js2p/OtelPropertyRule.java | 56 ++----- .../gradle/js2p/OtelRuleFactory.java | 6 + ...ntelemetry-sdk-extension-autoconfigure.txt | 8 +- .../internal/EntityExperimentConstants.java | 20 --- .../EnvironmentEntityResourceProvider.java | 22 +++ .../autoconfigure/EnvironmentResource.java | 65 +++++--- .../autoconfigure/ResourceConfiguration.java | 18 ++- ...try.sdk.autoconfigure.spi.ResourceProvider | 1 + .../ResourceConfigurationTest.java | 6 +- .../declarative-config/build.gradle.kts | 4 +- .../EntitiesEnvResourceDetector.java | 4 +- .../declarativeconfig/ResourceFactory.java | 12 +- .../ServiceResourceDetector.java | 39 ++--- .../model/MetricProducerModel.java | 10 ++ .../model/OpenCensusMetricProducerModel.java | 6 + .../ExperimentalResourceDetectionModel.java | 35 ++++- .../EntitiesEnvResourceDetectorTest.java | 73 --------- .../ResourceFactoryTest.java | 141 +++++++++++++++++- .../ServiceResourceDetectorTest.java | 125 ---------------- .../ServiceInstanceIdResourceProvider.java | 22 ++- ...ServiceInstanceIdResourceProviderTest.java | 4 +- 22 files changed, 390 insertions(+), 334 deletions(-) create mode 100644 buildSrc/src/main/java/io/opentelemetry/gradle/js2p/OtelDescriptionRule.java delete mode 100644 sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/EntityExperimentConstants.java create mode 100644 sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EnvironmentEntityResourceProvider.java delete mode 100644 sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetectorTest.java delete mode 100644 sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ServiceResourceDetectorTest.java 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/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/EntityExperimentConstants.java b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/EntityExperimentConstants.java deleted file mode 100644 index 4083fb5409d..00000000000 --- a/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/EntityExperimentConstants.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.autoconfigure.spi.internal; - -/** - * Constants for experimental entity SDK features. - * - *

This class is internal and is hence not for public use. Its APIs are unstable and can change - * at any time. - */ -public final class EntityExperimentConstants { - - /** The configuration key for enabling experimental entity support in resource detectors. */ - public static final String EXPERIMENTAL_ENTITIES_ENABLED = "otel.experimental.entities.enabled"; - - private EntityExperimentConstants() {} -} 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 874a65beb55..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,11 +5,10 @@ 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.autoconfigure.spi.internal.EntityExperimentConstants; +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; @@ -32,12 +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 @@ -48,11 +46,26 @@ final class EnvironmentResource { * @return the resource. */ @SuppressWarnings("JdkObsolete") // Recommended alternative was introduced in java 10 - static Resource createEnvironmentResource(ConfigProperties config) { + 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(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, false); + 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(); @@ -61,20 +74,34 @@ static Resource createEnvironmentResource(ConfigProperties config) { } } - for (Map.Entry entry : config.getMap(ATTRIBUTE_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 otelServiceNameResource(ConfigProperties config) { String serviceName = config.getString(SERVICE_NAME_PROPERTY); - if (serviceName != null) { - resourceBuilder.put(SERVICE_NAME, serviceName); + if (serviceName == null) { + return Resource.empty(); } - return resourceBuilder.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(); } /** 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 d790ca3a97f..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 @@ -178,7 +178,11 @@ void createEnvironmentResource_WithEntities( Map properties, Collection expectedEntities) { ConfigProperties configProperties = DefaultConfigProperties.createFromMap(properties); - Resource resource = ResourceConfiguration.createEnvironmentResource(configProperties); + Resource resource = + ResourceConfiguration.configureResource( + configProperties, + SpiHelper.create(ResourceConfigurationTest.class.getClassLoader()), + (r, c) -> r); Collection entities = EntityUtil.getEntities(resource); assertThat(entities).hasSize(expectedEntities.size()); 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 index 979d0654442..89e1349e69b 100644 --- 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 @@ -6,7 +6,7 @@ package io.opentelemetry.sdk.autoconfigure.declarativeconfig; import static io.opentelemetry.sdk.autoconfigure.declarativeconfig.EnvironmentResource.ENTITIES_PROPERTY; -import static io.opentelemetry.sdk.autoconfigure.spi.internal.EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED; +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; @@ -38,7 +38,7 @@ public Resource create(DeclarativeConfigProperties config) { configPropertiesMap.put(ENTITIES_PROPERTY, envConfigProperties.getString(ENTITIES_PROPERTY)); configPropertiesMap.put(EXPERIMENTAL_ENTITIES_ENABLED, "true"); - return EnvironmentResource.createEnvironmentResource( + 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 38dfd7090be..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,8 +5,8 @@ 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.sdk.autoconfigure.declarativeconfig.model.AttributeNameValueModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.IncludeExcludeModel; @@ -57,14 +57,18 @@ public Resource create(ResourceModel model, DeclarativeConfigContext context) { .build(); 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 469f286c263..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 @@ -10,7 +10,6 @@ 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.autoconfigure.spi.internal.EntityExperimentConstants; import io.opentelemetry.sdk.common.internal.SemConvAttributes; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.ResourceBuilder; @@ -22,7 +21,8 @@ public class ServiceResourceDetector implements ComponentProvider { // 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,33 +40,16 @@ public Resource create(DeclarativeConfigProperties config) { ConfigProperties properties = DefaultConfigProperties.create(Collections.emptyMap(), config.getComponentLoader()); - boolean entitiesEnabled = - properties.getBoolean(EntityExperimentConstants.EXPERIMENTAL_ENTITIES_ENABLED, false); - String serviceName = properties.getString("otel.service.name"); - if (entitiesEnabled) { - if (serviceName != null) { - Entity serviceEntity = - Entity.builder( - SemConvAttributes.SERVICE_TYPE, - Attributes.of(SemConvAttributes.SERVICE_NAME, serviceName)) - .setSchemaUrl(SemConvAttributes.SCHEMA_URL_V1_40_0) - .build(); - EntityUtil.addEntity(builder, serviceEntity); - } - 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); - } else { - if (serviceName != null) { - builder.put(SemConvAttributes.SERVICE_NAME, serviceName); - } - builder.put(SemConvAttributes.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 + * + *

https://github.com/open-telemetry/opentelemetry-specification/pull/5138. + * + *

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/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetectorTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetectorTest.java deleted file mode 100644 index 2e825900c60..00000000000 --- a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/EntitiesEnvResourceDetectorTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.autoconfigure.declarativeconfig; - -import static io.opentelemetry.api.common.AttributeKey.stringKey; -import static org.assertj.core.api.Assertions.assertThat; - -import com.google.common.collect.ImmutableMap; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.common.ComponentLoader; -import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.ResourceModel; -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.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.stream.Stream; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -class EntitiesEnvResourceDetectorTest { - - private static final DeclarativeConfigContext context = - new DeclarativeConfigContext( - ComponentLoader.forClassLoader(SpanProcessorFactoryTest.class.getClassLoader())); - - @ParameterizedTest - @MethodSource("createArgs") - void create(Map systemProperties, Collection expectedEntities) { - systemProperties.forEach(System::setProperty); - try { - ResourceModel resourceModel = - new ResourceModel() - .withDetectionDevelopment( - new ExperimentalResourceDetectionModel() - .withDetectors( - Collections.singletonList( - new ExperimentalResourceDetectorModel() - .withAdditionalProperty("env", null)))); - Resource resource = ResourceFactory.getInstance().create(resourceModel, context); - Collection entities = EntityUtil.getEntities(resource); - assertThat(entities).hasSize(expectedEntities.size()); - assertThat(entities).containsAll(expectedEntities); - } finally { - systemProperties.forEach((key, unused) -> System.clearProperty(key)); - } - } - - static Stream createArgs() { - return Stream.of( - Arguments.argumentSet( - "happy path", - ImmutableMap.of( - "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( - "empty", ImmutableMap.of("otel.entities", ""), Collections.emptyList())); - } -} 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 cb8ce64e184..00000000000 --- a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ServiceResourceDetectorTest.java +++ /dev/null @@ -1,125 +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 io.opentelemetry.sdk.resources.internal.Entity; -import io.opentelemetry.sdk.resources.internal.EntityUtil; -import java.util.Collection; -import java.util.Objects; -import java.util.UUID; -import org.junit.jupiter.api.Test; - -class ServiceResourceDetectorTest { - - @Test - void getTypeAndName() { - ServiceResourceDetector detector = new ServiceResourceDetector(); - - assertThat(detector.getType()).isEqualTo(Resource.class); - assertThat(detector.getName()).isEqualTo("service"); - } - - @Test - void create_SystemPropertySet() { - System.setProperty("otel.service.name", "test"); - try { - 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(); - }); - } finally { - System.clearProperty("otel.service.name"); - } - } - - @Test - void create_NoSystemProperty() { - System.clearProperty("otel.service.name"); - 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(); - }); - } - - @Test - void create_EntitiesEnabled() { - System.setProperty("otel.service.name", "my-service"); - System.setProperty("otel.experimental.entities.enabled", "true"); - try { - Resource resource = new ServiceResourceDetector().create(DeclarativeConfigProperties.empty()); - - Collection entities = EntityUtil.getEntities(resource); - assertThat(entities).hasSize(2); - - assertThat(entities) - .anyMatch( - e -> - e.getType().equals("service") - && e.getSchemaUrl().equals("https://opentelemetry.io/schemas/1.40.0") - && e.getId() - .equals( - Attributes.of(AttributeKey.stringKey("service.name"), "my-service"))); - - assertThat(entities) - .anyMatch( - e -> - e.getType().equals("service.instance") - && e.getSchemaUrl().equals("https://opentelemetry.io/schemas/1.40.0") - && e.getId().get(AttributeKey.stringKey("service.instance.id")) != null); - - // Flat attributes should also be present - Attributes attributes = resource.getAttributes(); - assertThat(attributes.get(AttributeKey.stringKey("service.name"))).isEqualTo("my-service"); - assertThat(attributes.get(AttributeKey.stringKey("service.instance.id"))).isNotNull(); - } finally { - System.clearProperty("otel.service.name"); - System.clearProperty("otel.experimental.entities.enabled"); - } - } - - @Test - void create_EntitiesDisabled() { - System.setProperty("otel.service.name", "my-service"); - System.setProperty("otel.experimental.entities.enabled", "false"); - try { - Resource resource = new ServiceResourceDetector().create(DeclarativeConfigProperties.empty()); - - Collection entities = EntityUtil.getEntities(resource); - assertThat(entities).isEmpty(); - - Attributes attributes = resource.getAttributes(); - assertThat(attributes.get(AttributeKey.stringKey("service.name"))).isEqualTo("my-service"); - assertThat(attributes.get(AttributeKey.stringKey("service.instance.id"))).isNotNull(); - } finally { - System.clearProperty("otel.service.name"); - System.clearProperty("otel.experimental.entities.enabled"); - } - } -} 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 {