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..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 @@ -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,118 @@ 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; } + @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: + put(stringKey(keyName), ((Value) valueObj).getValue()); + return; + case LONG: + put(longKey(keyName), ((Value) valueObj).getValue()); + return; + case DOUBLE: + put(doubleKey(keyName), ((Value) valueObj).getValue()); + return; + case BOOLEAN: + put(booleanKey(keyName), ((Value) valueObj).getValue()); + return; + case ARRAY: + List> arrayValues = (List>) valueObj.getValue(); + ExtendedAttributeType attributeType = attributeType(arrayValues); + switch (attributeType) { + case STRING_ARRAY: + 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<>(arrayValues.size()); + for (Value v : arrayValues) { + longs.add((Long) v.getValue()); + } + put(longArrayKey(keyName), longs); + return; + case DOUBLE_ARRAY: + 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<>(arrayValues.size()); + for (Value v : arrayValues) { + booleans.add((Boolean) v.getValue()); + } + put(booleanArrayKey(keyName), 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/ExtendedAttributeKey.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java index 13357a31a56..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 @@ -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; @@ -93,8 +94,31 @@ 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); } + + /** + * 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 8d2c67181b6..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 @@ -22,5 +22,13 @@ public enum ExtendedAttributeType { LONG_ARRAY, DOUBLE_ARRAY, // Extended types unique to ExtendedAttributes - EXTENDED_ATTRIBUTES; + /** + * 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, + 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..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 @@ -18,8 +18,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. + * 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 @@ -60,11 +60,60 @@ 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 + * 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 + * 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 +122,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 +145,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 1e0de3b4c38..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 @@ -17,6 +17,7 @@ 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; @@ -34,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,7 +131,10 @@ default ExtendedAttributesBuilder put(String key, boolean value) { * pre-allocate your keys, if possible. * * @return this Builder + * @deprecated Use {@link #put(ExtendedAttributeKey, Object)} with {@link Value#of(java.util.Map)} + * instead. */ + @Deprecated 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 e07f72f0121..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: @@ -139,6 +140,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..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 @@ -80,6 +81,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..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 @@ -5,21 +5,34 @@ 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 static org.assertj.core.api.Assertions.entry; 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; 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; +@SuppressWarnings("deprecation") // Testing deprecated EXTENDED_ATTRIBUTES until removed class ExtendedAttributesTest { @ParameterizedTest @@ -116,7 +129,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 +228,9 @@ private static Stream attributesArgs() { ImmutableMap.builder() .put("key", ImmutableMap.builder().put("child", "value").build()) .build()), + Arguments.of( + ExtendedAttributes.builder().put(valueKey("key"), Value.of("value")).build(), + ImmutableMap.builder().put("key", "value").build()), Arguments.of( ExtendedAttributes.builder() .put(ExtendedAttributeKey.stringKey("key"), "value") @@ -255,6 +274,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").build()), // Multiple entries Arguments.of( ExtendedAttributes.builder() @@ -268,6 +292,7 @@ private static Stream attributesArgs() { .put("key8", 1L, 2L) .put("key9", 1.1, 2.2) .put("key10", ExtendedAttributes.builder().put("child", "value").build()) + .put(valueKey("key11"), Value.of("value")) .build(), ImmutableMap.builder() .put("key1", "value1") @@ -280,6 +305,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") .build())); } @@ -316,6 +342,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 +383,340 @@ 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); } + + @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 + 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(); + } + + @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)); + } + + @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(); + } } 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..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 @@ -11,6 +11,8 @@ 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; import io.opentelemetry.api.logs.Logger; @@ -103,9 +105,8 @@ private static String flipCoin() { AttributeKey> booleanArrKey = AttributeKey.booleanArrayKey("acme.boolean_array"); AttributeKey> doubleArrKey = AttributeKey.doubleArrayKey("acme.double_array"); - // Extended keys - ExtendedAttributeKey mapKey = - ExtendedAttributeKey.extendedAttributesKey("acme.map"); + // VALUE key + ExtendedAttributeKey> valueKey = ExtendedAttributeKey.valueKey("acme.value"); @Test @SuppressLogger(ExtendedLogsBridgeApiUsageTest.class) @@ -123,8 +124,10 @@ void extendedAttributesUsage() { .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()) + valueKey, + Value.of( + KeyValue.of("childStr", Value.of("value")), + KeyValue.of("childLong", Value.of(1L)))) .build(); // Retrieval @@ -136,9 +139,11 @@ 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)) + assertThat(extendedAttributes.get(valueKey)) .isEqualTo( - ExtendedAttributes.builder().put("childStr", "value").put("childLong", 1L).build()); + Value.of( + KeyValue.of("childStr", Value.of("value")), + KeyValue.of("childLong", Value.of(1L)))); // Iteration // Output: @@ -148,9 +153,10 @@ 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}}, + // KeyValue{key=childLong, value=LongValue{value=1}}] extendedAttributes.forEach( (extendedAttributeKey, object) -> logger.info( @@ -160,7 +166,6 @@ void extendedAttributesUsage() { } @Test - @SuppressWarnings("deprecation") // testing deprecated code void logRecordBuilder_ExtendedAttributes() { InMemoryLogRecordExporter exporter = InMemoryLogRecordExporter.create(); SdkLoggerProvider loggerProvider = @@ -182,8 +187,9 @@ void logRecordBuilder_ExtendedAttributes() { .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()) + valueKey, + Value.of( + 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(); @@ -196,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") @@ -224,11 +230,10 @@ void logRecordBuilder_ExtendedAttributes() { .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()) + valueKey, + Value.of( + 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/ExtendedAttributeKeyValueStatelessMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ExtendedAttributeKeyValueStatelessMarshaler.java index de31139abcb..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 @@ -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; @@ -140,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) { @@ -174,13 +176,17 @@ 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. throw new IllegalArgumentException("Unsupported attribute type."); } - @SuppressWarnings("unchecked") + // Supporting deprecated EXTENDED_ATTRIBUTES type until removed + @SuppressWarnings({"unchecked", "deprecation"}) @Override public void writeTo( Serializer output, @@ -220,6 +226,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..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 @@ -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; @@ -80,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()) { @@ -116,6 +118,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. 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..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; @@ -49,6 +51,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}; @@ -90,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 @@ -118,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())); @@ -162,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())); } 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..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 @@ -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,57 @@ 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(); + 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)); + } + 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))); + } + 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 "); + } +}