From 5e889a8d12b642c294b286ab12bfc47617d71287 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Tue, 4 Nov 2025 16:02:38 -0800 Subject: [PATCH 01/28] Prototype: Complex attributes (Option C - minimal) --- .../opentelemetry/api/common/AttributeKey.java | 5 +++++ .../opentelemetry/api/common/AttributeType.java | 3 ++- .../api/common/AttributesBuilder.java | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java b/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java index 7d012aa14ca..64b101975ab 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java @@ -70,4 +70,9 @@ static AttributeKey> longArrayKey(String key) { static AttributeKey> doubleArrayKey(String key) { return InternalAttributeKeyImpl.create(key, AttributeType.DOUBLE_ARRAY); } + + /** Returns a new AttributeKey for generic {@link Value} valued attributes. */ + static AttributeKey> valueKey(String key) { + return InternalAttributeKeyImpl.create(key, AttributeType.VALUE); + } } diff --git a/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java b/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java index 1c51e36d644..8ed5baa94bf 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java @@ -17,5 +17,6 @@ public enum AttributeType { STRING_ARRAY, BOOLEAN_ARRAY, LONG_ARRAY, - DOUBLE_ARRAY + DOUBLE_ARRAY, + VALUE } diff --git a/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java b/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java index 6623d470137..683a0a7d4eb 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java @@ -14,6 +14,7 @@ import static io.opentelemetry.api.common.AttributeKey.longKey; import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.common.AttributeKey.valueKey; import java.util.Arrays; import java.util.List; @@ -164,6 +165,21 @@ default AttributesBuilder put(String key, boolean... value) { return put(booleanArrayKey(key), toList(value)); } + /** + * Puts a generic ({@link Value}) attribute into this. + * + *

Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + default AttributesBuilder put(String key, Value value) { + if (value == null) { + return this; + } + return put(valueKey(key), value); + } + /** * Puts all the provided attributes into this Builder. * From 4a2c0482a88572f2163e9ed88a5c6b5cf7e4816c Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Wed, 5 Nov 2025 16:45:01 -0800 Subject: [PATCH 02/28] Remove String, Value overload --- .../api/common/AttributesBuilder.java | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java b/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java index 683a0a7d4eb..6623d470137 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java @@ -14,7 +14,6 @@ import static io.opentelemetry.api.common.AttributeKey.longKey; import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; -import static io.opentelemetry.api.common.AttributeKey.valueKey; import java.util.Arrays; import java.util.List; @@ -165,21 +164,6 @@ default AttributesBuilder put(String key, boolean... value) { return put(booleanArrayKey(key), toList(value)); } - /** - * Puts a generic ({@link Value}) attribute into this. - * - *

Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate - * your keys, if possible. - * - * @return this Builder - */ - default AttributesBuilder put(String key, Value value) { - if (value == null) { - return this; - } - return put(valueKey(key), value); - } - /** * Puts all the provided attributes into this Builder. * From 3b8e35f2672c0ee9b19765d106bbd5d4f0a231c6 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Wed, 5 Nov 2025 16:50:30 -0800 Subject: [PATCH 03/28] Add javadoc explaining coersion --- .../api/common/AttributesBuilder.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java b/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java index 6623d470137..d3abb713885 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java @@ -39,6 +39,32 @@ public interface AttributesBuilder { /** * Puts an {@link AttributeKey} with an associated value into this if the value is non-null. * Providing a null value does not remove or unset previously set values. + * + *

Note: when passing a key of type {@link AttributeType#VALUE}, the call will be coerced into + * a narrower type if possible. + * + *

    + *
  • Calling {@code put(AttributeKey.valueKey("key"), Value.of("a"))} is equivalent to calling + * {@code put(AttributeKey.stringKey("key"), "a")}. + *
  • Calling {@code put(AttributeKey.valueKey("key"), Value.of(1L))} is equivalent to calling + * {@code put(AttributeKey.longKey("key"), 1L)}. + *
  • Calling {@code put(AttributeKey.valueKey("key"), Value.of(1.0))} is equivalent to calling + * {@code put(AttributeKey.doubleKey("key"), 1.0)}. + *
  • Calling {@code put(AttributeKey.valueKey("key"), Value.of(true))} is equivalent to + * calling {@code put(AttributeKey.booleanKey("key"), true)}. + *
  • Calling {@code put(AttributeKey.valueKey("key"), Value.of(Value.of("a"), Value.of("b")))} + * is equivalent to calling {@code put(AttributeKey.stringArrayKey("key"), + * Arrays.asList("a", "b"))}. + *
  • Calling {@code put(AttributeKey.valueKey("key"), Value.of(Value.of(1L), Value.of(2L)))} + * is equivalent to calling {@code put(AttributeKey.longArrayKey("key"), Arrays.asList(1L, + * 2L))}. + *
  • Calling {@code put(AttributeKey.valueKey("key"), Value.of(Value.of(1.0), Value.of(2.0)))} + * is equivalent to calling {@code put(AttributeKey.doubleArrayKey("key"), + * Arrays.asList(1.0, 2.0))}. + *
  • Calling {@code put(AttributeKey.valueKey("key"), Value.of(Value.of(true), + * Value.of(false)))} is equivalent to calling {@code + * put(AttributeKey.booleanArrayKey("key"), Arrays.asList(true, false))}. + *
*/ AttributesBuilder put(AttributeKey key, @Nullable T value); From 04937c0ad5dbcf38c18d88ed41c475d6f433e82a Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 6 Nov 2025 19:41:00 -0800 Subject: [PATCH 04/28] more javadoc --- .../api/common/AttributeType.java | 6 +++ .../opentelemetry/api/common/Attributes.java | 37 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java b/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java index 8ed5baa94bf..a174f0dcdda 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java @@ -18,5 +18,11 @@ public enum AttributeType { BOOLEAN_ARRAY, LONG_ARRAY, DOUBLE_ARRAY, + /** + * Simple attributes (i.e. anything other than {@code VALUE} attributes) SHOULD be used whenever + * possible. Instrumentations SHOULD assume that backends do not index individual properties of + * complex attributes, that querying or aggregating on such properties is inefficient and + * complicated, and that reporting complex attributes carries higher performance overhead. + */ VALUE } diff --git a/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java b/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java index 2a9d43793a7..ac00c404e07 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java @@ -33,7 +33,42 @@ @Immutable public interface Attributes { - /** Returns the value for the given {@link AttributeKey}, or {@code null} if not found. */ + /** + * Returns the value for the given {@link AttributeKey}, or {@code null} if not found. + * + *

Simple attributes (i.e. anything other than {@link Value} attributes) SHOULD be used + * whenever possible. Instrumentations SHOULD assume that backends do not index individual + * properties of complex attributes, that querying or aggregating on such properties is + * inefficient and complicated, and that reporting complex attributes carries higher performance + * overhead. + * + *

Note: when passing a key of type {@link AttributeType#VALUE}, the returned value will match + * a narrower type with the given key if one exists. This is the inverse of {@link + * AttributesBuilder#put(AttributeKey, Object)} when the key is {@link AttributeType#VALUE}. + * + *

    + *
  • If {@code put(AttributeKey.stringKey("key"), "a")} was called, then {@code + * get(AttributeKey.valueKey("key"))} returns {@code Value.of("a")}. + *
  • If {@code put(AttributeKey.longKey("key"), 1L)} was called, then {@code + * get(AttributeKey.valueKey("key"))} returns {@code Value.of(1L)}. + *
  • If {@code put(AttributeKey.doubleKey("key"), 1.0)} was called, then {@code + * get(AttributeKey.valueKey("key"))} returns {@code Value.of(1.0)}. + *
  • If {@code put(AttributeKey.booleanKey("key"), true)} was called, then {@code + * get(AttributeKey.valueKey("key"))} returns {@code Value.of(true)}. + *
  • If {@code put(AttributeKey.stringArrayKey("key"), Arrays.asList("a", "b"))} was called, + * then {@code get(AttributeKey.valueKey("key"))} returns {@code Value.of(Value.of("a"), + * Value.of("b"))}. + *
  • If {@code put(AttributeKey.longArrayKey("key"), Arrays.asList(1L, 2L))} was called, then + * {@code get(AttributeKey.valueKey("key"))} returns {@code Value.of(Value.of(1L), + * Value.of(2L))}. + *
  • If {@code put(AttributeKey.doubleArrayKey("key"), Arrays.asList(1.0, 2.0))} was called, + * then {@code get(AttributeKey.valueKey("key"))} returns {@code Value.of(Value.of(1.0), + * Value.of(2.0))}. + *
  • If {@code put(AttributeKey.booleanArrayKey("key"), Arrays.asList(true, false))} was + * called, then {@code get(AttributeKey.valueKey("key"))} returns {@code + * Value.of(Value.of(true), Value.of(false))}. + *
+ */ @Nullable T get(AttributeKey key); From 73466c8375672a423f67f67eb317ca61ed05a271 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 6 Nov 2025 19:49:49 -0800 Subject: [PATCH 05/28] more javadoc --- .../api/common/AttributeType.java | 11 +++++---- .../opentelemetry/api/common/Attributes.java | 24 +++++++++++-------- .../api/common/AttributesBuilder.java | 12 ++++++++-- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java b/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java index a174f0dcdda..6209301f573 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java @@ -19,10 +19,13 @@ public enum AttributeType { LONG_ARRAY, DOUBLE_ARRAY, /** - * Simple attributes (i.e. anything other than {@code VALUE} attributes) SHOULD be used whenever - * possible. Instrumentations SHOULD assume that backends do not index individual properties of - * complex attributes, that querying or aggregating on such properties is inefficient and - * complicated, and that reporting complex attributes carries higher performance overhead. + * Simple attributes ({@link AttributeType#STRING}, {@link AttributeType#LONG}, {@link + * AttributeType#DOUBLE}, {@link AttributeType#BOOLEAN}, {@link AttributeType#STRING_ARRAY}, + * {@link AttributeType#LONG_ARRAY}, {@link AttributeType#DOUBLE_ARRAY}, {@link + * AttributeType#BOOLEAN_ARRAY}) SHOULD be used whenever possible. Instrumentations SHOULD assume + * that backends do not index individual properties of complex attributes, that querying or + * aggregating on such properties is inefficient and complicated, and that reporting complex + * attributes carries higher performance overhead. */ VALUE } diff --git a/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java b/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java index ac00c404e07..1ee1c43718c 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java @@ -36,14 +36,8 @@ public interface Attributes { /** * Returns the value for the given {@link AttributeKey}, or {@code null} if not found. * - *

Simple attributes (i.e. anything other than {@link Value} attributes) SHOULD be used - * whenever possible. Instrumentations SHOULD assume that backends do not index individual - * properties of complex attributes, that querying or aggregating on such properties is - * inefficient and complicated, and that reporting complex attributes carries higher performance - * overhead. - * - *

Note: when passing a key of type {@link AttributeType#VALUE}, the returned value will match - * a narrower type with the given key if one exists. This is the inverse of {@link + *

Note: this method will automatically convert simple attributes to complex attributes when + * passed a key of type {@link AttributeType#VALUE}. This is the inverse of {@link * AttributesBuilder#put(AttributeKey, Object)} when the key is {@link AttributeType#VALUE}. * *

    @@ -72,7 +66,12 @@ public interface Attributes { @Nullable T get(AttributeKey key); - /** Iterates over all the key-value pairs of attributes contained by this instance. */ + /** + * Iterates over all the key-value pairs of attributes contained by this instance. + * + *

    Note: {@link AttributeType#VALUE} attributes will be represented as simple attributes if + * possible. See {@link AttributesBuilder#put(AttributeKey, Object)} for more details. + */ void forEach(BiConsumer, ? super Object> consumer); /** The number of attributes contained in this. */ @@ -81,7 +80,12 @@ public interface Attributes { /** Whether there are any attributes contained in this. */ boolean isEmpty(); - /** Returns a read-only view of this {@link Attributes} as a {@link Map}. */ + /** + * Returns a read-only view of this {@link Attributes} as a {@link Map}. + * + *

    Note: {@link AttributeType#VALUE} attributes will be represented as simple attributes in + * this map if possible. See {@link AttributesBuilder#put(AttributeKey, Object)} for more details. + */ Map, Object> asMap(); /** Returns a {@link Attributes} instance with no attributes. */ diff --git a/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java b/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java index d3abb713885..bc246ad3e8b 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java @@ -40,8 +40,16 @@ public interface AttributesBuilder { * Puts an {@link AttributeKey} with an associated value into this if the value is non-null. * Providing a null value does not remove or unset previously set values. * - *

    Note: when passing a key of type {@link AttributeType#VALUE}, the call will be coerced into - * a narrower type if possible. + *

    Simple attributes ({@link AttributeType#STRING}, {@link AttributeType#LONG}, {@link + * AttributeType#DOUBLE}, {@link AttributeType#BOOLEAN}, {@link AttributeType#STRING_ARRAY}, + * {@link AttributeType#LONG_ARRAY}, {@link AttributeType#DOUBLE_ARRAY}, {@link + * AttributeType#BOOLEAN_ARRAY}) SHOULD be used whenever possible. Instrumentations SHOULD assume + * that backends do not index individual properties of complex attributes, that querying or + * aggregating on such properties is inefficient and complicated, and that reporting complex + * attributes carries higher performance overhead. + * + *

    Note: This method will automatically convert complex attributes ({@link + * AttributeType#VALUE}) to simple attributes when possible. * *

      *
    • Calling {@code put(AttributeKey.valueKey("key"), Value.of("a"))} is equivalent to calling From 1958768a881c7d5d86eafbf7bfab342d76a8b7d7 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 7 Nov 2025 12:57:49 -0800 Subject: [PATCH 06/28] more --- .../opentelemetry/api/common/Attributes.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java b/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java index 1ee1c43718c..8ac46146e6b 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java @@ -36,9 +36,10 @@ public interface Attributes { /** * Returns the value for the given {@link AttributeKey}, or {@code null} if not found. * - *

      Note: this method will automatically convert simple attributes to complex attributes when - * passed a key of type {@link AttributeType#VALUE}. This is the inverse of {@link - * AttributesBuilder#put(AttributeKey, Object)} when the key is {@link AttributeType#VALUE}. + *

      Note: this method will automatically return the corresponding {@link Value} instance when + * passed a key of type {@link AttributeType#VALUE} and a simple attribute is found. This is the + * inverse of {@link AttributesBuilder#put(AttributeKey, Object)} when the key is {@link + * AttributeType#VALUE}. * *

        *
      • If {@code put(AttributeKey.stringKey("key"), "a")} was called, then {@code @@ -62,6 +63,17 @@ public interface Attributes { * called, then {@code get(AttributeKey.valueKey("key"))} returns {@code * Value.of(Value.of(true), Value.of(false))}. *
      + * + * Further, if {@code put(AttributeKey.valueKey("key"), Value.of(emptyList()))} was called, then + * + *
        + *
      • {@code get(AttributeKey.stringArrayKey("key"))} + *
      • {@code get(AttributeKey.longArrayKey("key"))} + *
      • {@code get(AttributeKey.booleanArrayKey("key"))} + *
      • {@code get(AttributeKey.doubleArrayKey("key"))} + *
      + * + * all return an empty list (as opposed to {@code null}). */ @Nullable T get(AttributeKey key); From e4308cfea303ab7ecabc2d91776fe42d9b19967d Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Tue, 4 Nov 2025 17:29:32 -0800 Subject: [PATCH 07/28] Complex Attributes - incubating implementation --- .../common/ExtendedAttributeKey.java | 6 ++++++ .../common/ExtendedAttributeType.java | 3 ++- .../incubator/common/ExtendedAttributes.java | 3 ++- .../common/ExtendedAttributesBuilder.java | 14 +++++++++++++ .../InternalExtendedAttributeKeyImpl.java | 1 + .../common/ExtendedAttributeKeyTest.java | 4 +++- .../common/ExtendedAttributesTest.java | 21 ++++++++++++++++++- ...edAttributeKeyValueStatelessMarshaler.java | 7 +++++++ .../internal/otlp/IncubatingUtil.java | 3 +++ 9 files changed, 58 insertions(+), 4 deletions(-) diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java index 13357a31a56..14f913d8b16 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java @@ -6,6 +6,7 @@ package io.opentelemetry.api.incubator.common; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.incubator.internal.InternalExtendedAttributeKeyImpl; import java.util.List; import javax.annotation.Nullable; @@ -97,4 +98,9 @@ static ExtendedAttributeKey> doubleArrayKey(String key) { static ExtendedAttributeKey extendedAttributesKey(String key) { return InternalExtendedAttributeKeyImpl.create(key, ExtendedAttributeType.EXTENDED_ATTRIBUTES); } + + /** Returns a new ExtendedAttributeKey for {@link Value} valued attributes. */ + static ExtendedAttributeKey> valueKey(String key) { + return InternalExtendedAttributeKeyImpl.create(key, ExtendedAttributeType.VALUE); + } } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java index 8d2c67181b6..5f34daa1f77 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java @@ -22,5 +22,6 @@ public enum ExtendedAttributeType { LONG_ARRAY, DOUBLE_ARRAY, // Extended types unique to ExtendedAttributes - EXTENDED_ATTRIBUTES; + EXTENDED_ATTRIBUTES, + VALUE; } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java index 0fc88a2ea49..6be407b2b02 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java @@ -19,7 +19,8 @@ *

      "extended" refers an extended set of allowed value types compared to standard {@link * Attributes}. Notably, {@link ExtendedAttributes} values can be of type {@link * ExtendedAttributeType#EXTENDED_ATTRIBUTES}, allowing nested {@link ExtendedAttributes} of - * arbitrary depth. + * arbitrary depth, and {@link ExtendedAttributeType#VALUE}, allowing attributes backed by {@link + * io.opentelemetry.api.common.Value}. * *

      Where standard {@link Attributes} are accepted everyone that OpenTelemetry represents key / * value pairs, {@link ExtendedAttributes} are only accepted in select places, such as log records diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java index 1e0de3b4c38..0a9e91b04ae 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java @@ -14,9 +14,11 @@ import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.longKey; import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.stringArrayKey; import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.stringKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.valueKey; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; import java.util.Arrays; import java.util.List; import java.util.function.Predicate; @@ -97,6 +99,18 @@ default ExtendedAttributesBuilder put(String key, ExtendedAttributes value) return put(ExtendedAttributeKey.extendedAttributesKey(key), value); } + /** + * Puts a {@link Value} attribute into this. + * + *

      Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @return this Builder + */ + default ExtendedAttributesBuilder put(String key, Value value) { + return put(valueKey(key), value); + } + /** * Puts a String array attribute into this. * diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java index e07f72f0121..daf5c3264d6 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java @@ -139,6 +139,7 @@ public static AttributeKey toAttributeKey(ExtendedAttributeKey extende return InternalAttributeKeyImpl.create( extendedAttributeKey.getKey(), AttributeType.DOUBLE_ARRAY); case EXTENDED_ATTRIBUTES: + case VALUE: return null; } throw new IllegalArgumentException( diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKeyTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKeyTest.java index d2f77625d1f..8c35459a467 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKeyTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKeyTest.java @@ -80,6 +80,8 @@ private static Stream attributeKeyArgs() { ExtendedAttributeKey.extendedAttributesKey("key"), "key", ExtendedAttributeType.EXTENDED_ATTRIBUTES, - null)); + null), + Arguments.of( + ExtendedAttributeKey.valueKey("key"), "key", ExtendedAttributeType.VALUE, null)); } } diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java index 4481cead3d0..443b7b6fb54 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java @@ -10,6 +10,7 @@ import com.google.common.collect.ImmutableMap; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -116,7 +117,10 @@ void asAttributes(ExtendedAttributes extendedAttributes, Map exp }); long expectedSize = - expectedMap.values().stream().filter(value -> !(value instanceof Map)).count(); + expectedMap.values().stream() + .filter(value -> !(value instanceof Map)) + .filter(value -> !(value instanceof Value)) + .count(); assertThat(attributes.size()).isEqualTo(expectedSize); } @@ -212,6 +216,9 @@ private static Stream attributesArgs() { ImmutableMap.builder() .put("key", ImmutableMap.builder().put("child", "value").build()) .build()), + Arguments.of( + ExtendedAttributes.builder().put("key", Value.of("value")).build(), + ImmutableMap.builder().put("key", Value.of("value")).build()), Arguments.of( ExtendedAttributes.builder() .put(ExtendedAttributeKey.stringKey("key"), "value") @@ -255,6 +262,11 @@ private static Stream attributesArgs() { ImmutableMap.builder() .put("key", ImmutableMap.builder().put("child", "value").build()) .build()), + Arguments.of( + ExtendedAttributes.builder() + .put(ExtendedAttributeKey.valueKey("key"), Value.of("value")) + .build(), + ImmutableMap.builder().put("key", Value.of("value")).build()), // Multiple entries Arguments.of( ExtendedAttributes.builder() @@ -268,6 +280,7 @@ private static Stream attributesArgs() { .put("key8", 1L, 2L) .put("key9", 1.1, 2.2) .put("key10", ExtendedAttributes.builder().put("child", "value").build()) + .put("key11", Value.of("value")) .build(), ImmutableMap.builder() .put("key1", "value1") @@ -280,6 +293,7 @@ private static Stream attributesArgs() { .put("key8", Arrays.asList(1L, 2L)) .put("key9", Arrays.asList(1.1, 2.2)) .put("key10", ImmutableMap.builder().put("child", "value").build()) + .put("key11", Value.of("value")) .build())); } @@ -316,6 +330,8 @@ private static ExtendedAttributeKey getKey(String key, Object value) { return ExtendedAttributeKey.doubleArrayKey(key); case EXTENDED_ATTRIBUTES: return ExtendedAttributeKey.extendedAttributesKey(key); + case VALUE: + return ExtendedAttributeKey.valueKey(key); } throw new IllegalArgumentException(); } @@ -355,6 +371,9 @@ private static ExtendedAttributeType getType(Object value) { if ((value instanceof Map)) { return ExtendedAttributeType.EXTENDED_ATTRIBUTES; } + if (value instanceof Value) { + return ExtendedAttributeType.VALUE; + } throw new IllegalArgumentException("Unrecognized value type: " + value); } } diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ExtendedAttributeKeyValueStatelessMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ExtendedAttributeKeyValueStatelessMarshaler.java index de31139abcb..7cde8c9f855 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ExtendedAttributeKeyValueStatelessMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ExtendedAttributeKeyValueStatelessMarshaler.java @@ -5,6 +5,7 @@ package io.opentelemetry.exporter.internal.otlp; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; import io.opentelemetry.api.incubator.common.ExtendedAttributeType; import io.opentelemetry.api.incubator.common.ExtendedAttributes; @@ -174,6 +175,9 @@ public int getBinarySerializedSize( (ExtendedAttributes) value, ExtendedAttributesKeyValueListStatelessMarshaler.INSTANCE, context); + case VALUE: + return AnyValueStatelessMarshaler.INSTANCE.getBinarySerializedSize( + (Value) value, context); } // Error prone ensures the switch statement is complete, otherwise only can happen with // unaligned versions which are not supported. @@ -220,6 +224,9 @@ public void writeTo( ExtendedAttributesKeyValueListStatelessMarshaler.INSTANCE, context); return; + case VALUE: + AnyValueStatelessMarshaler.INSTANCE.writeTo(output, (Value) value, context); + return; } // Error prone ensures the switch statement is complete, otherwise only can happen with // unaligned versions which are not supported. diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/IncubatingUtil.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/IncubatingUtil.java index 90660fd5a85..29b51644aa6 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/IncubatingUtil.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/IncubatingUtil.java @@ -5,6 +5,7 @@ package io.opentelemetry.exporter.internal.otlp; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; import io.opentelemetry.api.incubator.common.ExtendedAttributes; import io.opentelemetry.api.incubator.internal.InternalExtendedAttributeKeyImpl; @@ -116,6 +117,8 @@ private static KeyValueMarshaler create(ExtendedAttributeKey attributeKey, Ob new KeyValueListAnyValueMarshaler( new KeyValueListAnyValueMarshaler.KeyValueListMarshaler( createForExtendedAttributes((ExtendedAttributes) value)))); + case VALUE: + return new KeyValueMarshaler(keyUtf8, AnyValueMarshaler.create((Value) value)); } // Error prone ensures the switch statement is complete, otherwise only can happen with // unaligned versions which are not supported. From 5f342e29c61471efbf27a5e2144e17b43176169d Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Tue, 4 Nov 2025 18:12:55 -0800 Subject: [PATCH 08/28] Deprecation --- .../api/incubator/common/ExtendedAttributeKey.java | 9 ++++++++- .../api/incubator/common/ExtendedAttributeType.java | 5 +++++ .../api/incubator/common/ExtendedAttributes.java | 5 +++-- .../api/incubator/common/ExtendedAttributesBuilder.java | 3 +++ .../internal/InternalExtendedAttributeKeyImpl.java | 1 + .../api/incubator/common/ExtendedAttributeKeyTest.java | 1 + .../api/incubator/common/ExtendedAttributesTest.java | 1 + .../incubator/logs/ExtendedLogsBridgeApiUsageTest.java | 1 + .../ExtendedAttributeKeyValueStatelessMarshaler.java | 6 ++++-- .../exporter/internal/otlp/IncubatingUtil.java | 3 ++- 10 files changed, 29 insertions(+), 6 deletions(-) diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java index 14f913d8b16..10e2c113b50 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java @@ -94,7 +94,14 @@ static ExtendedAttributeKey> doubleArrayKey(String key) { return fromAttributeKey(AttributeKey.doubleArrayKey(key)); } - /** Returns a new ExtendedAttributeKey for Map valued attributes. */ + /** + * Returns a new ExtendedAttributeKey for {@link ExtendedAttributes} valued attributes. + * + * @deprecated Use {@link #valueKey(String)} in combination with {@link Value#of(java.util.Map)} + * instead. + */ + @Deprecated + @SuppressWarnings("deprecation") static ExtendedAttributeKey extendedAttributesKey(String key) { return InternalExtendedAttributeKeyImpl.create(key, ExtendedAttributeType.EXTENDED_ATTRIBUTES); } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java index 5f34daa1f77..d75bd2c5610 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java @@ -22,6 +22,11 @@ public enum ExtendedAttributeType { LONG_ARRAY, DOUBLE_ARRAY, // Extended types unique to ExtendedAttributes + /** + * @deprecated Use {@link #VALUE} with {@link io.opentelemetry.api.common.Value}-based maps + * instead. + */ + @Deprecated EXTENDED_ATTRIBUTES, VALUE; } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java index 6be407b2b02..8ba76a69576 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java @@ -18,9 +18,10 @@ * *

      "extended" refers an extended set of allowed value types compared to standard {@link * Attributes}. Notably, {@link ExtendedAttributes} values can be of type {@link + * ExtendedAttributeType#VALUE}, allowing attributes backed by {@link + * io.opentelemetry.api.common.Value}, and the deprecated {@link * ExtendedAttributeType#EXTENDED_ATTRIBUTES}, allowing nested {@link ExtendedAttributes} of - * arbitrary depth, and {@link ExtendedAttributeType#VALUE}, allowing attributes backed by {@link - * io.opentelemetry.api.common.Value}. + * arbitrary depth. * *

      Where standard {@link Attributes} are accepted everyone that OpenTelemetry represents key / * value pairs, {@link ExtendedAttributes} are only accepted in select places, such as log records diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java index 0a9e91b04ae..1e7a4234839 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java @@ -93,8 +93,11 @@ default ExtendedAttributesBuilder put(String key, boolean value) { *

      Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and * pre-allocate your keys, if possible. * + * @deprecated Use {@link #put(String, Value)} with {@link Value#of(java.util.Map)} instead. * @return this Builder */ + @Deprecated + @SuppressWarnings("deprecation") default ExtendedAttributesBuilder put(String key, ExtendedAttributes value) { return put(ExtendedAttributeKey.extendedAttributesKey(key), value); } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java index daf5c3264d6..005952b3040 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java @@ -115,6 +115,7 @@ private static int buildHashCode(ExtendedAttributeType type, String key) { * io.opentelemetry.api.common.AttributeType}. */ @Nullable + @SuppressWarnings("deprecation") // Supporting deprecated EXTENDED_ATTRIBUTES until removed public static AttributeKey toAttributeKey(ExtendedAttributeKey extendedAttributeKey) { switch (extendedAttributeKey.getType()) { case STRING: diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKeyTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKeyTest.java index 8c35459a467..ba148e7c9bf 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKeyTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKeyTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +@SuppressWarnings("deprecation") // Testing deprecated EXTENDED_ATTRIBUTES until removed public class ExtendedAttributeKeyTest { @ParameterizedTest diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java index 443b7b6fb54..d4e72f4da00 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +@SuppressWarnings("deprecation") // Testing deprecated EXTENDED_ATTRIBUTES until removed class ExtendedAttributesTest { @ParameterizedTest diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java index 05c3e65f859..80e87d4ad97 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java @@ -104,6 +104,7 @@ private static String flipCoin() { AttributeKey> doubleArrKey = AttributeKey.doubleArrayKey("acme.double_array"); // Extended keys + @SuppressWarnings("deprecation") // Supporting deprecated EXTENDED_ATTRIBUTES until removed ExtendedAttributeKey mapKey = ExtendedAttributeKey.extendedAttributesKey("acme.map"); diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ExtendedAttributeKeyValueStatelessMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ExtendedAttributeKeyValueStatelessMarshaler.java index 7cde8c9f855..b812a9d2a29 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ExtendedAttributeKeyValueStatelessMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ExtendedAttributeKeyValueStatelessMarshaler.java @@ -141,7 +141,8 @@ private static class ValueStatelessMarshaler implements StatelessMarshaler2, Object> { static final ValueStatelessMarshaler INSTANCE = new ValueStatelessMarshaler(); - @SuppressWarnings("unchecked") + // Supporting deprecated EXTENDED_ATTRIBUTES type until removed + @SuppressWarnings({"unchecked", "deprecation"}) @Override public int getBinarySerializedSize( ExtendedAttributeKey attributeKey, Object value, MarshalerContext context) { @@ -184,7 +185,8 @@ public int getBinarySerializedSize( throw new IllegalArgumentException("Unsupported attribute type."); } - @SuppressWarnings("unchecked") + // Supporting deprecated EXTENDED_ATTRIBUTES type until removed + @SuppressWarnings({"unchecked", "deprecation"}) @Override public void writeTo( Serializer output, diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/IncubatingUtil.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/IncubatingUtil.java index 29b51644aa6..da34f7756cc 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/IncubatingUtil.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/IncubatingUtil.java @@ -81,7 +81,8 @@ public void accept(ExtendedAttributeKey attributeKey, Object o) { } // TODO(jack-berg): move to KeyValueMarshaler when ExtendedAttributes is stable - @SuppressWarnings("unchecked") + // Supporting deprecated EXTENDED_ATTRIBUTES type until removed + @SuppressWarnings({"unchecked", "deprecation"}) private static KeyValueMarshaler create(ExtendedAttributeKey attributeKey, Object value) { byte[] keyUtf8; if (attributeKey.getKey().isEmpty()) { From 8c40374be0c86f5f27b3feb02ba6371e72ba1ea7 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 7 Nov 2025 10:02:53 -0800 Subject: [PATCH 09/28] More implementation --- .../api/common/ArrayBackedAttributes.java | 66 ++++++ .../common/ArrayBackedAttributesBuilder.java | 121 ++++++++++ .../api/common/AttributesTest.java | 218 ++++++++++++++++++ .../incubator/common/ExtendedAttributes.java | 4 +- .../common/ExtendedAttributesBuilder.java | 14 +- .../InternalExtendedAttributeKeyImpl.java | 3 + .../logs/ExtendedLogsBridgeApiUsageTest.java | 25 ++ .../AttributeKeyValueStatelessMarshaler.java | 7 + .../internal/otlp/KeyValueMarshaler.java | 3 + 9 files changed, 445 insertions(+), 16 deletions(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java index 165c9ffe2e2..1d7ddbe9491 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java @@ -8,6 +8,7 @@ import io.opentelemetry.api.internal.ImmutableKeyValuePairs; import java.util.ArrayList; import java.util.Comparator; +import java.util.List; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -45,9 +46,74 @@ public AttributesBuilder toBuilder() { @Override @Nullable public T get(AttributeKey key) { + if (key != null && key.getType() == AttributeType.VALUE) { + return (T) getAsValue(key.getKey()); + } return (T) super.get(key); } + @Nullable + private Value getAsValue(String keyName) { + // Find any attribute with the same key name and convert it to Value + List data = data(); + for (int i = 0; i < data.size(); i += 2) { + AttributeKey currentKey = (AttributeKey) data.get(i); + if (currentKey.getKey().equals(keyName)) { + Object value = data.get(i + 1); + return asValue(currentKey.getType(), value); + } + } + return null; + } + + @SuppressWarnings("unchecked") + @Nullable + private static Value asValue(AttributeType type, Object value) { + switch (type) { + case STRING: + return Value.of((String) value); + case LONG: + return Value.of((Long) value); + case DOUBLE: + return Value.of((Double) value); + case BOOLEAN: + return Value.of((Boolean) value); + case STRING_ARRAY: + List stringList = (List) value; + Value[] stringValues = new Value[stringList.size()]; + for (int i = 0; i < stringList.size(); i++) { + stringValues[i] = Value.of(stringList.get(i)); + } + return Value.of(stringValues); + case LONG_ARRAY: + List longList = (List) value; + Value[] longValues = new Value[longList.size()]; + for (int i = 0; i < longList.size(); i++) { + longValues[i] = Value.of(longList.get(i)); + } + return Value.of(longValues); + case DOUBLE_ARRAY: + List doubleList = (List) value; + Value[] doubleValues = new Value[doubleList.size()]; + for (int i = 0; i < doubleList.size(); i++) { + doubleValues[i] = Value.of(doubleList.get(i)); + } + return Value.of(doubleValues); + case BOOLEAN_ARRAY: + List booleanList = (List) value; + Value[] booleanValues = new Value[booleanList.size()]; + for (int i = 0; i < booleanList.size(); i++) { + booleanValues[i] = Value.of(booleanList.get(i)); + } + return Value.of(booleanValues); + case VALUE: + // Already a Value + return (Value) value; + } + // Should not reach here + return null; + } + static Attributes sortAndFilterToAttributes(Object... data) { // null out any empty keys or keys with null values // so they will then be removed by the sortAndFilter method. diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java index af575a61e36..4ec0822ca55 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java @@ -5,6 +5,15 @@ package io.opentelemetry.api.common; +import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -38,15 +47,127 @@ public AttributesBuilder put(AttributeKey key, int value) { } @Override + @SuppressWarnings("unchecked") public AttributesBuilder put(AttributeKey key, @Nullable T value) { if (key == null || key.getKey().isEmpty() || value == null) { return this; } + if (key.getType() == AttributeType.VALUE && value instanceof Value) { + putValue(key, (Value) value); + return this; + } data.add(key); data.add(value); return this; } + private void putValue(AttributeKey key, Value valueObj) { + // Convert VALUE type to narrower type when possible + String keyName = key.getKey(); + switch (valueObj.getType()) { + case STRING: + data.add(stringKey(keyName)); + data.add(valueObj.getValue()); + return; + case LONG: + data.add(longKey(keyName)); + data.add(valueObj.getValue()); + return; + case DOUBLE: + data.add(doubleKey(keyName)); + data.add(valueObj.getValue()); + return; + case BOOLEAN: + data.add(booleanKey(keyName)); + data.add(valueObj.getValue()); + return; + case ARRAY: + @SuppressWarnings("unchecked") + List> arrayValues = (List>) valueObj.getValue(); + AttributeType attributeType = attributeType(arrayValues); + switch (attributeType) { + case STRING_ARRAY: + List strings = new ArrayList<>(); + for (Value v : arrayValues) { + strings.add((String) v.getValue()); + } + data.add(stringArrayKey(keyName)); + data.add(strings); + return; + case LONG_ARRAY: + List longs = new ArrayList<>(); + for (Value v : arrayValues) { + longs.add((Long) v.getValue()); + } + data.add(longArrayKey(keyName)); + data.add(longs); + return; + case DOUBLE_ARRAY: + List doubles = new ArrayList<>(); + for (Value v : arrayValues) { + doubles.add((Double) v.getValue()); + } + data.add(doubleArrayKey(keyName)); + data.add(doubles); + return; + case BOOLEAN_ARRAY: + List booleans = new ArrayList<>(); + for (Value v : arrayValues) { + booleans.add((Boolean) v.getValue()); + } + data.add(booleanArrayKey(keyName)); + data.add(booleans); + return; + case VALUE: + // Not coercible (empty, non-homogeneous, or unsupported element type) + // TODO when empty, retrieve it when asked for any kind of array? + data.add(key); + data.add(valueObj); + return; + default: + throw new IllegalArgumentException("Unexpected array attribute type: " + attributeType); + } + case KEY_VALUE_LIST: + case BYTES: + // Keep as VALUE type + data.add(key); + data.add(valueObj); + return; + } + } + + /** + * Returns the AttributeType for a homogeneous array (STRING_ARRAY, LONG_ARRAY, DOUBLE_ARRAY, or + * BOOLEAN_ARRAY), or VALUE if the array is empty, non-homogeneous, or contains unsupported + * element types. + */ + private static AttributeType attributeType(List> arrayValues) { + if (arrayValues.isEmpty()) { + return AttributeType.VALUE; + } + ValueType elementType = arrayValues.get(0).getType(); + for (Value v : arrayValues) { + if (v.getType() != elementType) { + return AttributeType.VALUE; + } + } + switch (elementType) { + case STRING: + return AttributeType.STRING_ARRAY; + case LONG: + return AttributeType.LONG_ARRAY; + case DOUBLE: + return AttributeType.DOUBLE_ARRAY; + case BOOLEAN: + return AttributeType.BOOLEAN_ARRAY; + case ARRAY: + case KEY_VALUE_LIST: + case BYTES: + return AttributeType.VALUE; + } + throw new IllegalArgumentException("Unsupported element type: " + elementType); + } + @Override @SuppressWarnings({"unchecked", "rawtypes"}) public AttributesBuilder putAll(Attributes attributes) { diff --git a/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java b/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java index dcc6f701e5a..21c7c3a6b28 100644 --- a/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java @@ -13,6 +13,7 @@ import static io.opentelemetry.api.common.AttributeKey.longKey; import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.common.AttributeKey.valueKey; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -590,4 +591,221 @@ void emptyValueIsValid() { Attributes attributes = Attributes.of(key, ""); assertThat(attributes.get(key)).isEqualTo(""); } + + @Test + void complexValueStoredAsString() { + // When putting a VALUE attribute with a string Value, it should be stored as STRING type + Attributes attributes = + Attributes.builder() + .put(valueKey("key"), Value.of("test")) + .build(); + + // Should be stored as STRING type internally + assertThat(attributes.get(stringKey("key"))).isEqualTo("test"); + assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of("test")); + + // forEach should show STRING type + Map, Object> entriesSeen = new LinkedHashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(stringKey("key"), "test")); + + // asMap should show STRING type + assertThat(attributes.asMap()).containsExactly(entry(stringKey("key"), "test")); + } + + @Test + void complexValueStoredAsLong() { + // When putting a VALUE attribute with a long Value, it should be stored as LONG type + Attributes attributes = + Attributes.builder() + .put(valueKey("key"), Value.of(123L)) + .build(); + + // Should be stored as LONG type internally + assertThat(attributes.get(longKey("key"))).isEqualTo(123L); + assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(123L)); + + // forEach should show LONG type + Map, Object> entriesSeen = new LinkedHashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(longKey("key"), 123L)); + + // asMap should show LONG type + assertThat(attributes.asMap()).containsExactly(entry(longKey("key"), 123L)); + } + + @Test + void complexValueStoredAsDouble() { + // When putting a VALUE attribute with a double Value, it should be stored as DOUBLE type + Attributes attributes = + Attributes.builder() + .put(valueKey("key"), Value.of(3.14)) + .build(); + + // Should be stored as DOUBLE type internally + assertThat(attributes.get(doubleKey("key"))).isEqualTo(3.14); + assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(3.14)); + + // forEach should show DOUBLE type + Map, Object> entriesSeen = new LinkedHashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(doubleKey("key"), 3.14)); + + // asMap should show DOUBLE type + assertThat(attributes.asMap()).containsExactly(entry(doubleKey("key"), 3.14)); + } + + @Test + void complexValueStoredAsBoolean() { + // When putting a VALUE attribute with a boolean Value, it should be stored as BOOLEAN type + Attributes attributes = + Attributes.builder() + .put(valueKey("key"), Value.of(true)) + .build(); + + // Should be stored as BOOLEAN type internally + assertThat(attributes.get(booleanKey("key"))).isEqualTo(true); + assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(true)); + + // forEach should show BOOLEAN type + Map, Object> entriesSeen = new LinkedHashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(booleanKey("key"), true)); + + // asMap should show BOOLEAN type + assertThat(attributes.asMap()).containsExactly(entry(booleanKey("key"), true)); + } + + @Test + void complexValueStoredAsStringArray() { + // When putting a VALUE attribute with a homogeneous string array, it should be stored as + // STRING_ARRAY type + Attributes attributes = + Attributes.builder() + .put(valueKey("key"), Value.of(Value.of("a"), Value.of("b"))) + .build(); + + // Should be stored as STRING_ARRAY type internally + assertThat(attributes.get(stringArrayKey("key"))).isEqualTo(Arrays.asList("a", "b")); + assertThat(attributes.get(valueKey("key"))) + .isEqualTo(Value.of(Value.of("a"), Value.of("b"))); + + // forEach should show STRING_ARRAY type + Map, Object> entriesSeen = new LinkedHashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(stringArrayKey("key"), Arrays.asList("a", "b"))); + + // asMap should show STRING_ARRAY type + assertThat(attributes.asMap()) + .containsExactly(entry(stringArrayKey("key"), Arrays.asList("a", "b"))); + } + + @Test + void complexValueStoredAsLongArray() { + // When putting a VALUE attribute with a homogeneous long array, it should be stored as + // LONG_ARRAY type + Attributes attributes = + Attributes.builder() + .put(valueKey("key"), Value.of(Value.of(1L), Value.of(2L))) + .build(); + + // Should be stored as LONG_ARRAY type internally + assertThat(attributes.get(longArrayKey("key"))).isEqualTo(Arrays.asList(1L, 2L)); + assertThat(attributes.get(valueKey("key"))) + .isEqualTo(Value.of(Value.of(1L), Value.of(2L))); + + // forEach should show LONG_ARRAY type + Map, Object> entriesSeen = new LinkedHashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(longArrayKey("key"), Arrays.asList(1L, 2L))); + + // asMap should show LONG_ARRAY type + assertThat(attributes.asMap()) + .containsExactly(entry(longArrayKey("key"), Arrays.asList(1L, 2L))); + } + + @Test + void complexValueStoredAsDoubleArray() { + // When putting a VALUE attribute with a homogeneous double array, it should be stored as + // DOUBLE_ARRAY type + Attributes attributes = + Attributes.builder() + .put(valueKey("key"), Value.of(Value.of(1.1), Value.of(2.2))) + .build(); + + // Should be stored as DOUBLE_ARRAY type internally + assertThat(attributes.get(doubleArrayKey("key"))).isEqualTo(Arrays.asList(1.1, 2.2)); + assertThat(attributes.get(valueKey("key"))) + .isEqualTo(Value.of(Value.of(1.1), Value.of(2.2))); + + // forEach should show DOUBLE_ARRAY type + Map, Object> entriesSeen = new LinkedHashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(doubleArrayKey("key"), Arrays.asList(1.1, 2.2))); + + // asMap should show DOUBLE_ARRAY type + assertThat(attributes.asMap()) + .containsExactly(entry(doubleArrayKey("key"), Arrays.asList(1.1, 2.2))); + } + + @Test + void complexValueStoredAsBooleanArray() { + // When putting a VALUE attribute with a homogeneous boolean array, it should be stored as + // BOOLEAN_ARRAY type + Attributes attributes = + Attributes.builder() + .put(valueKey("key"), Value.of(Value.of(true), Value.of(false))) + .build(); + + // Should be stored as BOOLEAN_ARRAY type internally + assertThat(attributes.get(booleanArrayKey("key"))).isEqualTo(Arrays.asList(true, false)); + assertThat(attributes.get(valueKey("key"))) + .isEqualTo(Value.of(Value.of(true), Value.of(false))); + + // forEach should show BOOLEAN_ARRAY type + Map, Object> entriesSeen = new LinkedHashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen) + .containsExactly(entry(booleanArrayKey("key"), Arrays.asList(true, false))); + + // asMap should show BOOLEAN_ARRAY type + assertThat(attributes.asMap()) + .containsExactly(entry(booleanArrayKey("key"), Arrays.asList(true, false))); + } + + @Test + void simpleAttributeRetrievedAsComplexValue() { + // When getting with a VALUE key, simple attributes should be converted to Value + Attributes attributes = + Attributes.builder() + .put(stringKey("string"), "test") + .put(longKey("long"), 123L) + .put(doubleKey("double"), 3.14) + .put(booleanKey("boolean"), true) + .put(stringArrayKey("stringArray"), "a", "b") + .put(longArrayKey("longArray"), 1L, 2L) + .put(doubleArrayKey("doubleArray"), 1.1, 2.2) + .put(booleanArrayKey("booleanArray"), true, false) + .build(); + + // Should be able to get simple attributes as Value + assertThat(attributes.get(valueKey("string"))).isEqualTo(Value.of("test")); + assertThat(attributes.get(valueKey("long"))).isEqualTo(Value.of(123L)); + assertThat(attributes.get(valueKey("double"))).isEqualTo(Value.of(3.14)); + assertThat(attributes.get(valueKey("boolean"))).isEqualTo(Value.of(true)); + assertThat(attributes.get(valueKey("stringArray"))) + .isEqualTo(Value.of(Value.of("a"), Value.of("b"))); + assertThat(attributes.get(valueKey("longArray"))) + .isEqualTo(Value.of(Value.of(1L), Value.of(2L))); + assertThat(attributes.get(valueKey("doubleArray"))) + .isEqualTo(Value.of(Value.of(1.1), Value.of(2.2))); + assertThat(attributes.get(valueKey("booleanArray"))) + .isEqualTo(Value.of(Value.of(true), Value.of(false))); + + // Original simple attributes should still be accessible with their original keys + assertThat(attributes.get(stringKey("string"))).isEqualTo("test"); + assertThat(attributes.get(longKey("long"))).isEqualTo(123L); + assertThat(attributes.get(doubleKey("double"))).isEqualTo(3.14); + assertThat(attributes.get(booleanKey("boolean"))).isEqualTo(true); + } } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java index 8ba76a69576..5806e44bf24 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java @@ -19,9 +19,7 @@ *

      "extended" refers an extended set of allowed value types compared to standard {@link * Attributes}. Notably, {@link ExtendedAttributes} values can be of type {@link * ExtendedAttributeType#VALUE}, allowing attributes backed by {@link - * io.opentelemetry.api.common.Value}, and the deprecated {@link - * ExtendedAttributeType#EXTENDED_ATTRIBUTES}, allowing nested {@link ExtendedAttributes} of - * arbitrary depth. + * io.opentelemetry.api.common.Value}. * *

      Where standard {@link Attributes} are accepted everyone that OpenTelemetry represents key / * value pairs, {@link ExtendedAttributes} are only accepted in select places, such as log records diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java index 1e7a4234839..c8a918f6315 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java @@ -93,7 +93,7 @@ default ExtendedAttributesBuilder put(String key, boolean value) { *

      Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and * pre-allocate your keys, if possible. * - * @deprecated Use {@link #put(String, Value)} with {@link Value#of(java.util.Map)} instead. + * @deprecated Use {@link #put(AttributeKey, Value)} with {@link Value#of(java.util.Map)} instead. * @return this Builder */ @Deprecated @@ -102,18 +102,6 @@ default ExtendedAttributesBuilder put(String key, ExtendedAttributes value) return put(ExtendedAttributeKey.extendedAttributesKey(key), value); } - /** - * Puts a {@link Value} attribute into this. - * - *

      Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and - * pre-allocate your keys, if possible. - * - * @return this Builder - */ - default ExtendedAttributesBuilder put(String key, Value value) { - return put(valueKey(key), value); - } - /** * Puts a String array attribute into this. * diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java index 005952b3040..9ff816d7be8 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java @@ -174,6 +174,9 @@ public static ExtendedAttributeKey toExtendedAttributeKey(AttributeKey case DOUBLE_ARRAY: return InternalExtendedAttributeKeyImpl.create( attributeKey.getKey(), ExtendedAttributeType.DOUBLE_ARRAY); + case VALUE: + return InternalExtendedAttributeKeyImpl.create( + attributeKey.getKey(), ExtendedAttributeType.VALUE); } throw new IllegalArgumentException("Unrecognized attributeKey type: " + attributeKey.getType()); } diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java index 80e87d4ad97..1bebb3f397e 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java @@ -11,6 +11,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; import io.opentelemetry.api.incubator.common.ExtendedAttributes; import io.opentelemetry.api.logs.Logger; @@ -108,6 +109,9 @@ private static String flipCoin() { ExtendedAttributeKey mapKey = ExtendedAttributeKey.extendedAttributesKey("acme.map"); + // VALUE key + AttributeKey> valueKey = AttributeKey.valueKey("acme.value"); + @Test @SuppressLogger(ExtendedLogsBridgeApiUsageTest.class) void extendedAttributesUsage() { @@ -126,6 +130,11 @@ void extendedAttributesUsage() { .put( mapKey, ExtendedAttributes.builder().put("childStr", "value").put("childLong", 1L).build()) + .put( + valueKey, + Value.of( + Value.KeyValue.of("childStr", Value.of("value")), + Value.KeyValue.of("childLong", Value.of(1L)))) .build(); // Retrieval @@ -140,6 +149,11 @@ void extendedAttributesUsage() { assertThat(extendedAttributes.get(mapKey)) .isEqualTo( ExtendedAttributes.builder().put("childStr", "value").put("childLong", 1L).build()); + assertThat(extendedAttributes.get(valueKey)) + .isEqualTo( + Value.of( + Value.KeyValue.of("childStr", Value.of("value")), + Value.KeyValue.of("childLong", Value.of(1L)))); // Iteration // Output: @@ -152,6 +166,7 @@ void extendedAttributesUsage() { // acme.map(EXTENDED_ATTRIBUTES): {childLong=1, childStr="value"} // acme.string(STRING): value // acme.string_array(STRING_ARRAY): [value1, value2] + // acme.value(VALUE): [KeyValue{key=childStr, value=StringValue{value=value}}, KeyValue{key=childLong, value=LongValue{value=1}}] extendedAttributes.forEach( (extendedAttributeKey, object) -> logger.info( @@ -185,6 +200,11 @@ void logRecordBuilder_ExtendedAttributes() { .setAttribute( mapKey, ExtendedAttributes.builder().put("childStr", "value").put("childLong", 1L).build()) + .setAttribute( + valueKey, + Value.of( + Value.KeyValue.of("childStr", Value.of("value")), + Value.KeyValue.of("childLong", Value.of(1L)))) .setAllAttributes(Attributes.builder().put("key1", "value").build()) .setAllAttributes(ExtendedAttributes.builder().put("key2", "value").build()) .emit(); @@ -230,6 +250,11 @@ void logRecordBuilder_ExtendedAttributes() { .put("childStr", "value") .put("childLong", 1L) .build()) + .put( + valueKey, + Value.of( + Value.KeyValue.of("childStr", Value.of("value")), + Value.KeyValue.of("childLong", Value.of(1L)))) .put("key1", "value") .put("key2", "value") .build()); diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeKeyValueStatelessMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeKeyValueStatelessMarshaler.java index 3fb1f7c25f6..08dee75822f 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeKeyValueStatelessMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeKeyValueStatelessMarshaler.java @@ -100,6 +100,9 @@ public int getBinarySerializedSize( (List) value, AttributeArrayAnyValueStatelessMarshaler.INSTANCE, context); + case VALUE: + return AnyValueStatelessMarshaler.INSTANCE.getBinarySerializedSize( + (io.opentelemetry.api.common.Value) value, context); } // Error prone ensures the switch statement is complete, otherwise only can happen with // unaligned versions which are not supported. @@ -136,6 +139,10 @@ public void writeTo( AttributeArrayAnyValueStatelessMarshaler.INSTANCE, context); return; + case VALUE: + AnyValueStatelessMarshaler.INSTANCE.writeTo( + output, (io.opentelemetry.api.common.Value) value, context); + return; } // Error prone ensures the switch statement is complete, otherwise only can happen with // unaligned versions which are not supported. diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueMarshaler.java index ec7dd47f10b..8cf231b0c86 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueMarshaler.java @@ -8,6 +8,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.KeyValue; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.internal.InternalAttributeKeyImpl; import io.opentelemetry.exporter.internal.marshal.Marshaler; import io.opentelemetry.exporter.internal.marshal.MarshalerUtil; @@ -118,6 +119,8 @@ private static KeyValueMarshaler create(AttributeKey attributeKey, Object val case DOUBLE_ARRAY: return new KeyValueMarshaler( keyUtf8, ArrayAnyValueMarshaler.createDouble((List) value)); + case VALUE: + return new KeyValueMarshaler(keyUtf8, AnyValueMarshaler.create((Value) value)); } // Error prone ensures the switch statement is complete, otherwise only can happen with // unaligned versions which are not supported. From 7d49b5e5a9b1c1f309012943f5d96ca3362ecd30 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 7 Nov 2025 13:15:37 -0800 Subject: [PATCH 10/28] updates --- .../api/common/ArrayBackedAttributes.java | 46 ++++++++++++++++++- .../api/common/AttributesTest.java | 25 ++++++++++ .../common/ExtendedAttributesBuilder.java | 3 +- .../testing/assertj/AttributeAssertion.java | 2 + .../testing/assertj/LogRecordDataAssert.java | 4 +- 5 files changed, 76 insertions(+), 4 deletions(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java index 1d7ddbe9491..0798fbb638d 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java @@ -7,6 +7,7 @@ import io.opentelemetry.api.internal.ImmutableKeyValuePairs; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.List; import javax.annotation.Nullable; @@ -46,12 +47,55 @@ public AttributesBuilder toBuilder() { @Override @Nullable public T get(AttributeKey key) { - if (key != null && key.getType() == AttributeType.VALUE) { + if (key == null) { + return null; + } + if (key.getType() == AttributeType.VALUE) { return (T) getAsValue(key.getKey()); } + // Check if we're looking for an array type but have a VALUE with empty array + if (isArrayType(key.getType())) { + T value = (T) super.get(key); + if (value == null) { + // Check if there's a VALUE with the same key that contains an empty array + Value valueAttr = getValueAttribute(key.getKey()); + if (valueAttr != null && isEmptyArray(valueAttr)) { + return (T) Collections.emptyList(); + } + } + return value; + } return (T) super.get(key); } + private static boolean isArrayType(AttributeType type) { + return type == AttributeType.STRING_ARRAY + || type == AttributeType.LONG_ARRAY + || type == AttributeType.DOUBLE_ARRAY + || type == AttributeType.BOOLEAN_ARRAY; + } + + @Nullable + private Value getValueAttribute(String keyName) { + List data = data(); + for (int i = 0; i < data.size(); i += 2) { + AttributeKey currentKey = (AttributeKey) data.get(i); + if (currentKey.getKey().equals(keyName) && currentKey.getType() == AttributeType.VALUE) { + return (Value) data.get(i + 1); + } + } + return null; + } + + private static boolean isEmptyArray(Value value) { + if (value.getType() != ValueType.ARRAY) { + return false; + } + @SuppressWarnings("unchecked") + List> arrayValues = (List>) value.getValue(); + return arrayValues.isEmpty(); + } + @Nullable private Value getAsValue(String keyName) { // Find any attribute with the same key name and convert it to Value diff --git a/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java b/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java index 21c7c3a6b28..deed3a2076d 100644 --- a/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java @@ -808,4 +808,29 @@ void simpleAttributeRetrievedAsComplexValue() { assertThat(attributes.get(doubleKey("double"))).isEqualTo(3.14); assertThat(attributes.get(booleanKey("boolean"))).isEqualTo(true); } + + @Test + void emptyValueArrayRetrievedAsAnyArrayType() { + // When putting an empty VALUE array, it should be retrievable as any array type + Attributes attributes = + Attributes.builder().put(valueKey("key"), Value.of(Collections.emptyList())).build(); + + // Should be able to retrieve as any array type and get an empty list + assertThat(attributes.get(stringArrayKey("key"))).isEqualTo(Collections.emptyList()); + assertThat(attributes.get(longArrayKey("key"))).isEqualTo(Collections.emptyList()); + assertThat(attributes.get(doubleArrayKey("key"))).isEqualTo(Collections.emptyList()); + assertThat(attributes.get(booleanArrayKey("key"))).isEqualTo(Collections.emptyList()); + + // Should also be retrievable as VALUE + assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(Collections.emptyList())); + + // forEach should show VALUE type (since empty arrays are stored as VALUE) + Map, Object> entriesSeen = new LinkedHashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(valueKey("key"), Value.of(Collections.emptyList()))); + + // asMap should show VALUE type + assertThat(attributes.asMap()) + .containsExactly(entry(valueKey("key"), Value.of(Collections.emptyList()))); + } } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java index c8a918f6315..159e14cf329 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java @@ -14,7 +14,6 @@ import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.longKey; import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.stringArrayKey; import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.stringKey; -import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.valueKey; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; @@ -93,7 +92,7 @@ default ExtendedAttributesBuilder put(String key, boolean value) { *

      Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and * pre-allocate your keys, if possible. * - * @deprecated Use {@link #put(AttributeKey, Value)} with {@link Value#of(java.util.Map)} instead. + * @deprecated Use {@link #put(ExtendedAttributeKey, Object)} with {@link Value#of(java.util.Map)} instead. * @return this Builder */ @Deprecated diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributeAssertion.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributeAssertion.java index 971ff9446ff..c86b542ce85 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributeAssertion.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributeAssertion.java @@ -58,6 +58,8 @@ static AttributeAssertion create( case LONG_ARRAY: case DOUBLE_ARRAY: return assertThat((List) value); + case VALUE: + return assertThat(value); } throw new IllegalArgumentException("Unknown type for key " + key); } diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LogRecordDataAssert.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LogRecordDataAssert.java index d2f621629d7..4dd9a3940ca 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LogRecordDataAssert.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LogRecordDataAssert.java @@ -344,8 +344,10 @@ public LogRecordDataAssert hasBodyField(AttributeKey key, T value) { return hasBodyField( key.getKey(), Value.of(((List) value).stream().map(Value::of).collect(toList()))); + case VALUE: + return hasBodyField(key.getKey(), (Value) value); } - return this; + throw new IllegalArgumentException("Unknown type for key " + key); } /** Asserts the log has the given attributes. */ From d3f9dbe3dc3bd1cbd994a6607e7259a1e046a066 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 7 Nov 2025 14:16:34 -0800 Subject: [PATCH 11/28] javadoc --- .../api/common/ArrayBackedAttributes.java | 110 -------- .../common/ArrayBackedAttributesBuilder.java | 120 --------- .../api/common/AttributeKey.java | 5 - .../api/common/AttributeType.java | 12 +- .../opentelemetry/api/common/Attributes.java | 57 +--- .../api/common/AttributesBuilder.java | 34 --- .../api/common/AttributesTest.java | 243 ------------------ .../common/ArrayBackedExtendedAttributes.java | 116 +++++++++ .../ArrayBackedExtendedAttributesBuilder.java | 126 +++++++++ .../common/ExtendedAttributeType.java | 11 + .../incubator/common/ExtendedAttributes.java | 62 ++++- .../common/ExtendedAttributesBuilder.java | 43 +++- .../InternalExtendedAttributeKeyImpl.java | 3 - .../common/ExtendedAttributesTest.java | 125 ++++++++- .../logs/ExtendedLogsBridgeApiUsageTest.java | 21 +- .../AttributeKeyValueStatelessMarshaler.java | 7 - .../internal/otlp/KeyValueMarshaler.java | 3 - .../testing/assertj/AttributeAssertion.java | 2 - .../testing/assertj/LogRecordDataAssert.java | 4 +- 19 files changed, 488 insertions(+), 616 deletions(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java index 0798fbb638d..165c9ffe2e2 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java @@ -7,9 +7,7 @@ import io.opentelemetry.api.internal.ImmutableKeyValuePairs; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; -import java.util.List; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -47,117 +45,9 @@ public AttributesBuilder toBuilder() { @Override @Nullable public T get(AttributeKey key) { - if (key == null) { - return null; - } - if (key.getType() == AttributeType.VALUE) { - return (T) getAsValue(key.getKey()); - } - // Check if we're looking for an array type but have a VALUE with empty array - if (isArrayType(key.getType())) { - T value = (T) super.get(key); - if (value == null) { - // Check if there's a VALUE with the same key that contains an empty array - Value valueAttr = getValueAttribute(key.getKey()); - if (valueAttr != null && isEmptyArray(valueAttr)) { - return (T) Collections.emptyList(); - } - } - return value; - } return (T) super.get(key); } - private static boolean isArrayType(AttributeType type) { - return type == AttributeType.STRING_ARRAY - || type == AttributeType.LONG_ARRAY - || type == AttributeType.DOUBLE_ARRAY - || type == AttributeType.BOOLEAN_ARRAY; - } - - @Nullable - private Value getValueAttribute(String keyName) { - List data = data(); - for (int i = 0; i < data.size(); i += 2) { - AttributeKey currentKey = (AttributeKey) data.get(i); - if (currentKey.getKey().equals(keyName) && currentKey.getType() == AttributeType.VALUE) { - return (Value) data.get(i + 1); - } - } - return null; - } - - private static boolean isEmptyArray(Value value) { - if (value.getType() != ValueType.ARRAY) { - return false; - } - @SuppressWarnings("unchecked") - List> arrayValues = (List>) value.getValue(); - return arrayValues.isEmpty(); - } - - @Nullable - private Value getAsValue(String keyName) { - // Find any attribute with the same key name and convert it to Value - List data = data(); - for (int i = 0; i < data.size(); i += 2) { - AttributeKey currentKey = (AttributeKey) data.get(i); - if (currentKey.getKey().equals(keyName)) { - Object value = data.get(i + 1); - return asValue(currentKey.getType(), value); - } - } - return null; - } - - @SuppressWarnings("unchecked") - @Nullable - private static Value asValue(AttributeType type, Object value) { - switch (type) { - case STRING: - return Value.of((String) value); - case LONG: - return Value.of((Long) value); - case DOUBLE: - return Value.of((Double) value); - case BOOLEAN: - return Value.of((Boolean) value); - case STRING_ARRAY: - List stringList = (List) value; - Value[] stringValues = new Value[stringList.size()]; - for (int i = 0; i < stringList.size(); i++) { - stringValues[i] = Value.of(stringList.get(i)); - } - return Value.of(stringValues); - case LONG_ARRAY: - List longList = (List) value; - Value[] longValues = new Value[longList.size()]; - for (int i = 0; i < longList.size(); i++) { - longValues[i] = Value.of(longList.get(i)); - } - return Value.of(longValues); - case DOUBLE_ARRAY: - List doubleList = (List) value; - Value[] doubleValues = new Value[doubleList.size()]; - for (int i = 0; i < doubleList.size(); i++) { - doubleValues[i] = Value.of(doubleList.get(i)); - } - return Value.of(doubleValues); - case BOOLEAN_ARRAY: - List booleanList = (List) value; - Value[] booleanValues = new Value[booleanList.size()]; - for (int i = 0; i < booleanList.size(); i++) { - booleanValues[i] = Value.of(booleanList.get(i)); - } - return Value.of(booleanValues); - case VALUE: - // Already a Value - return (Value) value; - } - // Should not reach here - return null; - } - static Attributes sortAndFilterToAttributes(Object... data) { // null out any empty keys or keys with null values // so they will then be removed by the sortAndFilter method. diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java index 4ec0822ca55..1f39cc16fa7 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java @@ -5,15 +5,6 @@ package io.opentelemetry.api.common; -import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; -import static io.opentelemetry.api.common.AttributeKey.booleanKey; -import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; -import static io.opentelemetry.api.common.AttributeKey.doubleKey; -import static io.opentelemetry.api.common.AttributeKey.longArrayKey; -import static io.opentelemetry.api.common.AttributeKey.longKey; -import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; -import static io.opentelemetry.api.common.AttributeKey.stringKey; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -52,122 +43,11 @@ public AttributesBuilder put(AttributeKey key, @Nullable T value) { if (key == null || key.getKey().isEmpty() || value == null) { return this; } - if (key.getType() == AttributeType.VALUE && value instanceof Value) { - putValue(key, (Value) value); - return this; - } data.add(key); data.add(value); return this; } - private void putValue(AttributeKey key, Value valueObj) { - // Convert VALUE type to narrower type when possible - String keyName = key.getKey(); - switch (valueObj.getType()) { - case STRING: - data.add(stringKey(keyName)); - data.add(valueObj.getValue()); - return; - case LONG: - data.add(longKey(keyName)); - data.add(valueObj.getValue()); - return; - case DOUBLE: - data.add(doubleKey(keyName)); - data.add(valueObj.getValue()); - return; - case BOOLEAN: - data.add(booleanKey(keyName)); - data.add(valueObj.getValue()); - return; - case ARRAY: - @SuppressWarnings("unchecked") - List> arrayValues = (List>) valueObj.getValue(); - AttributeType attributeType = attributeType(arrayValues); - switch (attributeType) { - case STRING_ARRAY: - List strings = new ArrayList<>(); - for (Value v : arrayValues) { - strings.add((String) v.getValue()); - } - data.add(stringArrayKey(keyName)); - data.add(strings); - return; - case LONG_ARRAY: - List longs = new ArrayList<>(); - for (Value v : arrayValues) { - longs.add((Long) v.getValue()); - } - data.add(longArrayKey(keyName)); - data.add(longs); - return; - case DOUBLE_ARRAY: - List doubles = new ArrayList<>(); - for (Value v : arrayValues) { - doubles.add((Double) v.getValue()); - } - data.add(doubleArrayKey(keyName)); - data.add(doubles); - return; - case BOOLEAN_ARRAY: - List booleans = new ArrayList<>(); - for (Value v : arrayValues) { - booleans.add((Boolean) v.getValue()); - } - data.add(booleanArrayKey(keyName)); - data.add(booleans); - return; - case VALUE: - // Not coercible (empty, non-homogeneous, or unsupported element type) - // TODO when empty, retrieve it when asked for any kind of array? - data.add(key); - data.add(valueObj); - return; - default: - throw new IllegalArgumentException("Unexpected array attribute type: " + attributeType); - } - case KEY_VALUE_LIST: - case BYTES: - // Keep as VALUE type - data.add(key); - data.add(valueObj); - return; - } - } - - /** - * Returns the AttributeType for a homogeneous array (STRING_ARRAY, LONG_ARRAY, DOUBLE_ARRAY, or - * BOOLEAN_ARRAY), or VALUE if the array is empty, non-homogeneous, or contains unsupported - * element types. - */ - private static AttributeType attributeType(List> arrayValues) { - if (arrayValues.isEmpty()) { - return AttributeType.VALUE; - } - ValueType elementType = arrayValues.get(0).getType(); - for (Value v : arrayValues) { - if (v.getType() != elementType) { - return AttributeType.VALUE; - } - } - switch (elementType) { - case STRING: - return AttributeType.STRING_ARRAY; - case LONG: - return AttributeType.LONG_ARRAY; - case DOUBLE: - return AttributeType.DOUBLE_ARRAY; - case BOOLEAN: - return AttributeType.BOOLEAN_ARRAY; - case ARRAY: - case KEY_VALUE_LIST: - case BYTES: - return AttributeType.VALUE; - } - throw new IllegalArgumentException("Unsupported element type: " + elementType); - } - @Override @SuppressWarnings({"unchecked", "rawtypes"}) public AttributesBuilder putAll(Attributes attributes) { diff --git a/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java b/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java index 64b101975ab..7d012aa14ca 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java @@ -70,9 +70,4 @@ static AttributeKey> longArrayKey(String key) { static AttributeKey> doubleArrayKey(String key) { return InternalAttributeKeyImpl.create(key, AttributeType.DOUBLE_ARRAY); } - - /** Returns a new AttributeKey for generic {@link Value} valued attributes. */ - static AttributeKey> valueKey(String key) { - return InternalAttributeKeyImpl.create(key, AttributeType.VALUE); - } } diff --git a/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java b/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java index 6209301f573..1c51e36d644 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java @@ -17,15 +17,5 @@ public enum AttributeType { STRING_ARRAY, BOOLEAN_ARRAY, LONG_ARRAY, - DOUBLE_ARRAY, - /** - * Simple attributes ({@link AttributeType#STRING}, {@link AttributeType#LONG}, {@link - * AttributeType#DOUBLE}, {@link AttributeType#BOOLEAN}, {@link AttributeType#STRING_ARRAY}, - * {@link AttributeType#LONG_ARRAY}, {@link AttributeType#DOUBLE_ARRAY}, {@link - * AttributeType#BOOLEAN_ARRAY}) SHOULD be used whenever possible. Instrumentations SHOULD assume - * that backends do not index individual properties of complex attributes, that querying or - * aggregating on such properties is inefficient and complicated, and that reporting complex - * attributes carries higher performance overhead. - */ - VALUE + DOUBLE_ARRAY } diff --git a/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java b/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java index 8ac46146e6b..2a9d43793a7 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java @@ -33,57 +33,11 @@ @Immutable public interface Attributes { - /** - * Returns the value for the given {@link AttributeKey}, or {@code null} if not found. - * - *

      Note: this method will automatically return the corresponding {@link Value} instance when - * passed a key of type {@link AttributeType#VALUE} and a simple attribute is found. This is the - * inverse of {@link AttributesBuilder#put(AttributeKey, Object)} when the key is {@link - * AttributeType#VALUE}. - * - *

        - *
      • If {@code put(AttributeKey.stringKey("key"), "a")} was called, then {@code - * get(AttributeKey.valueKey("key"))} returns {@code Value.of("a")}. - *
      • If {@code put(AttributeKey.longKey("key"), 1L)} was called, then {@code - * get(AttributeKey.valueKey("key"))} returns {@code Value.of(1L)}. - *
      • If {@code put(AttributeKey.doubleKey("key"), 1.0)} was called, then {@code - * get(AttributeKey.valueKey("key"))} returns {@code Value.of(1.0)}. - *
      • If {@code put(AttributeKey.booleanKey("key"), true)} was called, then {@code - * get(AttributeKey.valueKey("key"))} returns {@code Value.of(true)}. - *
      • If {@code put(AttributeKey.stringArrayKey("key"), Arrays.asList("a", "b"))} was called, - * then {@code get(AttributeKey.valueKey("key"))} returns {@code Value.of(Value.of("a"), - * Value.of("b"))}. - *
      • If {@code put(AttributeKey.longArrayKey("key"), Arrays.asList(1L, 2L))} was called, then - * {@code get(AttributeKey.valueKey("key"))} returns {@code Value.of(Value.of(1L), - * Value.of(2L))}. - *
      • If {@code put(AttributeKey.doubleArrayKey("key"), Arrays.asList(1.0, 2.0))} was called, - * then {@code get(AttributeKey.valueKey("key"))} returns {@code Value.of(Value.of(1.0), - * Value.of(2.0))}. - *
      • If {@code put(AttributeKey.booleanArrayKey("key"), Arrays.asList(true, false))} was - * called, then {@code get(AttributeKey.valueKey("key"))} returns {@code - * Value.of(Value.of(true), Value.of(false))}. - *
      - * - * Further, if {@code put(AttributeKey.valueKey("key"), Value.of(emptyList()))} was called, then - * - *
        - *
      • {@code get(AttributeKey.stringArrayKey("key"))} - *
      • {@code get(AttributeKey.longArrayKey("key"))} - *
      • {@code get(AttributeKey.booleanArrayKey("key"))} - *
      • {@code get(AttributeKey.doubleArrayKey("key"))} - *
      - * - * all return an empty list (as opposed to {@code null}). - */ + /** Returns the value for the given {@link AttributeKey}, or {@code null} if not found. */ @Nullable T get(AttributeKey key); - /** - * Iterates over all the key-value pairs of attributes contained by this instance. - * - *

      Note: {@link AttributeType#VALUE} attributes will be represented as simple attributes if - * possible. See {@link AttributesBuilder#put(AttributeKey, Object)} for more details. - */ + /** Iterates over all the key-value pairs of attributes contained by this instance. */ void forEach(BiConsumer, ? super Object> consumer); /** The number of attributes contained in this. */ @@ -92,12 +46,7 @@ public interface Attributes { /** Whether there are any attributes contained in this. */ boolean isEmpty(); - /** - * Returns a read-only view of this {@link Attributes} as a {@link Map}. - * - *

      Note: {@link AttributeType#VALUE} attributes will be represented as simple attributes in - * this map if possible. See {@link AttributesBuilder#put(AttributeKey, Object)} for more details. - */ + /** Returns a read-only view of this {@link Attributes} as a {@link Map}. */ Map, Object> asMap(); /** Returns a {@link Attributes} instance with no attributes. */ diff --git a/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java b/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java index bc246ad3e8b..6623d470137 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java @@ -39,40 +39,6 @@ public interface AttributesBuilder { /** * Puts an {@link AttributeKey} with an associated value into this if the value is non-null. * Providing a null value does not remove or unset previously set values. - * - *

      Simple attributes ({@link AttributeType#STRING}, {@link AttributeType#LONG}, {@link - * AttributeType#DOUBLE}, {@link AttributeType#BOOLEAN}, {@link AttributeType#STRING_ARRAY}, - * {@link AttributeType#LONG_ARRAY}, {@link AttributeType#DOUBLE_ARRAY}, {@link - * AttributeType#BOOLEAN_ARRAY}) SHOULD be used whenever possible. Instrumentations SHOULD assume - * that backends do not index individual properties of complex attributes, that querying or - * aggregating on such properties is inefficient and complicated, and that reporting complex - * attributes carries higher performance overhead. - * - *

      Note: This method will automatically convert complex attributes ({@link - * AttributeType#VALUE}) to simple attributes when possible. - * - *

        - *
      • Calling {@code put(AttributeKey.valueKey("key"), Value.of("a"))} is equivalent to calling - * {@code put(AttributeKey.stringKey("key"), "a")}. - *
      • Calling {@code put(AttributeKey.valueKey("key"), Value.of(1L))} is equivalent to calling - * {@code put(AttributeKey.longKey("key"), 1L)}. - *
      • Calling {@code put(AttributeKey.valueKey("key"), Value.of(1.0))} is equivalent to calling - * {@code put(AttributeKey.doubleKey("key"), 1.0)}. - *
      • Calling {@code put(AttributeKey.valueKey("key"), Value.of(true))} is equivalent to - * calling {@code put(AttributeKey.booleanKey("key"), true)}. - *
      • Calling {@code put(AttributeKey.valueKey("key"), Value.of(Value.of("a"), Value.of("b")))} - * is equivalent to calling {@code put(AttributeKey.stringArrayKey("key"), - * Arrays.asList("a", "b"))}. - *
      • Calling {@code put(AttributeKey.valueKey("key"), Value.of(Value.of(1L), Value.of(2L)))} - * is equivalent to calling {@code put(AttributeKey.longArrayKey("key"), Arrays.asList(1L, - * 2L))}. - *
      • Calling {@code put(AttributeKey.valueKey("key"), Value.of(Value.of(1.0), Value.of(2.0)))} - * is equivalent to calling {@code put(AttributeKey.doubleArrayKey("key"), - * Arrays.asList(1.0, 2.0))}. - *
      • Calling {@code put(AttributeKey.valueKey("key"), Value.of(Value.of(true), - * Value.of(false)))} is equivalent to calling {@code - * put(AttributeKey.booleanArrayKey("key"), Arrays.asList(true, false))}. - *
      */ AttributesBuilder put(AttributeKey key, @Nullable T value); diff --git a/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java b/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java index deed3a2076d..dcc6f701e5a 100644 --- a/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java @@ -13,7 +13,6 @@ import static io.opentelemetry.api.common.AttributeKey.longKey; import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; -import static io.opentelemetry.api.common.AttributeKey.valueKey; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -591,246 +590,4 @@ void emptyValueIsValid() { Attributes attributes = Attributes.of(key, ""); assertThat(attributes.get(key)).isEqualTo(""); } - - @Test - void complexValueStoredAsString() { - // When putting a VALUE attribute with a string Value, it should be stored as STRING type - Attributes attributes = - Attributes.builder() - .put(valueKey("key"), Value.of("test")) - .build(); - - // Should be stored as STRING type internally - assertThat(attributes.get(stringKey("key"))).isEqualTo("test"); - assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of("test")); - - // forEach should show STRING type - Map, Object> entriesSeen = new LinkedHashMap<>(); - attributes.forEach(entriesSeen::put); - assertThat(entriesSeen).containsExactly(entry(stringKey("key"), "test")); - - // asMap should show STRING type - assertThat(attributes.asMap()).containsExactly(entry(stringKey("key"), "test")); - } - - @Test - void complexValueStoredAsLong() { - // When putting a VALUE attribute with a long Value, it should be stored as LONG type - Attributes attributes = - Attributes.builder() - .put(valueKey("key"), Value.of(123L)) - .build(); - - // Should be stored as LONG type internally - assertThat(attributes.get(longKey("key"))).isEqualTo(123L); - assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(123L)); - - // forEach should show LONG type - Map, Object> entriesSeen = new LinkedHashMap<>(); - attributes.forEach(entriesSeen::put); - assertThat(entriesSeen).containsExactly(entry(longKey("key"), 123L)); - - // asMap should show LONG type - assertThat(attributes.asMap()).containsExactly(entry(longKey("key"), 123L)); - } - - @Test - void complexValueStoredAsDouble() { - // When putting a VALUE attribute with a double Value, it should be stored as DOUBLE type - Attributes attributes = - Attributes.builder() - .put(valueKey("key"), Value.of(3.14)) - .build(); - - // Should be stored as DOUBLE type internally - assertThat(attributes.get(doubleKey("key"))).isEqualTo(3.14); - assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(3.14)); - - // forEach should show DOUBLE type - Map, Object> entriesSeen = new LinkedHashMap<>(); - attributes.forEach(entriesSeen::put); - assertThat(entriesSeen).containsExactly(entry(doubleKey("key"), 3.14)); - - // asMap should show DOUBLE type - assertThat(attributes.asMap()).containsExactly(entry(doubleKey("key"), 3.14)); - } - - @Test - void complexValueStoredAsBoolean() { - // When putting a VALUE attribute with a boolean Value, it should be stored as BOOLEAN type - Attributes attributes = - Attributes.builder() - .put(valueKey("key"), Value.of(true)) - .build(); - - // Should be stored as BOOLEAN type internally - assertThat(attributes.get(booleanKey("key"))).isEqualTo(true); - assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(true)); - - // forEach should show BOOLEAN type - Map, Object> entriesSeen = new LinkedHashMap<>(); - attributes.forEach(entriesSeen::put); - assertThat(entriesSeen).containsExactly(entry(booleanKey("key"), true)); - - // asMap should show BOOLEAN type - assertThat(attributes.asMap()).containsExactly(entry(booleanKey("key"), true)); - } - - @Test - void complexValueStoredAsStringArray() { - // When putting a VALUE attribute with a homogeneous string array, it should be stored as - // STRING_ARRAY type - Attributes attributes = - Attributes.builder() - .put(valueKey("key"), Value.of(Value.of("a"), Value.of("b"))) - .build(); - - // Should be stored as STRING_ARRAY type internally - assertThat(attributes.get(stringArrayKey("key"))).isEqualTo(Arrays.asList("a", "b")); - assertThat(attributes.get(valueKey("key"))) - .isEqualTo(Value.of(Value.of("a"), Value.of("b"))); - - // forEach should show STRING_ARRAY type - Map, Object> entriesSeen = new LinkedHashMap<>(); - attributes.forEach(entriesSeen::put); - assertThat(entriesSeen).containsExactly(entry(stringArrayKey("key"), Arrays.asList("a", "b"))); - - // asMap should show STRING_ARRAY type - assertThat(attributes.asMap()) - .containsExactly(entry(stringArrayKey("key"), Arrays.asList("a", "b"))); - } - - @Test - void complexValueStoredAsLongArray() { - // When putting a VALUE attribute with a homogeneous long array, it should be stored as - // LONG_ARRAY type - Attributes attributes = - Attributes.builder() - .put(valueKey("key"), Value.of(Value.of(1L), Value.of(2L))) - .build(); - - // Should be stored as LONG_ARRAY type internally - assertThat(attributes.get(longArrayKey("key"))).isEqualTo(Arrays.asList(1L, 2L)); - assertThat(attributes.get(valueKey("key"))) - .isEqualTo(Value.of(Value.of(1L), Value.of(2L))); - - // forEach should show LONG_ARRAY type - Map, Object> entriesSeen = new LinkedHashMap<>(); - attributes.forEach(entriesSeen::put); - assertThat(entriesSeen).containsExactly(entry(longArrayKey("key"), Arrays.asList(1L, 2L))); - - // asMap should show LONG_ARRAY type - assertThat(attributes.asMap()) - .containsExactly(entry(longArrayKey("key"), Arrays.asList(1L, 2L))); - } - - @Test - void complexValueStoredAsDoubleArray() { - // When putting a VALUE attribute with a homogeneous double array, it should be stored as - // DOUBLE_ARRAY type - Attributes attributes = - Attributes.builder() - .put(valueKey("key"), Value.of(Value.of(1.1), Value.of(2.2))) - .build(); - - // Should be stored as DOUBLE_ARRAY type internally - assertThat(attributes.get(doubleArrayKey("key"))).isEqualTo(Arrays.asList(1.1, 2.2)); - assertThat(attributes.get(valueKey("key"))) - .isEqualTo(Value.of(Value.of(1.1), Value.of(2.2))); - - // forEach should show DOUBLE_ARRAY type - Map, Object> entriesSeen = new LinkedHashMap<>(); - attributes.forEach(entriesSeen::put); - assertThat(entriesSeen).containsExactly(entry(doubleArrayKey("key"), Arrays.asList(1.1, 2.2))); - - // asMap should show DOUBLE_ARRAY type - assertThat(attributes.asMap()) - .containsExactly(entry(doubleArrayKey("key"), Arrays.asList(1.1, 2.2))); - } - - @Test - void complexValueStoredAsBooleanArray() { - // When putting a VALUE attribute with a homogeneous boolean array, it should be stored as - // BOOLEAN_ARRAY type - Attributes attributes = - Attributes.builder() - .put(valueKey("key"), Value.of(Value.of(true), Value.of(false))) - .build(); - - // Should be stored as BOOLEAN_ARRAY type internally - assertThat(attributes.get(booleanArrayKey("key"))).isEqualTo(Arrays.asList(true, false)); - assertThat(attributes.get(valueKey("key"))) - .isEqualTo(Value.of(Value.of(true), Value.of(false))); - - // forEach should show BOOLEAN_ARRAY type - Map, Object> entriesSeen = new LinkedHashMap<>(); - attributes.forEach(entriesSeen::put); - assertThat(entriesSeen) - .containsExactly(entry(booleanArrayKey("key"), Arrays.asList(true, false))); - - // asMap should show BOOLEAN_ARRAY type - assertThat(attributes.asMap()) - .containsExactly(entry(booleanArrayKey("key"), Arrays.asList(true, false))); - } - - @Test - void simpleAttributeRetrievedAsComplexValue() { - // When getting with a VALUE key, simple attributes should be converted to Value - Attributes attributes = - Attributes.builder() - .put(stringKey("string"), "test") - .put(longKey("long"), 123L) - .put(doubleKey("double"), 3.14) - .put(booleanKey("boolean"), true) - .put(stringArrayKey("stringArray"), "a", "b") - .put(longArrayKey("longArray"), 1L, 2L) - .put(doubleArrayKey("doubleArray"), 1.1, 2.2) - .put(booleanArrayKey("booleanArray"), true, false) - .build(); - - // Should be able to get simple attributes as Value - assertThat(attributes.get(valueKey("string"))).isEqualTo(Value.of("test")); - assertThat(attributes.get(valueKey("long"))).isEqualTo(Value.of(123L)); - assertThat(attributes.get(valueKey("double"))).isEqualTo(Value.of(3.14)); - assertThat(attributes.get(valueKey("boolean"))).isEqualTo(Value.of(true)); - assertThat(attributes.get(valueKey("stringArray"))) - .isEqualTo(Value.of(Value.of("a"), Value.of("b"))); - assertThat(attributes.get(valueKey("longArray"))) - .isEqualTo(Value.of(Value.of(1L), Value.of(2L))); - assertThat(attributes.get(valueKey("doubleArray"))) - .isEqualTo(Value.of(Value.of(1.1), Value.of(2.2))); - assertThat(attributes.get(valueKey("booleanArray"))) - .isEqualTo(Value.of(Value.of(true), Value.of(false))); - - // Original simple attributes should still be accessible with their original keys - assertThat(attributes.get(stringKey("string"))).isEqualTo("test"); - assertThat(attributes.get(longKey("long"))).isEqualTo(123L); - assertThat(attributes.get(doubleKey("double"))).isEqualTo(3.14); - assertThat(attributes.get(booleanKey("boolean"))).isEqualTo(true); - } - - @Test - void emptyValueArrayRetrievedAsAnyArrayType() { - // When putting an empty VALUE array, it should be retrievable as any array type - Attributes attributes = - Attributes.builder().put(valueKey("key"), Value.of(Collections.emptyList())).build(); - - // Should be able to retrieve as any array type and get an empty list - assertThat(attributes.get(stringArrayKey("key"))).isEqualTo(Collections.emptyList()); - assertThat(attributes.get(longArrayKey("key"))).isEqualTo(Collections.emptyList()); - assertThat(attributes.get(doubleArrayKey("key"))).isEqualTo(Collections.emptyList()); - assertThat(attributes.get(booleanArrayKey("key"))).isEqualTo(Collections.emptyList()); - - // Should also be retrievable as VALUE - assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(Collections.emptyList())); - - // forEach should show VALUE type (since empty arrays are stored as VALUE) - Map, Object> entriesSeen = new LinkedHashMap<>(); - attributes.forEach(entriesSeen::put); - assertThat(entriesSeen).containsExactly(entry(valueKey("key"), Value.of(Collections.emptyList()))); - - // asMap should show VALUE type - assertThat(attributes.asMap()) - .containsExactly(entry(valueKey("key"), Value.of(Collections.emptyList()))); - } } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributes.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributes.java index a864a1d9585..fed39323f98 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributes.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributes.java @@ -8,9 +8,13 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.common.ValueType; import io.opentelemetry.api.internal.ImmutableKeyValuePairs; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; +import java.util.List; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -51,9 +55,121 @@ public ExtendedAttributesBuilder toBuilder() { @Override @Nullable public T get(ExtendedAttributeKey key) { + if (key == null) { + return null; + } + if (key.getType() == ExtendedAttributeType.VALUE) { + return (T) getAsValue(key.getKey()); + } + // Check if we're looking for an array type but have a VALUE with empty array + if (isArrayType(key.getType())) { + T value = (T) super.get(key); + if (value == null) { + // Check if there's a VALUE with the same key that contains an empty array + Value valueAttr = getValueAttribute(key.getKey()); + if (valueAttr != null && isEmptyArray(valueAttr)) { + return (T) Collections.emptyList(); + } + } + return value; + } return (T) super.get(key); } + private static boolean isArrayType(ExtendedAttributeType type) { + return type == ExtendedAttributeType.STRING_ARRAY + || type == ExtendedAttributeType.LONG_ARRAY + || type == ExtendedAttributeType.DOUBLE_ARRAY + || type == ExtendedAttributeType.BOOLEAN_ARRAY; + } + + @Nullable + private Value getValueAttribute(String keyName) { + List data = data(); + for (int i = 0; i < data.size(); i += 2) { + ExtendedAttributeKey currentKey = (ExtendedAttributeKey) data.get(i); + if (currentKey.getKey().equals(keyName) + && currentKey.getType() == ExtendedAttributeType.VALUE) { + return (Value) data.get(i + 1); + } + } + return null; + } + + private static boolean isEmptyArray(Value value) { + if (value.getType() != ValueType.ARRAY) { + return false; + } + @SuppressWarnings("unchecked") + List> arrayValues = (List>) value.getValue(); + return arrayValues.isEmpty(); + } + + @Nullable + private Value getAsValue(String keyName) { + // Find any attribute with the same key name and convert it to Value + List data = data(); + for (int i = 0; i < data.size(); i += 2) { + ExtendedAttributeKey currentKey = (ExtendedAttributeKey) data.get(i); + if (currentKey.getKey().equals(keyName)) { + Object value = data.get(i + 1); + return asValue(currentKey.getType(), value); + } + } + return null; + } + + @SuppressWarnings("unchecked") + @Nullable + private static Value asValue(ExtendedAttributeType type, Object value) { + switch (type) { + case STRING: + return Value.of((String) value); + case LONG: + return Value.of((Long) value); + case DOUBLE: + return Value.of((Double) value); + case BOOLEAN: + return Value.of((Boolean) value); + case STRING_ARRAY: + List stringList = (List) value; + Value[] stringValues = new Value[stringList.size()]; + for (int i = 0; i < stringList.size(); i++) { + stringValues[i] = Value.of(stringList.get(i)); + } + return Value.of(stringValues); + case LONG_ARRAY: + List longList = (List) value; + Value[] longValues = new Value[longList.size()]; + for (int i = 0; i < longList.size(); i++) { + longValues[i] = Value.of(longList.get(i)); + } + return Value.of(longValues); + case DOUBLE_ARRAY: + List doubleList = (List) value; + Value[] doubleValues = new Value[doubleList.size()]; + for (int i = 0; i < doubleList.size(); i++) { + doubleValues[i] = Value.of(doubleList.get(i)); + } + return Value.of(doubleValues); + case BOOLEAN_ARRAY: + List booleanList = (List) value; + Value[] booleanValues = new Value[booleanList.size()]; + for (int i = 0; i < booleanList.size(); i++) { + booleanValues[i] = Value.of(booleanList.get(i)); + } + return Value.of(booleanValues); + case VALUE: + // Already a Value + return (Value) value; + case EXTENDED_ATTRIBUTES: + // Cannot convert EXTENDED_ATTRIBUTES to Value + return null; + } + // Should not reach here + return null; + } + @SuppressWarnings("unchecked") @Override public Attributes asAttributes() { diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java index 891707c61af..e27ab81b83c 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java @@ -5,6 +5,17 @@ package io.opentelemetry.api.incubator.common; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.booleanArrayKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.booleanKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.doubleArrayKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.doubleKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.longArrayKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.longKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.stringArrayKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.stringKey; + +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.common.ValueType; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -36,11 +47,126 @@ public ExtendedAttributesBuilder put(ExtendedAttributeKey key, T value) { if (key == null || key.getKey().isEmpty() || value == null) { return this; } + if (key.getType() == ExtendedAttributeType.VALUE && value instanceof Value) { + putValue(key, (Value) value); + return this; + } data.add(key); data.add(value); return this; } + private void putValue(ExtendedAttributeKey key, Value valueObj) { + // Convert VALUE type to narrower type when possible + String keyName = key.getKey(); + switch (valueObj.getType()) { + case STRING: + data.add(stringKey(keyName)); + data.add(valueObj.getValue()); + return; + case LONG: + data.add(longKey(keyName)); + data.add(valueObj.getValue()); + return; + case DOUBLE: + data.add(doubleKey(keyName)); + data.add(valueObj.getValue()); + return; + case BOOLEAN: + data.add(booleanKey(keyName)); + data.add(valueObj.getValue()); + return; + case ARRAY: + @SuppressWarnings("unchecked") + List> arrayValues = (List>) valueObj.getValue(); + ExtendedAttributeType attributeType = attributeType(arrayValues); + switch (attributeType) { + case STRING_ARRAY: + List strings = new ArrayList<>(); + for (Value v : arrayValues) { + strings.add((String) v.getValue()); + } + data.add(stringArrayKey(keyName)); + data.add(strings); + return; + case LONG_ARRAY: + List longs = new ArrayList<>(); + for (Value v : arrayValues) { + longs.add((Long) v.getValue()); + } + data.add(longArrayKey(keyName)); + data.add(longs); + return; + case DOUBLE_ARRAY: + List doubles = new ArrayList<>(); + for (Value v : arrayValues) { + doubles.add((Double) v.getValue()); + } + data.add(doubleArrayKey(keyName)); + data.add(doubles); + return; + case BOOLEAN_ARRAY: + List booleans = new ArrayList<>(); + for (Value v : arrayValues) { + booleans.add((Boolean) v.getValue()); + } + data.add(booleanArrayKey(keyName)); + data.add(booleans); + return; + case VALUE: + // Not coercible (empty, non-homogeneous, or unsupported element type) + data.add(key); + data.add(valueObj); + return; + case EXTENDED_ATTRIBUTES: + // Not coercible + data.add(key); + data.add(valueObj); + return; + default: + throw new IllegalArgumentException("Unexpected array attribute type: " + attributeType); + } + case KEY_VALUE_LIST: + case BYTES: + // Keep as VALUE type + data.add(key); + data.add(valueObj); + return; + } + } + + /** + * Returns the ExtendedAttributeType for a homogeneous array (STRING_ARRAY, LONG_ARRAY, + * DOUBLE_ARRAY, or BOOLEAN_ARRAY), or VALUE if the array is empty, non-homogeneous, or contains + * unsupported element types. + */ + private static ExtendedAttributeType attributeType(List> arrayValues) { + if (arrayValues.isEmpty()) { + return ExtendedAttributeType.VALUE; + } + ValueType elementType = arrayValues.get(0).getType(); + for (Value v : arrayValues) { + if (v.getType() != elementType) { + return ExtendedAttributeType.VALUE; + } + } + switch (elementType) { + case STRING: + return ExtendedAttributeType.STRING_ARRAY; + case LONG: + return ExtendedAttributeType.LONG_ARRAY; + case DOUBLE: + return ExtendedAttributeType.DOUBLE_ARRAY; + case BOOLEAN: + return ExtendedAttributeType.BOOLEAN_ARRAY; + case ARRAY: + case KEY_VALUE_LIST: + case BYTES: + return ExtendedAttributeType.VALUE; + } + throw new IllegalArgumentException("Unsupported element type: " + elementType); + } + @Override public ExtendedAttributesBuilder removeIf(Predicate> predicate) { if (predicate == null) { diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java index d75bd2c5610..1a3f2627818 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java @@ -23,10 +23,21 @@ public enum ExtendedAttributeType { DOUBLE_ARRAY, // Extended types unique to ExtendedAttributes /** + * Complex attribute type for {@link io.opentelemetry.api.common.Value}-based maps. + * * @deprecated Use {@link #VALUE} with {@link io.opentelemetry.api.common.Value}-based maps * instead. */ @Deprecated EXTENDED_ATTRIBUTES, + /** + * Simple attributes ({@link AttributeType#STRING}, {@link AttributeType#LONG}, {@link + * AttributeType#DOUBLE}, {@link AttributeType#BOOLEAN}, {@link AttributeType#STRING_ARRAY}, + * {@link AttributeType#LONG_ARRAY}, {@link AttributeType#DOUBLE_ARRAY}, {@link + * AttributeType#BOOLEAN_ARRAY}) SHOULD be used whenever possible. Instrumentations SHOULD assume + * that backends do not index individual properties of complex attributes, that querying or + * aggregating on such properties is inefficient and complicated, and that reporting complex + * attributes carries higher performance overhead. + */ VALUE; } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java index 5806e44bf24..f5d7d1d3416 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java @@ -60,11 +60,59 @@ default T get(AttributeKey key) { return get(ExtendedAttributeKey.fromAttributeKey(key)); } - /** Returns the value for the given {@link ExtendedAttributeKey}, or {@code null} if not found. */ + /** + * Returns the value for the given {@link ExtendedAttributeKey}, or {@code null} if not found. + * + *

      Note: this method will automatically return the corresponding {@link Value} instance when + * passed a key of type {@link ExtendedAttributeType#VALUE} and a simple attribute is found. This + * is the inverse of {@link AttributesBuilder#put(ExtendedAttributeKey, Object)} when the key is + * {@link ExtendedAttributeType#VALUE}. + * + *

        + *
      • If {@code put(ExtendedAttributeKey.stringKey("key"), "a")} was called, then {@code + * get(ExtendedAttributeKey.valueKey("key"))} returns {@code Value.of("a")}. + *
      • If {@code put(ExtendedAttributeKey.longKey("key"), 1L)} was called, then {@code + * get(ExtendedAttributeKey.valueKey("key"))} returns {@code Value.of(1L)}. + *
      • If {@code put(ExtendedAttributeKey.doubleKey("key"), 1.0)} was called, then {@code + * get(ExtendedAttributeKey.valueKey("key"))} returns {@code Value.of(1.0)}. + *
      • If {@code put(ExtendedAttributeKey.booleanKey("key"), true)} was called, then {@code + * get(ExtendedAttributeKey.valueKey("key"))} returns {@code Value.of(true)}. + *
      • If {@code put(ExtendedAttributeKey.stringArrayKey("key"), Arrays.asList("a", "b"))} was + * called, then {@code get(ExtendedAttributeKey.valueKey("key"))} returns {@code + * Value.of(Value.of("a"), Value.of("b"))}. + *
      • If {@code put(ExtendedAttributeKey.longArrayKey("key"), Arrays.asList(1L, 2L))} was + * called, then {@code get(ExtendedAttributeKey.valueKey("key"))} returns {@code + * Value.of(Value.of(1L), Value.of(2L))}. + *
      • If {@code put(ExtendedAttributeKey.doubleArrayKey("key"), Arrays.asList(1.0, 2.0))} was + * called, then {@code get(ExtendedAttributeKey.valueKey("key"))} returns {@code + * Value.of(Value.of(1.0), Value.of(2.0))}. + *
      • If {@code put(ExtendedAttributeKey.booleanArrayKey("key"), Arrays.asList(true, false))} + * was called, then {@code get(ExtendedAttributeKey.valueKey("key"))} returns {@code + * Value.of(Value.of(true), Value.of(false))}. + *
      + * + * Further, if {@code put(ExtendedAttributeKey.valueKey("key"), Value.of(emptyList()))} was + * called, then + * + *
        + *
      • {@code get(ExtendedAttributeKey.stringArrayKey("key"))} + *
      • {@code get(ExtendedAttributeKey.longArrayKey("key"))} + *
      • {@code get(ExtendedAttributeKey.booleanArrayKey("key"))} + *
      • {@code get(ExtendedAttributeKey.doubleArrayKey("key"))} + *
      + * + * all return an empty list (as opposed to {@code null}). + */ @Nullable T get(ExtendedAttributeKey key); - /** Iterates over all the key-value pairs of attributes contained by this instance. */ + /** + * Iterates over all the key-value pairs of attributes contained by this instance. + * + *

      Note: {@link ExtendedAttributeType#VALUE} attributes will be represented as simple + * attributes if possible. See {@link ExtendedAttributesBuilder#put(ExtendedAttributeKey, Object)} + * for more details. + */ void forEach(BiConsumer, ? super Object> consumer); /** The number of attributes contained in this. */ @@ -73,7 +121,13 @@ default T get(AttributeKey key) { /** Whether there are any attributes contained in this. */ boolean isEmpty(); - /** Returns a read-only view of this {@link ExtendedAttributes} as a {@link Map}. */ + /** + * Returns a read-only view of this {@link ExtendedAttributes} as a {@link Map}. + * + *

      Note: {@link ExtendedAttributeType#VALUE} attributes will be represented as simple + * attributes in this map if possible. See {@link + * ExtendedAttributesBuilder#put(ExtendedAttributeKey, Object)} for more details. + */ Map, Object> asMap(); /** @@ -90,6 +144,8 @@ static ExtendedAttributes empty() { /** * Returns a new {@link ExtendedAttributesBuilder} instance for creating arbitrary {@link * ExtendedAttributes}. + * + * @return a new {@link ExtendedAttributesBuilder} instance */ static ExtendedAttributesBuilder builder() { return new ArrayBackedExtendedAttributesBuilder(); diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java index 159e14cf329..87415573ebf 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java @@ -35,7 +35,45 @@ default ExtendedAttributesBuilder put(AttributeKey key, T value) { return put(ExtendedAttributeKey.fromAttributeKey(key), value); } - /** Puts a {@link ExtendedAttributeKey} with associated value into this. */ + /** + * Puts an {@link ExtendedAttributeKey} with an associated value into this if the value is + * non-null. Providing a null value does not remove or unset previously set values. + * + *

      Simple attributes ({@link ExtendedAttributeType#STRING}, {@link ExtendedAttributeType#LONG}, + * {@link ExtendedAttributeType#DOUBLE}, {@link ExtendedAttributeType#BOOLEAN}, {@link + * ExtendedAttributeType#STRING_ARRAY}, {@link ExtendedAttributeType#LONG_ARRAY}, {@link + * ExtendedAttributeType#DOUBLE_ARRAY}, {@link ExtendedAttributeType#BOOLEAN_ARRAY}) SHOULD be + * used whenever possible. Instrumentations SHOULD assume that backends do not index individual + * properties of complex attributes, that querying or aggregating on such properties is + * inefficient and complicated, and that reporting complex attributes carries higher performance + * overhead. + * + *

      Note: This method will automatically convert complex attributes ({@link + * ExtendedAttributeType#VALUE}) to simple attributes when possible. + * + *

        + *
      • Calling {@code put(ExtendedAttributeKey.valueKey("key"), Value.of("a"))} is equivalent to + * calling {@code put(ExtendedAttributeKey.stringKey("key"), "a")}. + *
      • Calling {@code put(ExtendedAttributeKey.valueKey("key"), Value.of(1L))} is equivalent to + * calling {@code put(ExtendedAttributeKey.longKey("key"), 1L)}. + *
      • Calling {@code put(ExtendedAttributeKey.valueKey("key"), Value.of(1.0))} is equivalent to + * calling {@code put(ExtendedAttributeKey.doubleKey("key"), 1.0)}. + *
      • Calling {@code put(ExtendedAttributeKey.valueKey("key"), Value.of(true))} is equivalent + * to calling {@code put(ExtendedAttributeKey.booleanKey("key"), true)}. + *
      • Calling {@code put(ExtendedAttributeKey.valueKey("key"), Value.of(Value.of("a"), + * Value.of("b")))} is equivalent to calling {@code + * put(ExtendedAttributeKey.stringArrayKey("key"), Arrays.asList("a", "b"))}. + *
      • Calling {@code put(ExtendedAttributeKey.valueKey("key"), Value.of(Value.of(1L), + * Value.of(2L)))} is equivalent to calling {@code + * put(ExtendedAttributeKey.longArrayKey("key"), Arrays.asList(1L, 2L))}. + *
      • Calling {@code put(ExtendedAttributeKey.valueKey("key"), Value.of(Value.of(1.0), + * Value.of(2.0)))} is equivalent to calling {@code + * put(ExtendedAttributeKey.doubleArrayKey("key"), Arrays.asList(1.0, 2.0))}. + *
      • Calling {@code put(ExtendedAttributeKey.valueKey("key"), Value.of(Value.of(true), + * Value.of(false)))} is equivalent to calling {@code + * put(ExtendedAttributeKey.booleanArrayKey("key"), Arrays.asList(true, false))}. + *
      + */ ExtendedAttributesBuilder put(ExtendedAttributeKey key, T value); /** @@ -92,8 +130,9 @@ default ExtendedAttributesBuilder put(String key, boolean value) { *

      Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and * pre-allocate your keys, if possible. * - * @deprecated Use {@link #put(ExtendedAttributeKey, Object)} with {@link Value#of(java.util.Map)} instead. * @return this Builder + * @deprecated Use {@link #put(ExtendedAttributeKey, Object)} with {@link Value#of(java.util.Map)} + * instead. */ @Deprecated @SuppressWarnings("deprecation") diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java index 9ff816d7be8..005952b3040 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java @@ -174,9 +174,6 @@ public static ExtendedAttributeKey toExtendedAttributeKey(AttributeKey case DOUBLE_ARRAY: return InternalExtendedAttributeKeyImpl.create( attributeKey.getKey(), ExtendedAttributeType.DOUBLE_ARRAY); - case VALUE: - return InternalExtendedAttributeKeyImpl.create( - attributeKey.getKey(), ExtendedAttributeType.VALUE); } throw new IllegalArgumentException("Unrecognized attributeKey type: " + attributeKey.getType()); } diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java index d4e72f4da00..1c67237ddb9 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java @@ -5,7 +5,16 @@ package io.opentelemetry.api.incubator.common; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.booleanArrayKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.booleanKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.doubleArrayKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.doubleKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.longArrayKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.longKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.stringArrayKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.stringKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.valueKey; +import static org.assertj.core.api.Assertions.assertThat; import com.google.common.collect.ImmutableMap; import io.opentelemetry.api.common.AttributeKey; @@ -17,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Stream; +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; @@ -218,8 +228,8 @@ private static Stream attributesArgs() { .put("key", ImmutableMap.builder().put("child", "value").build()) .build()), Arguments.of( - ExtendedAttributes.builder().put("key", Value.of("value")).build(), - ImmutableMap.builder().put("key", Value.of("value")).build()), + ExtendedAttributes.builder().put(valueKey("key"), Value.of("value")).build(), + ImmutableMap.builder().put("key", "value").build()), Arguments.of( ExtendedAttributes.builder() .put(ExtendedAttributeKey.stringKey("key"), "value") @@ -267,7 +277,7 @@ private static Stream attributesArgs() { ExtendedAttributes.builder() .put(ExtendedAttributeKey.valueKey("key"), Value.of("value")) .build(), - ImmutableMap.builder().put("key", Value.of("value")).build()), + ImmutableMap.builder().put("key", "value").build()), // Multiple entries Arguments.of( ExtendedAttributes.builder() @@ -281,7 +291,7 @@ private static Stream attributesArgs() { .put("key8", 1L, 2L) .put("key9", 1.1, 2.2) .put("key10", ExtendedAttributes.builder().put("child", "value").build()) - .put("key11", Value.of("value")) + .put(valueKey("key11"), Value.of("value")) .build(), ImmutableMap.builder() .put("key1", "value1") @@ -294,7 +304,7 @@ private static Stream attributesArgs() { .put("key8", Arrays.asList(1L, 2L)) .put("key9", Arrays.asList(1.1, 2.2)) .put("key10", ImmutableMap.builder().put("child", "value").build()) - .put("key11", Value.of("value")) + .put("key11", "value") .build())); } @@ -377,4 +387,107 @@ private static ExtendedAttributeType getType(Object value) { } throw new IllegalArgumentException("Unrecognized value type: " + value); } + + @Test + void complexValueStoredAsString() { + ExtendedAttributes attributes = + ExtendedAttributes.builder().put(valueKey("key"), Value.of("test")).build(); + assertThat(attributes.get(stringKey("key"))).isEqualTo("test"); + } + + @Test + void complexValueStoredAsLong() { + ExtendedAttributes attributes = + ExtendedAttributes.builder().put(valueKey("key"), Value.of(123L)).build(); + assertThat(attributes.get(longKey("key"))).isEqualTo(123L); + } + + @Test + void complexValueStoredAsDouble() { + ExtendedAttributes attributes = + ExtendedAttributes.builder().put(valueKey("key"), Value.of(1.23)).build(); + assertThat(attributes.get(doubleKey("key"))).isEqualTo(1.23); + } + + @Test + void complexValueStoredAsBoolean() { + ExtendedAttributes attributes = + ExtendedAttributes.builder().put(valueKey("key"), Value.of(true)).build(); + assertThat(attributes.get(booleanKey("key"))).isEqualTo(true); + } + + @Test + void complexValueStoredAsStringArray() { + ExtendedAttributes attributes = + ExtendedAttributes.builder() + .put(valueKey("key"), Value.of(Arrays.asList(Value.of("a"), Value.of("b")))) + .build(); + assertThat(attributes.get(stringArrayKey("key"))).containsExactly("a", "b"); + } + + @Test + void complexValueStoredAsLongArray() { + ExtendedAttributes attributes = + ExtendedAttributes.builder() + .put(valueKey("key"), Value.of(Arrays.asList(Value.of(1L), Value.of(2L)))) + .build(); + assertThat(attributes.get(longArrayKey("key"))).containsExactly(1L, 2L); + } + + @Test + void complexValueStoredAsDoubleArray() { + ExtendedAttributes attributes = + ExtendedAttributes.builder() + .put(valueKey("key"), Value.of(Arrays.asList(Value.of(1.1), Value.of(2.2)))) + .build(); + assertThat(attributes.get(doubleArrayKey("key"))).containsExactly(1.1, 2.2); + } + + @Test + void complexValueStoredAsBooleanArray() { + ExtendedAttributes attributes = + ExtendedAttributes.builder() + .put(valueKey("key"), Value.of(Arrays.asList(Value.of(true), Value.of(false)))) + .build(); + assertThat(attributes.get(booleanArrayKey("key"))).containsExactly(true, false); + } + + @Test + void simpleAttributeRetrievedAsComplexValue() { + ExtendedAttributes attributes = + ExtendedAttributes.builder() + .put("string", "test") + .put("long", 123L) + .put("double", 1.23) + .put("boolean", true) + .put("stringArray", "a", "b") + .put("longArray", 1L, 2L) + .put("doubleArray", 1.1, 2.2) + .put("booleanArray", true, false) + .build(); + assertThat(attributes.get(valueKey("string"))).isEqualTo(Value.of("test")); + assertThat(attributes.get(valueKey("long"))).isEqualTo(Value.of(123L)); + assertThat(attributes.get(valueKey("double"))).isEqualTo(Value.of(1.23)); + assertThat(attributes.get(valueKey("boolean"))).isEqualTo(Value.of(true)); + assertThat(attributes.get(valueKey("stringArray"))) + .isEqualTo(Value.of(Arrays.asList(Value.of("a"), Value.of("b")))); + assertThat(attributes.get(valueKey("longArray"))) + .isEqualTo(Value.of(Arrays.asList(Value.of(1L), Value.of(2L)))); + assertThat(attributes.get(valueKey("doubleArray"))) + .isEqualTo(Value.of(Arrays.asList(Value.of(1.1), Value.of(2.2)))); + assertThat(attributes.get(valueKey("booleanArray"))) + .isEqualTo(Value.of(Arrays.asList(Value.of(true), Value.of(false)))); + } + + @Test + void emptyValueArrayRetrievedAsAnyArrayType() { + ExtendedAttributes attributes = + ExtendedAttributes.builder() + .put(valueKey("key"), Value.of(Collections.emptyList())) + .build(); + assertThat(attributes.get(stringArrayKey("key"))).isEmpty(); + assertThat(attributes.get(longArrayKey("key"))).isEmpty(); + assertThat(attributes.get(doubleArrayKey("key"))).isEmpty(); + assertThat(attributes.get(booleanArrayKey("key"))).isEmpty(); + } } diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java index 1bebb3f397e..e3e17cff3dc 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java @@ -11,6 +11,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.KeyValue; import io.opentelemetry.api.common.Value; import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; import io.opentelemetry.api.incubator.common.ExtendedAttributes; @@ -110,7 +111,7 @@ private static String flipCoin() { ExtendedAttributeKey.extendedAttributesKey("acme.map"); // VALUE key - AttributeKey> valueKey = AttributeKey.valueKey("acme.value"); + ExtendedAttributeKey> valueKey = ExtendedAttributeKey.valueKey("acme.value"); @Test @SuppressLogger(ExtendedLogsBridgeApiUsageTest.class) @@ -133,8 +134,8 @@ void extendedAttributesUsage() { .put( valueKey, Value.of( - Value.KeyValue.of("childStr", Value.of("value")), - Value.KeyValue.of("childLong", Value.of(1L)))) + KeyValue.of("childStr", Value.of("value")), + KeyValue.of("childLong", Value.of(1L)))) .build(); // Retrieval @@ -152,8 +153,8 @@ void extendedAttributesUsage() { assertThat(extendedAttributes.get(valueKey)) .isEqualTo( Value.of( - Value.KeyValue.of("childStr", Value.of("value")), - Value.KeyValue.of("childLong", Value.of(1L)))); + KeyValue.of("childStr", Value.of("value")), + KeyValue.of("childLong", Value.of(1L)))); // Iteration // Output: @@ -166,7 +167,8 @@ void extendedAttributesUsage() { // acme.map(EXTENDED_ATTRIBUTES): {childLong=1, childStr="value"} // acme.string(STRING): value // acme.string_array(STRING_ARRAY): [value1, value2] - // acme.value(VALUE): [KeyValue{key=childStr, value=StringValue{value=value}}, KeyValue{key=childLong, value=LongValue{value=1}}] + // acme.value(VALUE): [KeyValue{key=childStr, value=StringValue{value=value}}, + // KeyValue{key=childLong, value=LongValue{value=1}}] extendedAttributes.forEach( (extendedAttributeKey, object) -> logger.info( @@ -203,8 +205,7 @@ void logRecordBuilder_ExtendedAttributes() { .setAttribute( valueKey, Value.of( - Value.KeyValue.of("childStr", Value.of("value")), - Value.KeyValue.of("childLong", Value.of(1L)))) + KeyValue.of("childStr", Value.of("value")), KeyValue.of("childLong", Value.of(1L)))) .setAllAttributes(Attributes.builder().put("key1", "value").build()) .setAllAttributes(ExtendedAttributes.builder().put("key2", "value").build()) .emit(); @@ -253,8 +254,8 @@ void logRecordBuilder_ExtendedAttributes() { .put( valueKey, Value.of( - Value.KeyValue.of("childStr", Value.of("value")), - Value.KeyValue.of("childLong", Value.of(1L)))) + KeyValue.of("childStr", Value.of("value")), + KeyValue.of("childLong", Value.of(1L)))) .put("key1", "value") .put("key2", "value") .build()); diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeKeyValueStatelessMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeKeyValueStatelessMarshaler.java index 08dee75822f..3fb1f7c25f6 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeKeyValueStatelessMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeKeyValueStatelessMarshaler.java @@ -100,9 +100,6 @@ public int getBinarySerializedSize( (List) value, AttributeArrayAnyValueStatelessMarshaler.INSTANCE, context); - case VALUE: - return AnyValueStatelessMarshaler.INSTANCE.getBinarySerializedSize( - (io.opentelemetry.api.common.Value) value, context); } // Error prone ensures the switch statement is complete, otherwise only can happen with // unaligned versions which are not supported. @@ -139,10 +136,6 @@ public void writeTo( AttributeArrayAnyValueStatelessMarshaler.INSTANCE, context); return; - case VALUE: - AnyValueStatelessMarshaler.INSTANCE.writeTo( - output, (io.opentelemetry.api.common.Value) value, context); - return; } // Error prone ensures the switch statement is complete, otherwise only can happen with // unaligned versions which are not supported. diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueMarshaler.java index 8cf231b0c86..ec7dd47f10b 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueMarshaler.java @@ -8,7 +8,6 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.KeyValue; -import io.opentelemetry.api.common.Value; import io.opentelemetry.api.internal.InternalAttributeKeyImpl; import io.opentelemetry.exporter.internal.marshal.Marshaler; import io.opentelemetry.exporter.internal.marshal.MarshalerUtil; @@ -119,8 +118,6 @@ private static KeyValueMarshaler create(AttributeKey attributeKey, Object val case DOUBLE_ARRAY: return new KeyValueMarshaler( keyUtf8, ArrayAnyValueMarshaler.createDouble((List) value)); - case VALUE: - return new KeyValueMarshaler(keyUtf8, AnyValueMarshaler.create((Value) value)); } // Error prone ensures the switch statement is complete, otherwise only can happen with // unaligned versions which are not supported. diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributeAssertion.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributeAssertion.java index c86b542ce85..971ff9446ff 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributeAssertion.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributeAssertion.java @@ -58,8 +58,6 @@ static AttributeAssertion create( case LONG_ARRAY: case DOUBLE_ARRAY: return assertThat((List) value); - case VALUE: - return assertThat(value); } throw new IllegalArgumentException("Unknown type for key " + key); } diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LogRecordDataAssert.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LogRecordDataAssert.java index 4dd9a3940ca..d2f621629d7 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LogRecordDataAssert.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LogRecordDataAssert.java @@ -344,10 +344,8 @@ public LogRecordDataAssert hasBodyField(AttributeKey key, T value) { return hasBodyField( key.getKey(), Value.of(((List) value).stream().map(Value::of).collect(toList()))); - case VALUE: - return hasBodyField(key.getKey(), (Value) value); } - throw new IllegalArgumentException("Unknown type for key " + key); + return this; } /** Asserts the log has the given attributes. */ From 08e6e0ceb95cf0de1ad0d37f7159946778a8088c Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 7 Nov 2025 17:11:29 -0800 Subject: [PATCH 12/28] tests --- .../common/ExtendedAttributeType.java | 15 +-- .../incubator/common/ExtendedAttributes.java | 9 +- .../common/ExtendedAttributesTest.java | 110 ++++++++++++++++++ 3 files changed, 123 insertions(+), 11 deletions(-) diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java index 1a3f2627818..b21747d3860 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java @@ -31,13 +31,14 @@ public enum ExtendedAttributeType { @Deprecated EXTENDED_ATTRIBUTES, /** - * Simple attributes ({@link AttributeType#STRING}, {@link AttributeType#LONG}, {@link - * AttributeType#DOUBLE}, {@link AttributeType#BOOLEAN}, {@link AttributeType#STRING_ARRAY}, - * {@link AttributeType#LONG_ARRAY}, {@link AttributeType#DOUBLE_ARRAY}, {@link - * AttributeType#BOOLEAN_ARRAY}) SHOULD be used whenever possible. Instrumentations SHOULD assume - * that backends do not index individual properties of complex attributes, that querying or - * aggregating on such properties is inefficient and complicated, and that reporting complex - * attributes carries higher performance overhead. + * Simple attributes ({@link ExtendedAttributeType#STRING}, {@link ExtendedAttributeType#LONG}, + * {@link ExtendedAttributeType#DOUBLE}, {@link ExtendedAttributeType#BOOLEAN}, {@link + * ExtendedAttributeType#STRING_ARRAY}, {@link ExtendedAttributeType#LONG_ARRAY}, {@link + * ExtendedAttributeType#DOUBLE_ARRAY}, {@link ExtendedAttributeType#BOOLEAN_ARRAY}) SHOULD be + * used whenever possible. Instrumentations SHOULD assume that backends do not index individual + * properties of complex attributes, that querying or aggregating on such properties is + * inefficient and complicated, and that reporting complex attributes carries higher performance + * overhead. */ VALUE; } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java index f5d7d1d3416..420fb2e4d5f 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java @@ -63,10 +63,11 @@ default T get(AttributeKey key) { /** * Returns the value for the given {@link ExtendedAttributeKey}, or {@code null} if not found. * - *

      Note: this method will automatically return the corresponding {@link Value} instance when - * passed a key of type {@link ExtendedAttributeType#VALUE} and a simple attribute is found. This - * is the inverse of {@link AttributesBuilder#put(ExtendedAttributeKey, Object)} when the key is - * {@link ExtendedAttributeType#VALUE}. + *

      Note: this method will automatically return the corresponding {@link + * io.opentelemetry.api.common.Value} instance when passed a key of type {@link + * ExtendedAttributeType#VALUE} and a simple attribute is found. This is the inverse of {@link + * ExtendedAttributesBuilder#put(ExtendedAttributeKey, Object)} when the key is {@link + * ExtendedAttributeType#VALUE}. * *

        *
      • If {@code put(ExtendedAttributeKey.stringKey("key"), "a")} was called, then {@code diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java index 1c67237ddb9..247993ed099 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java @@ -15,6 +15,7 @@ import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.stringKey; import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.valueKey; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import com.google.common.collect.ImmutableMap; import io.opentelemetry.api.common.AttributeKey; @@ -390,66 +391,175 @@ private static ExtendedAttributeType getType(Object value) { @Test void complexValueStoredAsString() { + // When putting a VALUE attribute with a string Value, it should be stored as STRING type ExtendedAttributes attributes = ExtendedAttributes.builder().put(valueKey("key"), Value.of("test")).build(); + + // Should be stored as STRING type internally assertThat(attributes.get(stringKey("key"))).isEqualTo("test"); + assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of("test")); + + // forEach should show STRING type + Map, Object> entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(stringKey("key"), "test")); + + // asMap should show STRING type + assertThat(attributes.asMap()).containsExactly(entry(stringKey("key"), "test")); } @Test void complexValueStoredAsLong() { + // When putting a VALUE attribute with a long Value, it should be stored as LONG type ExtendedAttributes attributes = ExtendedAttributes.builder().put(valueKey("key"), Value.of(123L)).build(); + + // Should be stored as LONG type internally assertThat(attributes.get(longKey("key"))).isEqualTo(123L); + assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(123L)); + + // forEach should show LONG type + Map, Object> entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(longKey("key"), 123L)); + + // asMap should show LONG type + assertThat(attributes.asMap()).containsExactly(entry(longKey("key"), 123L)); } @Test void complexValueStoredAsDouble() { + // When putting a VALUE attribute with a double Value, it should be stored as DOUBLE type ExtendedAttributes attributes = ExtendedAttributes.builder().put(valueKey("key"), Value.of(1.23)).build(); + + // Should be stored as DOUBLE type internally assertThat(attributes.get(doubleKey("key"))).isEqualTo(1.23); + assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(1.23)); + + // forEach should show DOUBLE type + Map, Object> entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(doubleKey("key"), 1.23)); + + // asMap should show DOUBLE type + assertThat(attributes.asMap()).containsExactly(entry(doubleKey("key"), 1.23)); } @Test void complexValueStoredAsBoolean() { + // When putting a VALUE attribute with a boolean Value, it should be stored as BOOLEAN type ExtendedAttributes attributes = ExtendedAttributes.builder().put(valueKey("key"), Value.of(true)).build(); + + // Should be stored as BOOLEAN type internally assertThat(attributes.get(booleanKey("key"))).isEqualTo(true); + assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(true)); + + // forEach should show BOOLEAN type + Map, Object> entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(booleanKey("key"), true)); + + // asMap should show BOOLEAN type + assertThat(attributes.asMap()).containsExactly(entry(booleanKey("key"), true)); } @Test void complexValueStoredAsStringArray() { + // When putting a VALUE attribute with a homogeneous string array, it should be stored as + // STRING_ARRAY type ExtendedAttributes attributes = ExtendedAttributes.builder() .put(valueKey("key"), Value.of(Arrays.asList(Value.of("a"), Value.of("b")))) .build(); + + // Should be stored as STRING_ARRAY type internally assertThat(attributes.get(stringArrayKey("key"))).containsExactly("a", "b"); + assertThat(attributes.get(valueKey("key"))) + .isEqualTo(Value.of(Arrays.asList(Value.of("a"), Value.of("b")))); + + // forEach should show STRING_ARRAY type + Map, Object> entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(stringArrayKey("key"), Arrays.asList("a", "b"))); + + // asMap should show STRING_ARRAY type + assertThat(attributes.asMap()) + .containsExactly(entry(stringArrayKey("key"), Arrays.asList("a", "b"))); } @Test void complexValueStoredAsLongArray() { + // When putting a VALUE attribute with a homogeneous long array, it should be stored as + // LONG_ARRAY type ExtendedAttributes attributes = ExtendedAttributes.builder() .put(valueKey("key"), Value.of(Arrays.asList(Value.of(1L), Value.of(2L)))) .build(); + + // Should be stored as LONG_ARRAY type internally assertThat(attributes.get(longArrayKey("key"))).containsExactly(1L, 2L); + assertThat(attributes.get(valueKey("key"))) + .isEqualTo(Value.of(Arrays.asList(Value.of(1L), Value.of(2L)))); + + // forEach should show LONG_ARRAY type + Map, Object> entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(longArrayKey("key"), Arrays.asList(1L, 2L))); + + // asMap should show LONG_ARRAY type + assertThat(attributes.asMap()) + .containsExactly(entry(longArrayKey("key"), Arrays.asList(1L, 2L))); } @Test void complexValueStoredAsDoubleArray() { + // When putting a VALUE attribute with a homogeneous double array, it should be stored as + // DOUBLE_ARRAY type ExtendedAttributes attributes = ExtendedAttributes.builder() .put(valueKey("key"), Value.of(Arrays.asList(Value.of(1.1), Value.of(2.2)))) .build(); + + // Should be stored as DOUBLE_ARRAY type internally assertThat(attributes.get(doubleArrayKey("key"))).containsExactly(1.1, 2.2); + assertThat(attributes.get(valueKey("key"))) + .isEqualTo(Value.of(Arrays.asList(Value.of(1.1), Value.of(2.2)))); + + // forEach should show DOUBLE_ARRAY type + Map, Object> entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(doubleArrayKey("key"), Arrays.asList(1.1, 2.2))); + + // asMap should show DOUBLE_ARRAY type + assertThat(attributes.asMap()) + .containsExactly(entry(doubleArrayKey("key"), Arrays.asList(1.1, 2.2))); } @Test void complexValueStoredAsBooleanArray() { + // When putting a VALUE attribute with a homogeneous boolean array, it should be stored as + // BOOLEAN_ARRAY type ExtendedAttributes attributes = ExtendedAttributes.builder() .put(valueKey("key"), Value.of(Arrays.asList(Value.of(true), Value.of(false)))) .build(); + + // Should be stored as BOOLEAN_ARRAY type internally assertThat(attributes.get(booleanArrayKey("key"))).containsExactly(true, false); + assertThat(attributes.get(valueKey("key"))) + .isEqualTo(Value.of(Arrays.asList(Value.of(true), Value.of(false)))); + + // forEach should show BOOLEAN_ARRAY type + Map, Object> entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen) + .containsExactly(entry(booleanArrayKey("key"), Arrays.asList(true, false))); + + // asMap should show BOOLEAN_ARRAY type + assertThat(attributes.asMap()) + .containsExactly(entry(booleanArrayKey("key"), Arrays.asList(true, false))); } @Test From c7824e1bf4601aa800d98480b74d59ff3279714e Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 7 Nov 2025 17:38:57 -0800 Subject: [PATCH 13/28] javadoc --- .../api/incubator/common/ExtendedAttributes.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java index 420fb2e4d5f..14a16778d3d 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java @@ -92,7 +92,7 @@ default T get(AttributeKey key) { * Value.of(Value.of(true), Value.of(false))}. *
      * - * Further, if {@code put(ExtendedAttributeKey.valueKey("key"), Value.of(emptyList()))} was + *

      Further, if {@code put(ExtendedAttributeKey.valueKey("key"), Value.of(emptyList()))} was * called, then * *

        @@ -102,7 +102,7 @@ default T get(AttributeKey key) { *
      • {@code get(ExtendedAttributeKey.doubleArrayKey("key"))} *
      * - * all return an empty list (as opposed to {@code null}). + *

      all return an empty list (as opposed to {@code null}). */ @Nullable T get(ExtendedAttributeKey key); From 26a3413f7d4e96d4bd468efbb35c2a5c09b46ab0 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 13 Nov 2025 11:20:02 -0800 Subject: [PATCH 14/28] fix --- .../internal/otlp/logs/LogsRequestMarshalerIncubatingTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/exporters/otlp/common/src/testIncubating/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerIncubatingTest.java b/exporters/otlp/common/src/testIncubating/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerIncubatingTest.java index 60d4c4fe799..8897bfdb425 100644 --- a/exporters/otlp/common/src/testIncubating/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerIncubatingTest.java +++ b/exporters/otlp/common/src/testIncubating/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerIncubatingTest.java @@ -49,6 +49,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +@SuppressWarnings("deprecation") // Testing deprecated EXTENDED_ATTRIBUTES until removed class LogsRequestMarshalerIncubatingTest { private static final byte[] TRACE_ID_BYTES = new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}; From d83ff5da96be43015f2af37c480b7e72b1785467 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 13 Nov 2025 13:08:36 -0800 Subject: [PATCH 15/28] Add a missing test --- .../LogsRequestMarshalerIncubatingTest.java | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/exporters/otlp/common/src/testIncubating/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerIncubatingTest.java b/exporters/otlp/common/src/testIncubating/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerIncubatingTest.java index 8897bfdb425..84639db8312 100644 --- a/exporters/otlp/common/src/testIncubating/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerIncubatingTest.java +++ b/exporters/otlp/common/src/testIncubating/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerIncubatingTest.java @@ -13,6 +13,8 @@ import com.google.protobuf.Message; import com.google.protobuf.util.JsonFormat; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; import io.opentelemetry.api.incubator.common.ExtendedAttributes; import io.opentelemetry.api.internal.OtelEncodingUtils; import io.opentelemetry.api.logs.Severity; @@ -91,7 +93,7 @@ void toProtoLogRecord(MarshalerSource marshalerSource) { .setSpanContext( SpanContext.create( TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())) - .setTotalAttributeCount(10) + .setTotalAttributeCount(11) .setTimestamp(12345, TimeUnit.NANOSECONDS) .setObservedTimestamp(6789, TimeUnit.NANOSECONDS) // Extended fields @@ -119,6 +121,21 @@ void toProtoLogRecord(MarshalerSource marshalerSource) { .build()) .put("str_key", "str_value") .build()) + .put( + ExtendedAttributeKey.valueKey("value_key"), + Value.of( + io.opentelemetry.api.common.KeyValue.of( + "bool_key", Value.of(true)), + io.opentelemetry.api.common.KeyValue.of( + "double_key", Value.of(1.1)), + io.opentelemetry.api.common.KeyValue.of("int_key", Value.of(1)), + io.opentelemetry.api.common.KeyValue.of( + "value_key", + Value.of( + io.opentelemetry.api.common.KeyValue.of( + "str_key", Value.of("str_value")))), + io.opentelemetry.api.common.KeyValue.of( + "str_key", Value.of("str_value")))) .build()) .build())); @@ -163,6 +180,27 @@ void toProtoLogRecord(MarshalerSource marshalerSource) { .build())) .addValues(keyValue("str_key", anyValue("str_value"))) .build()) + .build()), + keyValue( + "value_key", + AnyValue.newBuilder() + .setKvlistValue( + KeyValueList.newBuilder() + .addValues(keyValue("bool_key", anyValue(true))) + .addValues(keyValue("double_key", anyValue(1.1))) + .addValues(keyValue("int_key", anyValue(1))) + .addValues( + keyValue( + "value_key", + AnyValue.newBuilder() + .setKvlistValue( + KeyValueList.newBuilder() + .addValues( + keyValue("str_key", anyValue("str_value"))) + .build()) + .build())) + .addValues(keyValue("str_key", anyValue("str_value"))) + .build()) .build())); } From 10d6e02634ea04f4537f21574428196732557f71 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 13 Nov 2025 14:32:17 -0800 Subject: [PATCH 16/28] Apply suggestion from @trask --- .../opentelemetry/api/common/ArrayBackedAttributesBuilder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java index 1f39cc16fa7..af575a61e36 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java @@ -38,7 +38,6 @@ public AttributesBuilder put(AttributeKey key, int value) { } @Override - @SuppressWarnings("unchecked") public AttributesBuilder put(AttributeKey key, @Nullable T value) { if (key == null || key.getKey().isEmpty() || value == null) { return this; From 198faa2c454307e025eedd178add804aab312246 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 13 Nov 2025 14:52:51 -0800 Subject: [PATCH 17/28] codecov --- .../common/ExtendedAttributesTest.java | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java index 247993ed099..beff501826e 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java @@ -600,4 +600,111 @@ void emptyValueArrayRetrievedAsAnyArrayType() { assertThat(attributes.get(doubleArrayKey("key"))).isEmpty(); assertThat(attributes.get(booleanArrayKey("key"))).isEmpty(); } + + @Test + void getNullKey() { + ExtendedAttributes attributes = ExtendedAttributes.builder().put("key", "value").build(); + assertThat(attributes.get((ExtendedAttributeKey) null)).isNull(); + } + + @Test + void putNullKey() { + ExtendedAttributes attributes = + ExtendedAttributes.builder().put((ExtendedAttributeKey) null, "value").build(); + assertThat(attributes.isEmpty()).isTrue(); + } + + @Test + void putNullValue() { + ExtendedAttributes attributes = + ExtendedAttributes.builder().put(stringKey("key"), null).build(); + assertThat(attributes.isEmpty()).isTrue(); + } + + @Test + void putEmptyKey() { + ExtendedAttributes attributes = + ExtendedAttributes.builder().put(stringKey(""), "value").build(); + assertThat(attributes.isEmpty()).isTrue(); + } + + @Test + void extendedAttributesNotConvertibleToValue() { + ExtendedAttributes nested = ExtendedAttributes.builder().put("child", "value").build(); + ExtendedAttributes attributes = + ExtendedAttributes.builder() + .put(ExtendedAttributeKey.extendedAttributesKey("key"), nested) + .build(); + + // Getting as VALUE should return null since EXTENDED_ATTRIBUTES cannot be converted to Value + assertThat(attributes.get(valueKey("key"))).isNull(); + } + + @Test + void complexValueWithKeyValueList() { + // KEY_VALUE_LIST should be kept as VALUE type + Value kvListValue = Value.of(Collections.emptyMap()); + ExtendedAttributes attributes = + ExtendedAttributes.builder().put(valueKey("key"), kvListValue).build(); + + // Should be stored as VALUE type + assertThat(attributes.get(valueKey("key"))).isEqualTo(kvListValue); + + // forEach should show VALUE type + Map, Object> entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(valueKey("key"), kvListValue)); + } + + @Test + void complexValueWithBytes() { + // BYTES should be kept as VALUE type + Value bytesValue = Value.of(new byte[] {1, 2, 3}); + ExtendedAttributes attributes = + ExtendedAttributes.builder().put(valueKey("key"), bytesValue).build(); + + // Should be stored as VALUE type + assertThat(attributes.get(valueKey("key"))).isEqualTo(bytesValue); + + // forEach should show VALUE type + Map, Object> entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(valueKey("key"), bytesValue)); + } + + @Test + void complexValueWithNonHomogeneousArray() { + // Non-homogeneous array should be kept as VALUE type + Value mixedArray = Value.of(Arrays.asList(Value.of("string"), Value.of(123L))); + ExtendedAttributes attributes = + ExtendedAttributes.builder().put(valueKey("key"), mixedArray).build(); + + // Should be stored as VALUE type + assertThat(attributes.get(valueKey("key"))).isEqualTo(mixedArray); + + // forEach should show VALUE type + Map, Object> entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(valueKey("key"), mixedArray)); + } + + @Test + void complexValueWithNestedArray() { + // Array containing arrays should be kept as VALUE type + Value nestedArray = + Value.of( + Arrays.asList( + Value.of(Arrays.asList(Value.of("a"), Value.of("b"))), + Value.of(Arrays.asList(Value.of("c"), Value.of("d"))))); + ExtendedAttributes attributes = + ExtendedAttributes.builder().put(valueKey("key"), nestedArray).build(); + + // Should be stored as VALUE type + assertThat(attributes.get(valueKey("key"))).isEqualTo(nestedArray); + + // forEach should show VALUE type + Map, Object> entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(valueKey("key"), nestedArray)); + } } From 4386c8c1139afdb362467224ab7e2ad0d4c64689 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Tue, 18 Nov 2025 14:16:05 -0800 Subject: [PATCH 18/28] Implement limits --- sdk/common/build.gradle.kts | 1 + .../sdk/internal/AttributeUtil.java | 82 ++++++++++++++++++- .../sdk/internal/ExtendedAttributesMap.java | 2 - .../internal/ExtendedAttributesValueTest.java | 68 +++++++++++++++ 4 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 sdk/common/src/test/java/io/opentelemetry/sdk/internal/ExtendedAttributesValueTest.java diff --git a/sdk/common/build.gradle.kts b/sdk/common/build.gradle.kts index c092ec21448..8570cbe7148 100644 --- a/sdk/common/build.gradle.kts +++ b/sdk/common/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { testAnnotationProcessor("com.google.auto.value:auto-value") + testImplementation(project(":api:incubator")) // for ExtendedAttributesValueTest testImplementation(project(":sdk:testing")) testImplementation("com.google.guava:guava-testlib") testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating") diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java index de7fac88eba..f852f262430 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java @@ -8,6 +8,10 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.common.KeyValue; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.common.ValueType; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -60,6 +64,27 @@ private static boolean isValidLength(Object value, int lengthLimit) { return allMatch((List) value, entry -> isValidLength(entry, lengthLimit)); } else if (value instanceof String) { return ((String) value).length() < lengthLimit; + } else if (value instanceof Value) { + return isValidLengthValue((Value) value, lengthLimit); + } + return true; + } + + private static boolean isValidLengthValue(Value value, int lengthLimit) { + ValueType type = value.getType(); + if (type == ValueType.STRING) { + return ((String) value.getValue()).length() < lengthLimit; + } else if (type == ValueType.BYTES) { + ByteBuffer buffer = (ByteBuffer) value.getValue(); + return buffer.remaining() <= lengthLimit; + } else if (type == ValueType.ARRAY) { + @SuppressWarnings("unchecked") + List> array = (List>) value.getValue(); + return allMatch(array, element -> isValidLengthValue(element, lengthLimit)); + } else if (type == ValueType.KEY_VALUE_LIST) { + @SuppressWarnings("unchecked") + List kvList = (List) value.getValue(); + return allMatch(kvList, kv -> isValidLengthValue(kv.getValue(), lengthLimit)); } return true; } @@ -74,8 +99,19 @@ private static boolean allMatch(Iterable iterable, Predicate predicate } /** - * Apply the {@code lengthLimit} to the attribute {@code value}. Strings and strings in lists - * which exceed the length limit are truncated. + * Apply the {@code lengthLimit} to the attribute {@code value}. Strings, byte arrays, and nested + * values which exceed the length limit are truncated. + * + *

      Applies to: + * + *

        + *
      • String values + *
      • Each string within an array of strings + *
      • String values within {@link Value} objects + *
      • Byte array values within {@link Value} objects + *
      • Recursively, each element in an array of {@link Value}s + *
      • Recursively, each value in a {@link Value} key-value list + *
      */ public static Object applyAttributeLengthLimit(Object value, int lengthLimit) { if (lengthLimit == Integer.MAX_VALUE) { @@ -93,6 +129,48 @@ public static Object applyAttributeLengthLimit(Object value, int lengthLimit) { String str = (String) value; return str.length() < lengthLimit ? value : str.substring(0, lengthLimit); } + if (value instanceof Value) { + return applyValueLengthLimit((Value) value, lengthLimit); + } + return value; + } + + @SuppressWarnings("unchecked") + private static Value applyValueLengthLimit(Value value, int lengthLimit) { + ValueType type = value.getType(); + + if (type == ValueType.STRING) { + String str = (String) value.getValue(); + if (str.length() <= lengthLimit) { + return value; + } + return Value.of(str.substring(0, lengthLimit)); + } else if (type == ValueType.BYTES) { + ByteBuffer buffer = (ByteBuffer) value.getValue(); + int length = buffer.remaining(); + if (length <= lengthLimit) { + return value; + } + byte[] truncated = new byte[lengthLimit]; + buffer.get(truncated); + return Value.of(truncated); + } else if (type == ValueType.ARRAY) { + List> array = (List>) value.getValue(); + List> result = new ArrayList<>(array.size()); + for (Value element : array) { + result.add(applyValueLengthLimit(element, lengthLimit)); + } + return Value.of(result); + } else if (type == ValueType.KEY_VALUE_LIST) { + List kvList = (List) value.getValue(); + List result = new ArrayList<>(kvList.size()); + for (KeyValue kv : kvList) { + result.add(KeyValue.of(kv.getKey(), applyValueLengthLimit(kv.getValue(), lengthLimit))); + } + return Value.of(result.toArray(new KeyValue[0])); + } + + // For BOOLEAN, LONG, DOUBLE - no truncation needed return value; } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExtendedAttributesMap.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExtendedAttributesMap.java index 52959b1b617..0c8d1c9491e 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExtendedAttributesMap.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExtendedAttributesMap.java @@ -56,11 +56,9 @@ public Object put(ExtendedAttributeKey key, @Nullable Object value) { return null; } totalAddedValues++; - // TODO(jack-berg): apply capacity to nested entries if (size() >= capacity && !containsKey(key)) { return null; } - // TODO(jack-berg): apply limits to nested entries return super.put(key, AttributeUtil.applyAttributeLengthLimit(value, lengthLimit)); } diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/internal/ExtendedAttributesValueTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/internal/ExtendedAttributesValueTest.java new file mode 100644 index 00000000000..3ff20971415 --- /dev/null +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/internal/ExtendedAttributesValueTest.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.KeyValue; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; +import java.nio.ByteBuffer; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ExtendedAttributesValueTest { + + @Test + void put_ByteArrayTruncation() { + ExtendedAttributesMap map = ExtendedAttributesMap.create(128, 5); + byte[] bytes = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + Value value = Value.of(bytes); + + map.put(ExtendedAttributeKey.valueKey("key"), value); + + Value result = map.get(ExtendedAttributeKey.valueKey("key")); + ByteBuffer buffer = (ByteBuffer) result.getValue(); + byte[] resultBytes = new byte[buffer.remaining()]; + buffer.get(resultBytes); + assertThat(resultBytes).containsExactly(1, 2, 3, 4, 5); + } + + @Test + void put_ValueArrayTruncation() { + ExtendedAttributesMap map = ExtendedAttributesMap.create(128, 5); + + Value arrayValue = Value.of(Value.of("short"), Value.of("this is too long")); + + map.put(ExtendedAttributeKey.valueKey("key"), arrayValue); + + Value result = map.get(ExtendedAttributeKey.valueKey("key")); + @SuppressWarnings("unchecked") + List> resultList = (List>) result.getValue(); + assertThat(resultList).hasSize(2); + assertThat(resultList.get(0).getValue()).isEqualTo("short"); + assertThat(resultList.get(1).getValue()).isEqualTo("this "); + } + + @Test + void put_ValueKeyValueListTruncation() { + ExtendedAttributesMap map = ExtendedAttributesMap.create(128, 5); + + Value kvListValue = + Value.of( + KeyValue.of("key1", Value.of("short")), + KeyValue.of("key2", Value.of("this is too long"))); + + map.put(ExtendedAttributeKey.valueKey("key"), kvListValue); + + Value result = map.get(ExtendedAttributeKey.valueKey("key")); + @SuppressWarnings("unchecked") + List resultList = (List) result.getValue(); + assertThat(resultList).hasSize(2); + assertThat(resultList.get(0).getValue().getValue()).isEqualTo("short"); + assertThat(resultList.get(1).getValue().getValue()).isEqualTo("this "); + } +} From 575535d4ce30633200aee8b0a66e0455a3975738 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Tue, 18 Nov 2025 14:43:51 -0800 Subject: [PATCH 19/28] simplify putValue implementation --- .../ArrayBackedExtendedAttributesBuilder.java | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java index e27ab81b83c..6fbd4067eef 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java @@ -56,28 +56,24 @@ public ExtendedAttributesBuilder put(ExtendedAttributeKey key, T value) { return this; } + @SuppressWarnings("unchecked") private void putValue(ExtendedAttributeKey key, Value valueObj) { // Convert VALUE type to narrower type when possible String keyName = key.getKey(); switch (valueObj.getType()) { case STRING: - data.add(stringKey(keyName)); - data.add(valueObj.getValue()); + put(stringKey(keyName), ((Value) valueObj).getValue()); return; case LONG: - data.add(longKey(keyName)); - data.add(valueObj.getValue()); + put(longKey(keyName), ((Value) valueObj).getValue()); return; case DOUBLE: - data.add(doubleKey(keyName)); - data.add(valueObj.getValue()); + put(doubleKey(keyName), ((Value) valueObj).getValue()); return; case BOOLEAN: - data.add(booleanKey(keyName)); - data.add(valueObj.getValue()); + put(booleanKey(keyName), ((Value) valueObj).getValue()); return; case ARRAY: - @SuppressWarnings("unchecked") List> arrayValues = (List>) valueObj.getValue(); ExtendedAttributeType attributeType = attributeType(arrayValues); switch (attributeType) { @@ -86,32 +82,28 @@ private void putValue(ExtendedAttributeKey key, Value valueObj) { for (Value v : arrayValues) { strings.add((String) v.getValue()); } - data.add(stringArrayKey(keyName)); - data.add(strings); + put(stringArrayKey(keyName), strings); return; case LONG_ARRAY: List longs = new ArrayList<>(); for (Value v : arrayValues) { longs.add((Long) v.getValue()); } - data.add(longArrayKey(keyName)); - data.add(longs); + put(longArrayKey(keyName), longs); return; case DOUBLE_ARRAY: List doubles = new ArrayList<>(); for (Value v : arrayValues) { doubles.add((Double) v.getValue()); } - data.add(doubleArrayKey(keyName)); - data.add(doubles); + put(doubleArrayKey(keyName), doubles); return; case BOOLEAN_ARRAY: List booleans = new ArrayList<>(); for (Value v : arrayValues) { booleans.add((Boolean) v.getValue()); } - data.add(booleanArrayKey(keyName)); - data.add(booleans); + put(booleanArrayKey(keyName), booleans); return; case VALUE: // Not coercible (empty, non-homogeneous, or unsupported element type) From f1ffad886964a0a70bddc06258b13b3d60995c85 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Tue, 18 Nov 2025 14:47:14 -0800 Subject: [PATCH 20/28] initialize array lists with correct size --- .../common/ArrayBackedExtendedAttributesBuilder.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java index 6fbd4067eef..c1e4736647d 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java @@ -78,28 +78,28 @@ private void putValue(ExtendedAttributeKey key, Value valueObj) { ExtendedAttributeType attributeType = attributeType(arrayValues); switch (attributeType) { case STRING_ARRAY: - List strings = new ArrayList<>(); + List strings = new ArrayList<>(arrayValues.size()); for (Value v : arrayValues) { strings.add((String) v.getValue()); } put(stringArrayKey(keyName), strings); return; case LONG_ARRAY: - List longs = new ArrayList<>(); + List longs = new ArrayList<>(arrayValues.size()); for (Value v : arrayValues) { longs.add((Long) v.getValue()); } put(longArrayKey(keyName), longs); return; case DOUBLE_ARRAY: - List doubles = new ArrayList<>(); + List doubles = new ArrayList<>(arrayValues.size()); for (Value v : arrayValues) { doubles.add((Double) v.getValue()); } put(doubleArrayKey(keyName), doubles); return; case BOOLEAN_ARRAY: - List booleans = new ArrayList<>(); + List booleans = new ArrayList<>(arrayValues.size()); for (Value v : arrayValues) { booleans.add((Boolean) v.getValue()); } From 6480d0e909ce1f0c083e9b455f41bff11b844813 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Tue, 18 Nov 2025 15:16:34 -0800 Subject: [PATCH 21/28] Add test for non-existent array type to improve code coverage --- .../incubator/common/ExtendedAttributesTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java index beff501826e..eb47ed769b7 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java @@ -707,4 +707,17 @@ void complexValueWithNestedArray() { attributes.forEach(entriesSeen::put); assertThat(entriesSeen).containsExactly(entry(valueKey("key"), nestedArray)); } + + @Test + void getNonExistentArrayType() { + // Test the code path where we look for an array type that doesn't exist + ExtendedAttributes attributes = + ExtendedAttributes.builder().put("key", "value").build(); + + // Looking for an array type when only a string exists should return null + assertThat(attributes.get(stringArrayKey("key"))).isNull(); + assertThat(attributes.get(longArrayKey("key"))).isNull(); + assertThat(attributes.get(doubleArrayKey("key"))).isNull(); + assertThat(attributes.get(booleanArrayKey("key"))).isNull(); + } } From 22e857dfeff9c1a351b78d2a2d73285cedb09286 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Tue, 18 Nov 2025 15:20:25 -0800 Subject: [PATCH 22/28] spotless --- .../api/incubator/common/ExtendedAttributesTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java index eb47ed769b7..2d94aca5556 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java @@ -711,8 +711,7 @@ void complexValueWithNestedArray() { @Test void getNonExistentArrayType() { // Test the code path where we look for an array type that doesn't exist - ExtendedAttributes attributes = - ExtendedAttributes.builder().put("key", "value").build(); + ExtendedAttributes attributes = ExtendedAttributes.builder().put("key", "value").build(); // Looking for an array type when only a string exists should return null assertThat(attributes.get(stringArrayKey("key"))).isNull(); From b91d3bb04d936fb9bb6844172b6240de7030dca7 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Tue, 18 Nov 2025 15:26:11 -0800 Subject: [PATCH 23/28] Move warning to where users will see it --- .../api/incubator/common/ExtendedAttributeKey.java | 13 ++++++++++++- .../api/incubator/common/ExtendedAttributeType.java | 10 ---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java index 10e2c113b50..4f48dbd8973 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java @@ -106,7 +106,18 @@ static ExtendedAttributeKey extendedAttributesKey(String key return InternalExtendedAttributeKeyImpl.create(key, ExtendedAttributeType.EXTENDED_ATTRIBUTES); } - /** Returns a new ExtendedAttributeKey for {@link Value} valued attributes. */ + /** + * Returns a new ExtendedAttributeKey for {@link Value} valued attributes. + * + *

      Simple attributes ({@link ExtendedAttributeType#STRING}, {@link ExtendedAttributeType#LONG}, + * {@link ExtendedAttributeType#DOUBLE}, {@link ExtendedAttributeType#BOOLEAN}, {@link + * ExtendedAttributeType#STRING_ARRAY}, {@link ExtendedAttributeType#LONG_ARRAY}, {@link + * ExtendedAttributeType#DOUBLE_ARRAY}, {@link ExtendedAttributeType#BOOLEAN_ARRAY}) SHOULD be + * used whenever possible. Instrumentations SHOULD assume that backends do not index individual + * properties of complex attributes, that querying or aggregating on such properties is + * inefficient and complicated, and that reporting complex attributes carries higher performance + * overhead. + */ static ExtendedAttributeKey> valueKey(String key) { return InternalExtendedAttributeKeyImpl.create(key, ExtendedAttributeType.VALUE); } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java index b21747d3860..26e655e9ab2 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java @@ -30,15 +30,5 @@ public enum ExtendedAttributeType { */ @Deprecated EXTENDED_ATTRIBUTES, - /** - * Simple attributes ({@link ExtendedAttributeType#STRING}, {@link ExtendedAttributeType#LONG}, - * {@link ExtendedAttributeType#DOUBLE}, {@link ExtendedAttributeType#BOOLEAN}, {@link - * ExtendedAttributeType#STRING_ARRAY}, {@link ExtendedAttributeType#LONG_ARRAY}, {@link - * ExtendedAttributeType#DOUBLE_ARRAY}, {@link ExtendedAttributeType#BOOLEAN_ARRAY}) SHOULD be - * used whenever possible. Instrumentations SHOULD assume that backends do not index individual - * properties of complex attributes, that querying or aggregating on such properties is - * inefficient and complicated, and that reporting complex attributes carries higher performance - * overhead. - */ VALUE; } From d44e727421cbc9073afa697183ab5c12dd8837c5 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 24 Nov 2025 20:05:11 -0800 Subject: [PATCH 24/28] Don't allocate when already valid length --- .../java/io/opentelemetry/sdk/internal/AttributeUtil.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java index f852f262430..665f6b556a9 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java @@ -156,6 +156,10 @@ private static Value applyValueLengthLimit(Value value, int lengthLimit) { return Value.of(truncated); } else if (type == ValueType.ARRAY) { List> array = (List>) value.getValue(); + boolean allValidLength = allMatch(array, element -> isValidLengthValue(element, lengthLimit)); + if (allValidLength) { + return value; + } List> result = new ArrayList<>(array.size()); for (Value element : array) { result.add(applyValueLengthLimit(element, lengthLimit)); @@ -163,6 +167,10 @@ private static Value applyValueLengthLimit(Value value, int lengthLimit) { return Value.of(result); } else if (type == ValueType.KEY_VALUE_LIST) { List kvList = (List) value.getValue(); + boolean allValidLength = allMatch(kvList, kv -> isValidLengthValue(kv.getValue(), lengthLimit)); + if (allValidLength) { + return value; + } List result = new ArrayList<>(kvList.size()); for (KeyValue kv : kvList) { result.add(KeyValue.of(kv.getKey(), applyValueLengthLimit(kv.getValue(), lengthLimit))); From c6b9858bc2ecd6f8992fb9665a87e5d7519b5547 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 24 Nov 2025 20:06:27 -0800 Subject: [PATCH 25/28] Remove unnecessary suppress warnings --- .../api/incubator/common/ExtendedAttributesBuilder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java index 87415573ebf..0f4b9c942e2 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java @@ -135,7 +135,6 @@ default ExtendedAttributesBuilder put(String key, boolean value) { * instead. */ @Deprecated - @SuppressWarnings("deprecation") default ExtendedAttributesBuilder put(String key, ExtendedAttributes value) { return put(ExtendedAttributeKey.extendedAttributesKey(key), value); } From 28fedf2cc933108278202f010fc2e40861eb34f8 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 24 Nov 2025 20:09:52 -0800 Subject: [PATCH 26/28] Remove test of deprecated feature --- .../logs/ExtendedLogsBridgeApiUsageTest.java | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java index e3e17cff3dc..b4b28cdd88f 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java @@ -105,11 +105,6 @@ private static String flipCoin() { AttributeKey> booleanArrKey = AttributeKey.booleanArrayKey("acme.boolean_array"); AttributeKey> doubleArrKey = AttributeKey.doubleArrayKey("acme.double_array"); - // Extended keys - @SuppressWarnings("deprecation") // Supporting deprecated EXTENDED_ATTRIBUTES until removed - ExtendedAttributeKey mapKey = - ExtendedAttributeKey.extendedAttributesKey("acme.map"); - // VALUE key ExtendedAttributeKey> valueKey = ExtendedAttributeKey.valueKey("acme.value"); @@ -128,9 +123,6 @@ void extendedAttributesUsage() { .put(longArrKey, Arrays.asList(1L, 2L)) .put(booleanArrKey, Arrays.asList(true, false)) .put(doubleArrKey, Arrays.asList(1.1, 2.2)) - .put( - mapKey, - ExtendedAttributes.builder().put("childStr", "value").put("childLong", 1L).build()) .put( valueKey, Value.of( @@ -147,9 +139,6 @@ void extendedAttributesUsage() { assertThat(extendedAttributes.get(longArrKey)).isEqualTo(Arrays.asList(1L, 2L)); assertThat(extendedAttributes.get(booleanArrKey)).isEqualTo(Arrays.asList(true, false)); assertThat(extendedAttributes.get(doubleArrKey)).isEqualTo(Arrays.asList(1.1, 2.2)); - assertThat(extendedAttributes.get(mapKey)) - .isEqualTo( - ExtendedAttributes.builder().put("childStr", "value").put("childLong", 1L).build()); assertThat(extendedAttributes.get(valueKey)) .isEqualTo( Value.of( @@ -164,7 +153,6 @@ void extendedAttributesUsage() { // acme.double_array(DOUBLE_ARRAY): [1.1, 2.2] // acme.long(LONG): 1 // acme.long_array(LONG_ARRAY): [1, 2] - // acme.map(EXTENDED_ATTRIBUTES): {childLong=1, childStr="value"} // acme.string(STRING): value // acme.string_array(STRING_ARRAY): [value1, value2] // acme.value(VALUE): [KeyValue{key=childStr, value=StringValue{value=value}}, @@ -178,7 +166,6 @@ void extendedAttributesUsage() { } @Test - @SuppressWarnings("deprecation") // testing deprecated code void logRecordBuilder_ExtendedAttributes() { InMemoryLogRecordExporter exporter = InMemoryLogRecordExporter.create(); SdkLoggerProvider loggerProvider = @@ -199,9 +186,6 @@ void logRecordBuilder_ExtendedAttributes() { .setAttribute(longArrKey, Arrays.asList(1L, 2L)) .setAttribute(booleanArrKey, Arrays.asList(true, false)) .setAttribute(doubleArrKey, Arrays.asList(1.1, 2.2)) - .setAttribute( - mapKey, - ExtendedAttributes.builder().put("childStr", "value").put("childLong", 1L).build()) .setAttribute( valueKey, Value.of( @@ -245,12 +229,6 @@ void logRecordBuilder_ExtendedAttributes() { .put(longArrKey, Arrays.asList(1L, 2L)) .put(booleanArrKey, Arrays.asList(true, false)) .put(doubleArrKey, Arrays.asList(1.1, 2.2)) - .put( - mapKey, - ExtendedAttributes.builder() - .put("childStr", "value") - .put("childLong", 1L) - .build()) .put( valueKey, Value.of( From 587e7e3c0d5e52e325444361beba382a74284fab Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 24 Nov 2025 20:14:25 -0800 Subject: [PATCH 27/28] spotless --- .../main/java/io/opentelemetry/sdk/internal/AttributeUtil.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java index 665f6b556a9..f605da71446 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java @@ -167,7 +167,8 @@ private static Value applyValueLengthLimit(Value value, int lengthLimit) { return Value.of(result); } else if (type == ValueType.KEY_VALUE_LIST) { List kvList = (List) value.getValue(); - boolean allValidLength = allMatch(kvList, kv -> isValidLengthValue(kv.getValue(), lengthLimit)); + boolean allValidLength = + allMatch(kvList, kv -> isValidLengthValue(kv.getValue(), lengthLimit)); if (allValidLength) { return value; } From 9b9ea1674556fe6ffca21d682453e733af088e4a Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 24 Nov 2025 20:29:37 -0800 Subject: [PATCH 28/28] Don't call deprecated method --- .../api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java index b4b28cdd88f..80cd7a16a9c 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java @@ -202,7 +202,7 @@ void logRecordBuilder_ExtendedAttributes() { // Optionally access standard attributes, which filters out any extended attribute // types - assertThat(extendedLogRecordData.getAttributes()) + assertThat(logRecordData.getAttributes()) .isEqualTo( Attributes.builder() .put(strKey, "value")