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 539fc2f6393..d44022965ab 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,7 +7,9 @@ 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; @@ -45,9 +47,117 @@ 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 1872808898a..834394c97e3 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; @@ -42,11 +51,114 @@ 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; } + @SuppressWarnings("unchecked") + private void putValue(AttributeKey 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(); + AttributeType 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; + default: + throw new IllegalArgumentException("Unexpected array attribute type: " + attributeType); + } + case KEY_VALUE_LIST: + case BYTES: + case EMPTY: + // Keep as VALUE type + data.add(key); + data.add(valueObj); + } + } + + /** + * 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: + case EMPTY: + return AttributeType.VALUE; + } + throw new IllegalArgumentException("Unsupported element type: " + elementType); + } + @Override @SuppressWarnings({"unchecked", "rawtypes"}) // Safe: Attributes guarantees iteration over matching AttributeKey / value pairs. 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..978c41c9ffa 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,19 @@ static AttributeKey> longArrayKey(String key) { static AttributeKey> doubleArrayKey(String key) { return InternalAttributeKeyImpl.create(key, AttributeType.DOUBLE_ARRAY); } + + /** + * Returns a new ExtendedAttributeKey for {@link Value} valued 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. + */ + 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/Attributes.java b/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java index 2a9d43793a7..de1e624f361 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,11 +33,58 @@ @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. + * + *

Note: this method will automatically return the corresponding {@link + * io.opentelemetry.api.common.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}). + */ @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. */ @@ -46,7 +93,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 6623d470137..5548cdc4296 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; @@ -39,6 +40,40 @@ 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); @@ -164,6 +199,18 @@ default AttributesBuilder put(String key, boolean... value) { return put(booleanArrayKey(key), toList(value)); } + /** + * Puts a {@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) { + return put(valueKey(key), value); + } + /** * Puts all the provided attributes into this Builder. * diff --git a/api/all/src/main/java/io/opentelemetry/api/common/KeyValueList.java b/api/all/src/main/java/io/opentelemetry/api/common/KeyValueList.java index 42801205564..9035f39bb42 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/KeyValueList.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/KeyValueList.java @@ -55,6 +55,13 @@ public String asString() { .collect(joining(", ", "[", "]")); } + @Override + public String toProtoJson() { + StringBuilder sb = new StringBuilder(); + ProtoJson.append(sb, this); + return sb.toString(); + } + @Override public String toString() { return "KeyValueList{" + asString() + "}"; diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ProtoJson.java b/api/all/src/main/java/io/opentelemetry/api/common/ProtoJson.java new file mode 100644 index 00000000000..b757d955b63 --- /dev/null +++ b/api/all/src/main/java/io/opentelemetry/api/common/ProtoJson.java @@ -0,0 +1,132 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +import java.nio.ByteBuffer; +import java.util.Base64; +import java.util.List; + +final class ProtoJson { + + private static final char[] HEX_DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + @SuppressWarnings("unchecked") + static void append(StringBuilder sb, Value value) { + switch (value.getType()) { + case STRING: + appendString(sb, (String) value.getValue()); + break; + case LONG: + sb.append(value.getValue()); + break; + case DOUBLE: + appendDouble(sb, (Double) value.getValue()); + break; + case BOOLEAN: + sb.append(value.getValue()); + break; + case ARRAY: + appendArray(sb, (List>) value.getValue()); + break; + case KEY_VALUE_LIST: + appendMap(sb, (List) value.getValue()); + break; + case BYTES: + appendBytes(sb, (ByteBuffer) value.getValue()); + break; + case EMPTY: + sb.append("null"); + break; + } + } + + private static void appendString(StringBuilder sb, String value) { + sb.append('"'); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + switch (c) { + case '"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + default: + if (c < 0x20) { + // Control characters must be escaped as \\uXXXX + sb.append("\\u"); + sb.append(HEX_DIGITS[(c >> 12) & 0xF]); + sb.append(HEX_DIGITS[(c >> 8) & 0xF]); + sb.append(HEX_DIGITS[(c >> 4) & 0xF]); + sb.append(HEX_DIGITS[c & 0xF]); + } else { + sb.append(c); + } + } + } + sb.append('"'); + } + + private static void appendDouble(StringBuilder sb, double value) { + if (Double.isNaN(value)) { + sb.append("\"NaN\""); + } else if (Double.isInfinite(value)) { + sb.append(value > 0 ? "\"Infinity\"" : "\"-Infinity\""); + } else { + sb.append(value); + } + } + + private static void appendBytes(StringBuilder sb, ByteBuffer value) { + byte[] bytes = new byte[value.remaining()]; + value.duplicate().get(bytes); + sb.append('"').append(Base64.getEncoder().encodeToString(bytes)).append('"'); + } + + private static void appendArray(StringBuilder sb, List> values) { + sb.append('['); + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + sb.append(','); + } + append(sb, values.get(i)); + } + sb.append(']'); + } + + private static void appendMap(StringBuilder sb, List values) { + sb.append('{'); + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + sb.append(','); + } + KeyValue kv = values.get(i); + appendString(sb, kv.getKey()); + sb.append(':'); + append(sb, kv.getValue()); + } + sb.append('}'); + } + + private ProtoJson() {} +} diff --git a/api/all/src/main/java/io/opentelemetry/api/common/Value.java b/api/all/src/main/java/io/opentelemetry/api/common/Value.java index a29be801e27..e68706d0769 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/Value.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/Value.java @@ -26,6 +26,7 @@ * are type {@link Value}, arrays can contain primitives, complex types like maps or arrays, * or any combination. *

  • Raw bytes via {@link #of(byte[])} + *
  • An empty value via {@link #empty()} * * *

    Currently, Value is only used as an argument for {@link @@ -84,6 +85,11 @@ static Value> of(Map> value) { return KeyValueList.createFromMap(value); } + /** Returns an empty {@link Value}. */ + static Value empty() { + return ValueEmpty.create(); + } + /** Returns the type of this {@link Value}. Useful for building switch statements. */ ValueType getType(); @@ -101,6 +107,7 @@ static Value> of(Map> value) { *

  • {@link ValueType#KEY_VALUE_LIST} returns {@link List} of {@link KeyValue} *
  • {@link ValueType#BYTES} returns read only {@link ByteBuffer}. See {@link * ByteBuffer#asReadOnlyBuffer()}. + *
  • {@link ValueType#EMPTY} returns {@code null} * */ T getValue(); @@ -113,6 +120,32 @@ static Value> of(Map> value) { *

    WARNING: No guarantees are made about the encoding of this string response. It MAY change in * a future minor release. If you need a reliable string encoding, write your own serializer. */ - // TODO(jack-berg): Should this be a JSON encoding? + // TODO deprecate in favor of toString() or toProtoJson()? String asString(); + + /** + * Returns a JSON encoding of this {@link Value}. + * + *

    The output follows the ProtoJSON + * specification: + * + *

      + *
    • {@link ValueType#STRING} JSON string (including escaping and surrounding quotes) + *
    • {@link ValueType#BOOLEAN} JSON boolean ({@code true} or {@code false}) + *
    • {@link ValueType#LONG} JSON number + *
    • {@link ValueType#DOUBLE} JSON number, or {@code "NaN"}, {@code "Infinity"}, {@code + * "-Infinity"} for special values + *
    • {@link ValueType#ARRAY} JSON array (e.g. {@code [1,"two",true]}) + *
    • {@link ValueType#KEY_VALUE_LIST} JSON object (e.g. {@code {"key1":"value1","key2":2}}) + *
    • {@link ValueType#BYTES} JSON string (including surrounding double quotes) containing + * base64 encoded bytes + *
    • {@link ValueType#EMPTY} JSON {@code null} (the string {@code "null"} without the + * surrounding quotes) + *
    + * + * @return a JSON encoding of this value + */ + default String toProtoJson() { + return "\"unimplemented\""; + } } diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ValueArray.java b/api/all/src/main/java/io/opentelemetry/api/common/ValueArray.java index 55c9e5f42b7..4f9173b379f 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ValueArray.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueArray.java @@ -47,6 +47,13 @@ public String asString() { return value.stream().map(Value::asString).collect(joining(", ", "[", "]")); } + @Override + public String toProtoJson() { + StringBuilder sb = new StringBuilder(); + ProtoJson.append(sb, this); + return sb.toString(); + } + @Override public String toString() { return "ValueArray{" + asString() + "}"; diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ValueBoolean.java b/api/all/src/main/java/io/opentelemetry/api/common/ValueBoolean.java index a4364d414df..ae1f3997361 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ValueBoolean.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueBoolean.java @@ -34,6 +34,13 @@ public String asString() { return String.valueOf(value); } + @Override + public String toProtoJson() { + StringBuilder sb = new StringBuilder(); + ProtoJson.append(sb, this); + return sb.toString(); + } + @Override public String toString() { return "ValueBoolean{" + asString() + "}"; diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ValueBytes.java b/api/all/src/main/java/io/opentelemetry/api/common/ValueBytes.java index 8d925cd174d..6fa4f4cfaac 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ValueBytes.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueBytes.java @@ -38,6 +38,13 @@ public String asString() { return Base64.getEncoder().encodeToString(raw); } + @Override + public String toProtoJson() { + StringBuilder sb = new StringBuilder(); + ProtoJson.append(sb, this); + return sb.toString(); + } + @Override public String toString() { return "ValueBytes{" + asString() + "}"; diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ValueDouble.java b/api/all/src/main/java/io/opentelemetry/api/common/ValueDouble.java index 21f13dd7e78..7bcf5162387 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ValueDouble.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueDouble.java @@ -34,6 +34,13 @@ public String asString() { return String.valueOf(value); } + @Override + public String toProtoJson() { + StringBuilder sb = new StringBuilder(); + ProtoJson.append(sb, this); + return sb.toString(); + } + @Override public String toString() { return "ValueDouble{" + asString() + "}"; diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java b/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java new file mode 100644 index 00000000000..ec0e1d23db9 --- /dev/null +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +final class ValueEmpty implements Value { + + private static final ValueEmpty INSTANCE = new ValueEmpty(); + + private ValueEmpty() {} + + static Value create() { + return INSTANCE; + } + + @Override + public ValueType getType() { + return ValueType.EMPTY; + } + + @Override + public Void getValue() { + return null; + } + + @Override + public String asString() { + return ""; + } + + @Override + public String toProtoJson() { + return "null"; + } + + @Override + public String toString() { + return "ValueEmpty{}"; + } + + @Override + public boolean equals(Object o) { + return o instanceof ValueEmpty; + } + + @Override + public int hashCode() { + return 0; + } +} diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ValueLong.java b/api/all/src/main/java/io/opentelemetry/api/common/ValueLong.java index 8cd1bca4bf9..921f4cea48e 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ValueLong.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueLong.java @@ -34,6 +34,13 @@ public String asString() { return String.valueOf(value); } + @Override + public String toProtoJson() { + StringBuilder sb = new StringBuilder(); + ProtoJson.append(sb, this); + return sb.toString(); + } + @Override public String toString() { return "ValueLong{" + asString() + "}"; diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ValueString.java b/api/all/src/main/java/io/opentelemetry/api/common/ValueString.java index 726cb27dee3..05b690bb6c3 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ValueString.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueString.java @@ -35,6 +35,13 @@ public String asString() { return value; } + @Override + public String toProtoJson() { + StringBuilder sb = new StringBuilder(); + ProtoJson.append(sb, this); + return sb.toString(); + } + @Override public String toString() { return "ValueString{" + value + "}"; diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ValueType.java b/api/all/src/main/java/io/opentelemetry/api/common/ValueType.java index d7a60722a55..8299c6eebcb 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ValueType.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueType.java @@ -19,5 +19,6 @@ public enum ValueType { DOUBLE, ARRAY, KEY_VALUE_LIST, - BYTES + BYTES, + EMPTY } 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..d44eac6da8a 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; @@ -298,7 +299,7 @@ void builderWithAttributeKeyList() { .put(longKey("long"), 10) .put(stringArrayKey("anotherString"), "value1", "value2", "value3") .put(longArrayKey("anotherLong"), 10L, 20L, 30L) - .put(booleanArrayKey("anotherBoolean"), true, false, true) + .put(valueKey("value"), Value.of(new byte[] {1, 2, 3})) .build(); Attributes wantAttributes = @@ -311,8 +312,8 @@ void builderWithAttributeKeyList() { Arrays.asList("value1", "value2", "value3"), longArrayKey("anotherLong"), Arrays.asList(10L, 20L, 30L), - booleanArrayKey("anotherBoolean"), - Arrays.asList(true, false, true)); + valueKey("value"), + Value.of(new byte[] {1, 2, 3})); assertThat(attributes).isEqualTo(wantAttributes); AttributesBuilder newAttributes = attributes.toBuilder(); @@ -328,8 +329,8 @@ void builderWithAttributeKeyList() { Arrays.asList("value1", "value2", "value3"), longArrayKey("anotherLong"), Arrays.asList(10L, 20L, 30L), - booleanArrayKey("anotherBoolean"), - Arrays.asList(true, false, true), + valueKey("value"), + Value.of(new byte[] {1, 2, 3}), stringKey("newKey"), "newValue")); // Original not mutated. @@ -357,6 +358,352 @@ void builder_arrayTypes() { booleanArrayKey("boolean"), Arrays.asList(false, true))); } + @Test + void valueStoredAsString() { + // 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 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 valueStoredAsLong() { + // 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 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 valueStoredAsDouble() { + // 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(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 valueStoredAsBoolean() { + // 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 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 valueStoredAsStringArray() { + // 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(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 valueStoredAsLongArray() { + // 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(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 valueStoredAsDoubleArray() { + // 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(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 valueStoredAsBooleanArray() { + // 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(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 complexValueWithKeyValueList() { + // KEY_VALUE_LIST should be kept as VALUE type + Value kvListValue = Value.of(Collections.emptyMap()); + Attributes attributes = Attributes.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}); + Attributes attributes = Attributes.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))); + Attributes attributes = Attributes.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"))))); + Attributes attributes = Attributes.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 + Attributes attributes = Attributes.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(); + } + + @Test + void simpleAttributeRetrievedAsValue() { + Attributes attributes = + Attributes.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() { + Attributes attributes = + Attributes.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 valueWithKeyValueList() { + // KEY_VALUE_LIST should be kept as VALUE type + Value kvListValue = Value.of(Collections.emptyMap()); + Attributes attributes = Attributes.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 entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(valueKey("key"), kvListValue)); + } + + @Test + void valueWithBytes() { + // BYTES should be kept as VALUE type + Value bytesValue = Value.of(new byte[] {1, 2, 3}); + Attributes attributes = Attributes.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 entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(valueKey("key"), bytesValue)); + } + + @Test + void valueWithNonHomogeneousArray() { + // Non-homogeneous array should be kept as VALUE type + Value mixedArray = Value.of(Arrays.asList(Value.of("string"), Value.of(123L))); + Attributes attributes = Attributes.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 entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(valueKey("key"), mixedArray)); + } + + @Test + void valueWithNestedArray() { + // 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"))))); + Attributes attributes = Attributes.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 entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(valueKey("key"), nestedArray)); + } + @Test @SuppressWarnings("unchecked") void get_Null() { @@ -379,7 +726,7 @@ void get() { Attributes.of(stringKey("string"), "value", booleanKey("boolean"), true); assertThat(twoElements.get(booleanKey("boolean"))).isEqualTo(true); assertThat(twoElements.get(stringKey("string"))).isEqualTo("value"); - Attributes fourElements = + Attributes fiveElements = Attributes.of( stringKey("string"), "value", @@ -388,12 +735,16 @@ void get() { longKey("long"), 1L, stringArrayKey("array"), - Arrays.asList("one", "two", "three")); - assertThat(fourElements.get(stringArrayKey("array"))) + Arrays.asList("one", "two", "three"), + valueKey("value"), + Value.of(new byte[] {1, 2, 3})); + assertThat(fiveElements.get(stringArrayKey("array"))) .isEqualTo(Arrays.asList("one", "two", "three")); - assertThat(threeElements.get(booleanKey("boolean"))).isEqualTo(true); - assertThat(threeElements.get(stringKey("string"))).isEqualTo("value"); - assertThat(threeElements.get(longKey("long"))).isEqualTo(1L); + assertThat(fiveElements.get(booleanKey("boolean"))).isEqualTo(true); + assertThat(fiveElements.get(stringKey("string"))).isEqualTo("value"); + assertThat(fiveElements.get(longKey("long"))).isEqualTo(1L); + assertThat(fiveElements.get(valueKey("value"))).isEqualTo(Value.of(new byte[] {1, 2, 3})); + assertThat(fiveElements.get(valueKey("value")).getType()).isEqualTo(ValueType.BYTES); } @Test @@ -429,24 +780,26 @@ void nullsAreNoOps() { builder.put("arrayDouble", doubles); boolean[] booleans = {true}; builder.put("arrayBool", booleans); - assertThat(builder.build().size()).isEqualTo(9); + Value value = Value.of(new byte[] {1, 2, 3}); + builder.put(valueKey("value"), value); + assertThat(builder.build().size()).isEqualTo(10); - // note: currently these are no-op calls; that behavior is not required, so if it needs to - // change, that is fine. builder.put(stringKey("attrValue"), null); builder.put("string", (String) null); builder.put("arrayString", (String[]) null); builder.put("arrayLong", (long[]) null); builder.put("arrayDouble", (double[]) null); builder.put("arrayBool", (boolean[]) null); + builder.put(valueKey("value"), null); Attributes attributes = builder.build(); - assertThat(attributes.size()).isEqualTo(9); + assertThat(attributes.size()).isEqualTo(10); assertThat(attributes.get(stringKey("string"))).isEqualTo("string"); assertThat(attributes.get(stringArrayKey("arrayString"))).isEqualTo(singletonList("string")); assertThat(attributes.get(longArrayKey("arrayLong"))).isEqualTo(singletonList(10L)); assertThat(attributes.get(doubleArrayKey("arrayDouble"))).isEqualTo(singletonList(1.0d)); assertThat(attributes.get(booleanArrayKey("arrayBool"))).isEqualTo(singletonList(true)); + assertThat(attributes.get(valueKey("value"))).isEqualTo(Value.of(new byte[] {1, 2, 3})); } @Test @@ -590,4 +943,43 @@ void emptyValueIsValid() { Attributes attributes = Attributes.of(key, ""); assertThat(attributes.get(key)).isEqualTo(""); } + + @Test + void getValueAttribute_KeyNameMatching() { + // Test the getValueAttribute method's key name matching logic + Attributes attributes = + Attributes.builder() + .put(valueKey("key1"), Value.of(new byte[] {1, 2, 3})) + .put("key2", "value2") + .put(valueKey("key3"), Value.of(Collections.emptyMap())) + .build(); + + // When looking for array type with key1, should not find it (it's VALUE with BYTES) + assertThat(attributes.get(stringArrayKey("key1"))).isNull(); + + // When looking for array type with key2, should not find it (it's STRING, not VALUE) + assertThat(attributes.get(longArrayKey("key2"))).isNull(); + + // Verify VALUE types can be retrieved + assertThat(attributes.get(valueKey("key1"))).isEqualTo(Value.of(new byte[] {1, 2, 3})); + assertThat(attributes.get(valueKey("key3"))).isEqualTo(Value.of(Collections.emptyMap())); + } + + @Test + void emptyArrayValueNotStoredAsTypedArray() { + // When empty array is stored as VALUE, it should not be found when looking for + // the VALUE attribute with non-empty array + Attributes attributes = + Attributes.builder().put(valueKey("empty"), Value.of(Collections.emptyList())).build(); + + // Should return empty list for typed array lookups (testing isEmptyArray branch) + assertThat(attributes.get(stringArrayKey("empty"))).isEmpty(); + assertThat(attributes.get(longArrayKey("empty"))).isEmpty(); + + // Non-array VALUE types should not trigger the empty array logic + Attributes nonArrayAttrs = + Attributes.builder().put(valueKey("bytes"), Value.of(new byte[] {1, 2})).build(); + assertThat(nonArrayAttrs.get(stringArrayKey("bytes"))).isNull(); + assertThat(nonArrayAttrs.get(longArrayKey("bytes"))).isNull(); + } } diff --git a/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java b/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java new file mode 100644 index 00000000000..52fba6279c6 --- /dev/null +++ b/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java @@ -0,0 +1,392 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ValueToProtoJsonTest { + + @Test + void valueString_basic() { + assertThat(Value.of("hello").toProtoJson()).isEqualTo("\"hello\""); + } + + @Test + void valueString_empty() { + assertThat(Value.of("").toProtoJson()).isEqualTo("\"\""); + } + + @Test + void valueString_withEscapes() { + assertThat(Value.of("line1\nline2\ttab").toProtoJson()).isEqualTo("\"line1\\nline2\\ttab\""); + } + + @Test + void valueString_withQuotes() { + assertThat(Value.of("say \"hello\"").toProtoJson()).isEqualTo("\"say \\\"hello\\\"\""); + } + + @Test + void valueString_withBackslash() { + assertThat(Value.of("path\\to\\file").toProtoJson()).isEqualTo("\"path\\\\to\\\\file\""); + } + + @Test + void valueString_withControlCharacters() { + assertThat(Value.of("\u0000\u0001\u001F").toProtoJson()).isEqualTo("\"\\u0000\\u0001\\u001f\""); + } + + @Test + void valueString_unicode() { + assertThat(Value.of("Hello δΈ–η•Œ 🌍").toProtoJson()).isEqualTo("\"Hello δΈ–η•Œ 🌍\""); + } + + @Test + void valueBoolean_true() { + assertThat(Value.of(true).toProtoJson()).isEqualTo("true"); + } + + @Test + void valueBoolean_false() { + assertThat(Value.of(false).toProtoJson()).isEqualTo("false"); + } + + @Test + void valueLong_positive() { + assertThat(Value.of(42L).toProtoJson()).isEqualTo("42"); + } + + @Test + void valueLong_negative() { + assertThat(Value.of(-123L).toProtoJson()).isEqualTo("-123"); + } + + @Test + void valueLong_zero() { + assertThat(Value.of(0L).toProtoJson()).isEqualTo("0"); + } + + @Test + void valueLong_maxValue() { + assertThat(Value.of(Long.MAX_VALUE).toProtoJson()).isEqualTo("9223372036854775807"); + } + + @Test + void valueLong_minValue() { + assertThat(Value.of(Long.MIN_VALUE).toProtoJson()).isEqualTo("-9223372036854775808"); + } + + @Test + void valueDouble_regular() { + assertThat(Value.of(3.14).toProtoJson()).isEqualTo("3.14"); + } + + @Test + void valueDouble_negative() { + assertThat(Value.of(-2.5).toProtoJson()).isEqualTo("-2.5"); + } + + @Test + void valueDouble_zero() { + assertThat(Value.of(0.0).toProtoJson()).isEqualTo("0.0"); + } + + @Test + void valueDouble_negativeZero() { + assertThat(Value.of(-0.0).toProtoJson()).isEqualTo("-0.0"); + } + + @Test + void valueDouble_nan() { + assertThat(Value.of(Double.NaN).toProtoJson()).isEqualTo("\"NaN\""); + } + + @Test + void valueDouble_positiveInfinity() { + assertThat(Value.of(Double.POSITIVE_INFINITY).toProtoJson()).isEqualTo("\"Infinity\""); + } + + @Test + void valueDouble_negativeInfinity() { + assertThat(Value.of(Double.NEGATIVE_INFINITY).toProtoJson()).isEqualTo("\"-Infinity\""); + } + + @Test + void valueDouble_scientificNotation() { + assertThat(Value.of(1.23e10).toProtoJson()).isEqualTo("1.23E10"); + } + + @Test + void valueDouble_verySmall() { + assertThat(Value.of(1.23e-10).toProtoJson()).isEqualTo("1.23E-10"); + } + + @Test + void valueBytes_empty() { + assertThat(Value.of(new byte[] {}).toProtoJson()).isEqualTo("\"\""); + } + + @Test + void valueBytes_regular() { + byte[] bytes = new byte[] {0, 1, 2, Byte.MAX_VALUE, Byte.MIN_VALUE}; + assertThat(Value.of(bytes).toProtoJson()) + .isEqualTo('"' + Base64.getEncoder().encodeToString(bytes) + '"'); + } + + @Test + void valueEmpty() { + assertThat(Value.empty().toProtoJson()).isEqualTo("null"); + } + + @Test + @SuppressWarnings("ExplicitArrayForVarargs") + void valueArray_empty() { + assertThat(Value.of(new Value[] {}).toProtoJson()).isEqualTo("[]"); + } + + @Test + void valueArray_singleElement() { + assertThat(Value.of(Value.of("test")).toProtoJson()).isEqualTo("[\"test\"]"); + } + + @Test + void valueArray_multipleStrings() { + assertThat(Value.of(Value.of("a"), Value.of("b"), Value.of("c")).toProtoJson()) + .isEqualTo("[\"a\",\"b\",\"c\"]"); + } + + @Test + void valueArray_multipleNumbers() { + assertThat(Value.of(Value.of(1L), Value.of(2L), Value.of(3L)).toProtoJson()) + .isEqualTo("[1,2,3]"); + } + + @Test + void valueArray_mixedTypes() { + assertThat( + Value.of( + Value.of("string"), + Value.of(42L), + Value.of(3.14), + Value.of(true), + Value.of(false), + Value.empty()) + .toProtoJson()) + .isEqualTo("[\"string\",42,3.14,true,false,null]"); + } + + @Test + void valueArray_nested() { + assertThat( + Value.of( + Value.of("outer"), + Value.of(Value.of("inner1"), Value.of("inner2")), + Value.of(42L)) + .toProtoJson()) + .isEqualTo("[\"outer\",[\"inner1\",\"inner2\"],42]"); + } + + @Test + void valueArray_deeplyNested() { + assertThat( + Value.of(Value.of(Value.of(Value.of(Value.of(Value.of("deep"))))), Value.of("shallow")) + .toProtoJson()) + .isEqualTo("[[[[[\"deep\"]]]],\"shallow\"]"); + } + + @Test + @SuppressWarnings("ExplicitArrayForVarargs") + void valueKeyValueList_empty() { + assertThat(Value.of(new KeyValue[] {}).toProtoJson()).isEqualTo("{}"); + } + + @Test + void valueKeyValueList_singleEntry() { + assertThat(Value.of(KeyValue.of("key", Value.of("value"))).toProtoJson()) + .isEqualTo("{\"key\":\"value\"}"); + } + + @Test + void valueKeyValueList_multipleEntries() { + assertThat( + Value.of( + KeyValue.of("name", Value.of("Alice")), + KeyValue.of("age", Value.of(30L)), + KeyValue.of("active", Value.of(true))) + .toProtoJson()) + .isEqualTo("{\"name\":\"Alice\",\"age\":30,\"active\":true}"); + } + + @Test + void valueKeyValueList_nestedMap() { + assertThat( + Value.of( + KeyValue.of("outer", Value.of("value")), + KeyValue.of( + "inner", + Value.of( + KeyValue.of("nested1", Value.of("a")), + KeyValue.of("nested2", Value.of("b"))))) + .toProtoJson()) + .isEqualTo("{\"outer\":\"value\",\"inner\":{\"nested1\":\"a\",\"nested2\":\"b\"}}"); + } + + @Test + void valueKeyValueList_withArray() { + assertThat( + Value.of( + KeyValue.of("name", Value.of("test")), + KeyValue.of("items", Value.of(Value.of(1L), Value.of(2L), Value.of(3L)))) + .toProtoJson()) + .isEqualTo("{\"name\":\"test\",\"items\":[1,2,3]}"); + } + + @Test + void valueKeyValueList_allTypes() { + assertThat( + Value.of( + KeyValue.of("string", Value.of("text")), + KeyValue.of("long", Value.of(42L)), + KeyValue.of("double", Value.of(3.14)), + KeyValue.of("bool", Value.of(true)), + KeyValue.of("empty", Value.empty()), + KeyValue.of("bytes", Value.of(new byte[] {1, 2})), + KeyValue.of("array", Value.of(Value.of("a"), Value.of("b")))) + .toProtoJson()) + .isEqualTo( + "{\"string\":\"text\",\"long\":42,\"double\":3.14,\"bool\":true," + + "\"empty\":null,\"bytes\":\"AQI=\",\"array\":[\"a\",\"b\"]}"); + } + + @Test + void valueKeyValueList_fromMap() { + Map> map = new LinkedHashMap<>(); + map.put("key1", Value.of("value1")); + map.put("key2", Value.of(42L)); + assertThat(Value.of(map).toProtoJson()).isEqualTo("{\"key1\":\"value1\",\"key2\":42}"); + } + + @Test + void valueKeyValueList_keyWithSpecialCharacters() { + assertThat( + Value.of( + KeyValue.of("key with spaces", Value.of("value1")), + KeyValue.of("key\"with\"quotes", Value.of("value2")), + KeyValue.of("key\nwith\nnewlines", Value.of("value3"))) + .toProtoJson()) + .isEqualTo( + "{\"key with spaces\":\"value1\"," + + "\"key\\\"with\\\"quotes\":\"value2\"," + + "\"key\\nwith\\nnewlines\":\"value3\"}"); + } + + @Test + void complexNestedStructure() { + Value complexValue = + Value.of( + KeyValue.of("user", Value.of("Alice")), + KeyValue.of( + "scores", + Value.of( + Value.of(95L), + Value.of(87.5), + Value.of(92L), + Value.of(Double.NaN), + Value.of(Double.POSITIVE_INFINITY))), + KeyValue.of("passed", Value.of(true)), + KeyValue.of( + "metadata", + Value.of( + KeyValue.of("timestamp", Value.of(1234567890L)), + KeyValue.of( + "tags", + Value.of( + Value.of("important"), Value.of("reviewed"), Value.of("final")))))); + + assertThat(complexValue.toProtoJson()) + .isEqualTo( + "{\"user\":\"Alice\"," + + "\"scores\":[95,87.5,92,\"NaN\",\"Infinity\"]," + + "\"passed\":true," + + "\"metadata\":{\"timestamp\":1234567890," + + "\"tags\":[\"important\",\"reviewed\",\"final\"]}}"); + } + + @Test + void edgeCase_emptyStringKey() { + assertThat(Value.of(KeyValue.of("", Value.of("value"))).toProtoJson()) + .isEqualTo("{\"\":\"value\"}"); + } + + @Test + void edgeCase_multipleEmptyValues() { + assertThat(Value.of(Value.empty(), Value.empty(), Value.empty()).toProtoJson()) + .isEqualTo("[null,null,null]"); + } + + @Test + void edgeCase_arrayOfMaps() { + assertThat( + Value.of( + Value.of(KeyValue.of("id", Value.of(1L)), KeyValue.of("name", Value.of("A"))), + Value.of(KeyValue.of("id", Value.of(2L)), KeyValue.of("name", Value.of("B"))), + Value.of(KeyValue.of("id", Value.of(3L)), KeyValue.of("name", Value.of("C")))) + .toProtoJson()) + .isEqualTo( + "[{\"id\":1,\"name\":\"A\"},{\"id\":2,\"name\":\"B\"},{\"id\":3,\"name\":\"C\"}]"); + } + + @Test + @SuppressWarnings("ExplicitArrayForVarargs") + void edgeCase_mapWithEmptyArray() { + assertThat( + Value.of( + KeyValue.of("data", Value.of("test")), + KeyValue.of("items", Value.of(new Value[] {}))) + .toProtoJson()) + .isEqualTo("{\"data\":\"test\",\"items\":[]}"); + } + + @Test + @SuppressWarnings("ExplicitArrayForVarargs") + void edgeCase_mapWithEmptyMap() { + assertThat( + Value.of( + KeyValue.of("data", Value.of("test")), + KeyValue.of("metadata", Value.of(new KeyValue[] {}))) + .toProtoJson()) + .isEqualTo("{\"data\":\"test\",\"metadata\":{}}"); + } + + @Test + void defaultImplementation_returnsUnimplemented() { + // Create a custom Value implementation that doesn't override toProtoJson() + Value customValue = + new Value() { + @Override + public ValueType getType() { + return ValueType.STRING; + } + + @Override + public String getValue() { + return "test"; + } + + @Override + public String asString() { + return "test"; + } + }; + + assertThat(customValue.toProtoJson()).isEqualTo("\"unimplemented\""); + } +} 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 c1e4736647d..ec2d4454236 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 @@ -120,6 +120,7 @@ private void putValue(ExtendedAttributeKey key, Value valueObj) { } case KEY_VALUE_LIST: case BYTES: + case EMPTY: // Keep as VALUE type data.add(key); data.add(valueObj); @@ -154,6 +155,7 @@ private static ExtendedAttributeType attributeType(List> arrayValues) { case ARRAY: case KEY_VALUE_LIST: case BYTES: + case EMPTY: return ExtendedAttributeType.VALUE; } throw new IllegalArgumentException("Unsupported element type: " + elementType); 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..834c9653f39 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,8 +139,9 @@ public static AttributeKey toAttributeKey(ExtendedAttributeKey extende case DOUBLE_ARRAY: return InternalAttributeKeyImpl.create( extendedAttributeKey.getKey(), AttributeType.DOUBLE_ARRAY); - case EXTENDED_ATTRIBUTES: case VALUE: + return InternalAttributeKeyImpl.create(extendedAttributeKey.getKey(), AttributeType.VALUE); + case EXTENDED_ATTRIBUTES: return null; } throw new IllegalArgumentException( @@ -174,6 +175,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/common/ExtendedAttributeKeyTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKeyTest.java index ba148e7c9bf..4ff77c1144a 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 @@ -83,6 +83,9 @@ private static Stream attributeKeyArgs() { ExtendedAttributeType.EXTENDED_ATTRIBUTES, null), Arguments.of( - ExtendedAttributeKey.valueKey("key"), "key", ExtendedAttributeType.VALUE, null)); + ExtendedAttributeKey.valueKey("key"), + "key", + ExtendedAttributeType.VALUE, + AttributeKey.valueKey("key"))); } } 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 80cd7a16a9c..f5b5e7d0c39 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 @@ -213,6 +213,11 @@ void logRecordBuilder_ExtendedAttributes() { .put(longArrKey, Arrays.asList(1L, 2L)) .put(booleanArrKey, Arrays.asList(true, false)) .put(doubleArrKey, Arrays.asList(1.1, 2.2)) + .put( + AttributeKey.valueKey("acme.value"), + Value.of( + KeyValue.of("childStr", Value.of("value")), + KeyValue.of("childLong", Value.of(1L)))) .put("key1", "value") .put("key2", "value") .build()); diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index 9ca1c6c2e64..ce251801e76 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -1,2 +1,19 @@ Comparing source compatibility of opentelemetry-api-1.59.0-SNAPSHOT.jar against opentelemetry-api-1.58.0.jar -No changes. \ No newline at end of file +*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.common.AttributeKey (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + GENERIC TEMPLATES: === T:java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.common.AttributeKey> valueKey(java.lang.String) +*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.common.AttributesBuilder (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.common.AttributesBuilder put(java.lang.String, io.opentelemetry.api.common.Value) +*** MODIFIED ENUM: PUBLIC FINAL io.opentelemetry.api.common.AttributeType (compatible) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.common.AttributeType VALUE +*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.common.Value (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + GENERIC TEMPLATES: === T:java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.common.Value empty() + +++ NEW METHOD: PUBLIC(+) java.lang.String toProtoJson() +*** MODIFIED ENUM: PUBLIC FINAL io.opentelemetry.api.common.ValueType (compatible) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.common.ValueType EMPTY diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-testing.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-testing.txt index 758e352b94c..02c49bacd8b 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-testing.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-testing.txt @@ -1,2 +1,12 @@ Comparing source compatibility of opentelemetry-sdk-testing-1.59.0-SNAPSHOT.jar against opentelemetry-sdk-testing-1.58.0.jar -No changes. \ No newline at end of file +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.testing.assertj.AttributesAssert (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.testing.assertj.AttributesAssert containsEntry(java.lang.String, io.opentelemetry.api.common.Value) +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) STATIC(+) java.util.Map$Entry>,io.opentelemetry.api.common.Value> attributeEntry(java.lang.String, io.opentelemetry.api.common.Value) + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.testing.assertj.AttributeAssertion satisfies(io.opentelemetry.api.common.AttributeKey>, io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions$ValueAssertConsumer) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) STATIC(+) io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions$ValueAssertConsumer (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW INTERFACE: java.util.function.Consumer + +++ NEW SUPERCLASS: java.lang.Object diff --git a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java index f6f13467887..ab3d59b78b7 100644 --- a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java @@ -8,9 +8,12 @@ import static io.opentelemetry.api.common.AttributeKey.booleanKey; import static io.opentelemetry.api.common.AttributeKey.longKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.common.AttributeKey.valueKey; import com.google.common.io.Resources; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.KeyValue; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; @@ -59,8 +62,16 @@ abstract class TestDataExporter { .setSeverityText("INFO") .setTimestamp(100L, TimeUnit.NANOSECONDS) .setObservedTimestamp(200L, TimeUnit.NANOSECONDS) - .setAttributes(Attributes.of(stringKey("animal"), "cat", longKey("lives"), 9L)) - .setTotalAttributeCount(2) + .setAttributes( + Attributes.builder() + .put(stringKey("animal"), "cat") + .put(longKey("lives"), 9L) + .put(valueKey("bytes"), Value.of(new byte[] {1, 2, 3})) + .put(valueKey("map"), Value.of(KeyValue.of("nested", Value.of("value")))) + .put(valueKey("heterogeneousArray"), Value.of(Value.of("string"), Value.of(123L))) + .put(valueKey("empty"), Value.empty()) + .build()) + .setTotalAttributeCount(6) .setSpanContext( SpanContext.create( "12345678876543211234567887654322", @@ -103,7 +114,15 @@ abstract class TestDataExporter { .setStatus(StatusData.ok()) .setName("testSpan1") .setKind(SpanKind.INTERNAL) - .setAttributes(Attributes.of(stringKey("animal"), "cat", longKey("lives"), 9L)) + .setAttributes( + Attributes.builder() + .put(stringKey("animal"), "cat") + .put(longKey("lives"), 9L) + .put(valueKey("bytes"), Value.of(new byte[] {1, 2, 3})) + .put(valueKey("map"), Value.of(KeyValue.of("nested", Value.of("value")))) + .put(valueKey("heterogeneousArray"), Value.of(Value.of("string"), Value.of(123L))) + .put(valueKey("empty"), Value.empty()) + .build()) .setEvents( Collections.singletonList( EventData.create( @@ -155,7 +174,18 @@ abstract class TestDataExporter { AggregationTemporality.CUMULATIVE, Collections.singletonList( ImmutableDoublePointData.create( - 1, 2, Attributes.of(stringKey("cat"), "meow"), 4)))); + 1, + 2, + Attributes.builder() + .put(stringKey("cat"), "meow") + .put(valueKey("bytes"), Value.of(new byte[] {1, 2, 3})) + .put(valueKey("map"), Value.of(KeyValue.of("nested", Value.of("value")))) + .put( + valueKey("heterogeneousArray"), + Value.of(Value.of("string"), Value.of(123L))) + .put(valueKey("empty"), Value.empty()) + .build(), + 4)))); private static final MetricData METRIC2 = ImmutableMetricData.createDoubleSum( diff --git a/exporters/logging-otlp/src/test/resources/expected-logs-wrapper.json b/exporters/logging-otlp/src/test/resources/expected-logs-wrapper.json index 6557b719863..59a198ba9be 100644 --- a/exporters/logging-otlp/src/test/resources/expected-logs-wrapper.json +++ b/exporters/logging-otlp/src/test/resources/expected-logs-wrapper.json @@ -41,11 +41,52 @@ "stringValue": "cat" } }, + { + "key": "bytes", + "value": { + "bytesValue": "AQID" + } + }, + { + "key": "empty", + "value": { + } + }, + { + "key": "heterogeneousArray", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "string" + }, + { + "intValue": "123" + } + ] + } + } + }, { "key": "lives", "value": { "intValue": "9" } + }, + { + "key": "map", + "value": { + "kvlistValue": { + "values": [ + { + "key": "nested", + "value": { + "stringValue": "value" + } + } + ] + } + } } ], "traceId": "12345678876543211234567887654322", diff --git a/exporters/logging-otlp/src/test/resources/expected-logs.json b/exporters/logging-otlp/src/test/resources/expected-logs.json index b1d46cc8f5e..b781ab1c89b 100644 --- a/exporters/logging-otlp/src/test/resources/expected-logs.json +++ b/exporters/logging-otlp/src/test/resources/expected-logs.json @@ -39,11 +39,52 @@ "stringValue": "cat" } }, + { + "key": "bytes", + "value": { + "bytesValue": "AQID" + } + }, + { + "key": "empty", + "value": { + } + }, + { + "key": "heterogeneousArray", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "string" + }, + { + "intValue": "123" + } + ] + } + } + }, { "key": "lives", "value": { "intValue": "9" } + }, + { + "key": "map", + "value": { + "kvlistValue": { + "values": [ + { + "key": "nested", + "value": { + "stringValue": "value" + } + } + ] + } + } } ], "traceId": "12345678876543211234567887654322", diff --git a/exporters/logging-otlp/src/test/resources/expected-metrics-wrapper.json b/exporters/logging-otlp/src/test/resources/expected-metrics-wrapper.json index 9c1255a6279..34f3c856e5f 100644 --- a/exporters/logging-otlp/src/test/resources/expected-metrics-wrapper.json +++ b/exporters/logging-otlp/src/test/resources/expected-metrics-wrapper.json @@ -38,11 +38,51 @@ "asDouble": 4.0, "exemplars": [], "attributes": [ + { + "key": "bytes", + "value": { + "bytesValue": "AQID" + } + }, { "key": "cat", "value": { "stringValue": "meow" } + }, + { + "key": "empty", + "value": {} + }, + { + "key": "heterogeneousArray", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "string" + }, + { + "intValue": "123" + } + ] + } + } + }, + { + "key": "map", + "value": { + "kvlistValue": { + "values": [ + { + "key": "nested", + "value": { + "stringValue": "value" + } + } + ] + } + } } ] } diff --git a/exporters/logging-otlp/src/test/resources/expected-metrics.json b/exporters/logging-otlp/src/test/resources/expected-metrics.json index 1a05a682e56..6946dd2a11f 100644 --- a/exporters/logging-otlp/src/test/resources/expected-metrics.json +++ b/exporters/logging-otlp/src/test/resources/expected-metrics.json @@ -36,11 +36,51 @@ "asDouble": 4.0, "exemplars": [], "attributes": [ + { + "key": "bytes", + "value": { + "bytesValue": "AQID" + } + }, { "key": "cat", "value": { "stringValue": "meow" } + }, + { + "key": "empty", + "value": {} + }, + { + "key": "heterogeneousArray", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "string" + }, + { + "intValue": "123" + } + ] + } + } + }, + { + "key": "map", + "value": { + "kvlistValue": { + "values": [ + { + "key": "nested", + "value": { + "stringValue": "value" + } + } + ] + } + } } ] } diff --git a/exporters/logging-otlp/src/test/resources/expected-spans-wrapper.json b/exporters/logging-otlp/src/test/resources/expected-spans-wrapper.json index f1dd80ed9e8..aa5856bd3f8 100644 --- a/exporters/logging-otlp/src/test/resources/expected-spans-wrapper.json +++ b/exporters/logging-otlp/src/test/resources/expected-spans-wrapper.json @@ -40,11 +40,52 @@ "stringValue": "cat" } }, + { + "key": "bytes", + "value": { + "bytesValue": "AQID" + } + }, + { + "key": "empty", + "value": { + } + }, + { + "key": "heterogeneousArray", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "string" + }, + { + "intValue": "123" + } + ] + } + } + }, { "key": "lives", "value": { "intValue": "9" } + }, + { + "key": "map", + "value": { + "kvlistValue": { + "values": [ + { + "key": "nested", + "value": { + "stringValue": "value" + } + } + ] + } + } } ], "events": [ diff --git a/exporters/logging-otlp/src/test/resources/expected-spans.json b/exporters/logging-otlp/src/test/resources/expected-spans.json index 22e57949d90..dc33d9f75d8 100644 --- a/exporters/logging-otlp/src/test/resources/expected-spans.json +++ b/exporters/logging-otlp/src/test/resources/expected-spans.json @@ -38,11 +38,52 @@ "stringValue": "cat" } }, + { + "key": "bytes", + "value": { + "bytesValue": "AQID" + } + }, + { + "key": "empty", + "value": { + } + }, + { + "key": "heterogeneousArray", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "string" + }, + { + "intValue": "123" + } + ] + } + } + }, { "key": "lives", "value": { "intValue": "9" } + }, + { + "key": "map", + "value": { + "kvlistValue": { + "values": [ + { + "key": "nested", + "value": { + "stringValue": "value" + } + } + ] + } + } } ], "events": [ diff --git a/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/LoggingSpanExporterTest.java b/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/LoggingSpanExporterTest.java index 385784732ff..f849b97c570 100644 --- a/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/LoggingSpanExporterTest.java +++ b/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/LoggingSpanExporterTest.java @@ -8,10 +8,13 @@ import static io.opentelemetry.api.common.AttributeKey.booleanKey; import static io.opentelemetry.api.common.AttributeKey.longKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.common.AttributeKey.valueKey; import static org.assertj.core.api.Assertions.assertThat; import io.github.netmikey.logunit.api.LogCapturer; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.KeyValue; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.TraceFlags; @@ -53,7 +56,15 @@ class LoggingSpanExporterTest { .setStatus(StatusData.ok()) .setName("testSpan1") .setKind(SpanKind.INTERNAL) - .setAttributes(Attributes.of(stringKey("animal"), "cat", longKey("lives"), 9L)) + .setAttributes( + Attributes.builder() + .put(stringKey("animal"), "cat") + .put(longKey("lives"), 9L) + .put(valueKey("bytes"), Value.of(new byte[] {1, 2, 3})) + .put(valueKey("map"), Value.of(KeyValue.of("nested", Value.of("value")))) + .put(valueKey("heterogeneousArray"), Value.of(Value.of("string"), Value.of(123L))) + .put(valueKey("empty"), Value.empty()) + .build()) .setEvents( Collections.singletonList( EventData.create( @@ -104,7 +115,9 @@ void export() { .isEqualTo( "'testSpan1' : 12345678876543211234567887654321 8765432112345678 " + "INTERNAL [tracer: tracer1:] " - + "{animal=\"cat\", lives=9}"); + + "{animal=\"cat\", bytes=ValueBytes{AQID}, empty=ValueEmpty{}, " + + "heterogeneousArray=ValueArray{[string, 123]}, lives=9, " + + "map=KeyValueList{[nested=value]}}"); assertThat(logs.getEvents().get(1).getMessage()) .isEqualTo( "'testSpan2' : 12340000000043211234000000004321 8765000000005678 " diff --git a/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/SystemOutLogRecordExporterTest.java b/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/SystemOutLogRecordExporterTest.java index 2cf9b22f5cc..b9003c9cc34 100644 --- a/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/SystemOutLogRecordExporterTest.java +++ b/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/SystemOutLogRecordExporterTest.java @@ -7,10 +7,13 @@ import static io.opentelemetry.api.common.AttributeKey.longKey; 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 io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.KeyValue; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.TraceFlags; @@ -45,7 +48,9 @@ void format() { assertThat(output.toString()) .isEqualTo( "1970-08-07T10:00:00Z ERROR3 'message' : 00000000000000010000000000000002 0000000000000003 " - + "[scopeInfo: logTest:1.0] {amount=1, cheese=\"cheddar\"}"); + + "[scopeInfo: logTest:1.0] {amount=1, bytes=ValueBytes{AQID}, cheese=\"cheddar\", " + + "empty=ValueEmpty{}, heterogeneousArray=ValueArray{[string, 123]}, " + + "map=KeyValueList{[nested=value]}}"); } @Test @@ -72,7 +77,15 @@ private static LogRecordData sampleLog(long timestamp) { .setResource(Resource.empty()) .setInstrumentationScopeInfo( InstrumentationScopeInfo.builder("logTest").setVersion("1.0").build()) - .setAttributes(Attributes.of(stringKey("cheese"), "cheddar", longKey("amount"), 1L)) + .setAttributes( + Attributes.builder() + .put(stringKey("cheese"), "cheddar") + .put(longKey("amount"), 1L) + .put(valueKey("bytes"), Value.of(new byte[] {1, 2, 3})) + .put(valueKey("map"), Value.of(KeyValue.of("nested", Value.of("value")))) + .put(valueKey("heterogeneousArray"), Value.of(Value.of("string"), Value.of(123L))) + .put(valueKey("empty"), Value.empty()) + .build()) .setBody("message") .setSeverity(Severity.ERROR3) .setTimestamp(timestamp, TimeUnit.MILLISECONDS) diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AnyValueMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AnyValueMarshaler.java index 327ad471e4e..e2431889d37 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AnyValueMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AnyValueMarshaler.java @@ -38,6 +38,8 @@ public static MarshalerWithSize create(Value value) { return KeyValueListAnyValueMarshaler.create((List) value.getValue()); case BYTES: return BytesAnyValueMarshaler.create((ByteBuffer) value.getValue()); + case EMPTY: + return EmptyAnyValueMarshaler.INSTANCE; } throw new IllegalArgumentException("Unsupported Value type: " + value.getType()); } diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AnyValueStatelessMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AnyValueStatelessMarshaler.java index bad0d9060d5..9a441b26bbf 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AnyValueStatelessMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AnyValueStatelessMarshaler.java @@ -65,6 +65,9 @@ public void writeTo(Serializer output, Value value, MarshalerContext context) BytesAnyValueStatelessMarshaler.INSTANCE.writeTo( output, (ByteBuffer) value.getValue(), context); return; + case EMPTY: + // no field to write + return; } // Error prone ensures the switch statement is complete, otherwise only can happen with // unaligned versions which are not supported. @@ -102,6 +105,8 @@ public int getBinarySerializedSize(Value value, MarshalerContext context) { case BYTES: return BytesAnyValueStatelessMarshaler.INSTANCE.getBinarySerializedSize( (ByteBuffer) value.getValue(), context); + case EMPTY: + return 0; } // 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/AttributeKeyValueStatelessMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeKeyValueStatelessMarshaler.java index 3fb1f7c25f6..2644dde82b4 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 @@ -7,6 +7,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributeType; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.internal.InternalAttributeKeyImpl; import io.opentelemetry.exporter.internal.marshal.MarshalerContext; import io.opentelemetry.exporter.internal.marshal.MarshalerUtil; @@ -100,6 +101,9 @@ public int getBinarySerializedSize( (List) value, AttributeArrayAnyValueStatelessMarshaler.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. @@ -136,6 +140,9 @@ public void writeTo( AttributeArrayAnyValueStatelessMarshaler.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/EmptyAnyValueMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/EmptyAnyValueMarshaler.java new file mode 100644 index 00000000000..d7ee4242475 --- /dev/null +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/EmptyAnyValueMarshaler.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal.otlp; + +import io.opentelemetry.exporter.internal.marshal.MarshalerWithSize; +import io.opentelemetry.exporter.internal.marshal.Serializer; + +/** + * A Marshaler of empty {@link io.opentelemetry.proto.common.v1.internal.AnyValue}. Represents an + * AnyValue with no field set. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +final class EmptyAnyValueMarshaler extends MarshalerWithSize { + + static final EmptyAnyValueMarshaler INSTANCE = new EmptyAnyValueMarshaler(); + + private EmptyAnyValueMarshaler() { + super(0); + } + + @Override + public void writeTo(Serializer output) { + // no field to write + } +} 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. diff --git a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/logs/LowAllocationLogRequestMarshalerTest.java b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/logs/LowAllocationLogRequestMarshalerTest.java index 4890e02dd66..8b86a907b64 100644 --- a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/logs/LowAllocationLogRequestMarshalerTest.java +++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/logs/LowAllocationLogRequestMarshalerTest.java @@ -9,6 +9,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.logs.Severity; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.TraceFlags; @@ -39,6 +41,11 @@ class LowAllocationLogRequestMarshalerTest { AttributeKey.doubleArrayKey("key_double_array"); private static final AttributeKey> KEY_BOOLEAN_ARRAY = AttributeKey.booleanArrayKey("key_boolean_array"); + private static final AttributeKey> KEY_BYTES = AttributeKey.valueKey("key_bytes"); + private static final AttributeKey> KEY_MAP = AttributeKey.valueKey("key_map"); + private static final AttributeKey> KEY_HETEROGENEOUS_ARRAY = + AttributeKey.valueKey("key_heterogeneous_array"); + private static final AttributeKey> KEY_EMPTY = AttributeKey.valueKey("key_empty"); private static final String BODY = "Hello world from this log..."; private static final Resource RESOURCE = @@ -52,6 +59,10 @@ class LowAllocationLogRequestMarshalerTest { .put(KEY_LONG_ARRAY, Arrays.asList(12L, 23L)) .put(KEY_DOUBLE_ARRAY, Arrays.asList(12.3, 23.1)) .put(KEY_BOOLEAN_ARRAY, Arrays.asList(true, false)) + .put(KEY_BYTES, Value.of(new byte[] {1, 2, 3})) + .put(KEY_MAP, Value.of(KeyValue.of("nested", Value.of("value")))) + .put(KEY_HETEROGENEOUS_ARRAY, Value.of(Value.of("string"), Value.of(123L))) + .put(KEY_EMPTY, Value.empty()) .build()); private static final InstrumentationScopeInfo INSTRUMENTATION_SCOPE_INFO = diff --git a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/metrics/LowAllocationMetricsRequestMarshalerTest.java b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/metrics/LowAllocationMetricsRequestMarshalerTest.java index b94205228b5..6ae3fa94a4f 100644 --- a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/metrics/LowAllocationMetricsRequestMarshalerTest.java +++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/metrics/LowAllocationMetricsRequestMarshalerTest.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.metrics.DoubleCounter; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.DoubleUpDownCounter; @@ -217,6 +219,14 @@ private static Collection metrics(Consumer metricProd .put( AttributeKey.booleanArrayKey("key_boolean_array"), Arrays.asList(true, false)) + .put(AttributeKey.valueKey("key_bytes"), Value.of(new byte[] {1, 2, 3})) + .put( + AttributeKey.valueKey("key_map"), + Value.of(KeyValue.of("nested", Value.of("value")))) + .put( + AttributeKey.valueKey("key_heterogeneous_array"), + Value.of(Value.of("string"), Value.of(123L))) + .put(AttributeKey.valueKey("key_empty"), Value.empty()) .build())) .build(); metricProducer.accept(meterProvider); diff --git a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/traces/LowAllocationTraceRequestMarshalerTest.java b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/traces/LowAllocationTraceRequestMarshalerTest.java index e868373d0ca..833c14c4a97 100644 --- a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/traces/LowAllocationTraceRequestMarshalerTest.java +++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/traces/LowAllocationTraceRequestMarshalerTest.java @@ -9,6 +9,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.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.TraceFlags; @@ -41,6 +43,11 @@ class LowAllocationTraceRequestMarshalerTest { AttributeKey.doubleArrayKey("key_double_array"); private static final AttributeKey> KEY_BOOLEAN_ARRAY = AttributeKey.booleanArrayKey("key_boolean_array"); + private static final AttributeKey> KEY_BYTES = AttributeKey.valueKey("key_bytes"); + private static final AttributeKey> KEY_MAP = AttributeKey.valueKey("key_map"); + private static final AttributeKey> KEY_HETEROGENEOUS_ARRAY = + AttributeKey.valueKey("key_heterogeneous_array"); + private static final AttributeKey> KEY_EMPTY = AttributeKey.valueKey("key_empty"); private static final AttributeKey LINK_ATTR_KEY = AttributeKey.stringKey("link_attr_key"); private static final Resource RESOURCE = @@ -54,6 +61,10 @@ class LowAllocationTraceRequestMarshalerTest { .put(KEY_LONG_ARRAY, Arrays.asList(12L, 23L)) .put(KEY_DOUBLE_ARRAY, Arrays.asList(12.3, 23.1)) .put(KEY_BOOLEAN_ARRAY, Arrays.asList(true, false)) + .put(KEY_BYTES, Value.of(new byte[] {1, 2, 3})) + .put(KEY_MAP, Value.of(KeyValue.of("nested", Value.of("value")))) + .put(KEY_HETEROGENEOUS_ARRAY, Value.of(Value.of("string"), Value.of(123L))) + .put(KEY_EMPTY, Value.empty()) .build()); private static final InstrumentationScopeInfo INSTRUMENTATION_SCOPE_INFO = diff --git a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/traces/TraceRequestMarshalerTest.java b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/traces/TraceRequestMarshalerTest.java index 9e00b634783..4f4e159dd63 100644 --- a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/traces/TraceRequestMarshalerTest.java +++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/traces/TraceRequestMarshalerTest.java @@ -18,6 +18,7 @@ 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.internal.OtelEncodingUtils; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanId; @@ -34,6 +35,7 @@ import io.opentelemetry.proto.common.v1.ArrayValue; import io.opentelemetry.proto.common.v1.InstrumentationScope; import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.common.v1.KeyValueList; import io.opentelemetry.proto.trace.v1.ResourceSpans; import io.opentelemetry.proto.trace.v1.ScopeSpans; import io.opentelemetry.proto.trace.v1.Span; @@ -147,8 +149,16 @@ void toProtoSpan(MarshalerSource marshalerSource) { .put("long_array", 12L, 23L) .put("double_array", 12.3, 23.1) .put("boolean_array", true, false) + .put("bytes", Value.of(new byte[] {1, 2, 3})) + .put( + "map", + Value.of( + io.opentelemetry.api.common.KeyValue.of( + "nested", Value.of("value")))) + .put("heterogeneousArray", Value.of(Value.of("string"), Value.of(123L))) + .put("empty", Value.empty()) .build()) - .setTotalAttributeCount(9) + .setTotalAttributeCount(13) .setEvents( Collections.singletonList( EventData.create(12347, "my_event", Attributes.empty()))) @@ -231,6 +241,40 @@ void toProtoSpan(MarshalerSource marshalerSource) { .addValues(AnyValue.newBuilder().setBoolValue(false).build()) .build()) .build()) + .build(), + KeyValue.newBuilder() + .setKey("bytes") + .setValue( + AnyValue.newBuilder() + .setBytesValue(ByteString.copyFrom(new byte[] {1, 2, 3})) + .build()) + .build(), + KeyValue.newBuilder().setKey("empty").setValue(AnyValue.newBuilder().build()).build(), + KeyValue.newBuilder() + .setKey("heterogeneousArray") + .setValue( + AnyValue.newBuilder() + .setArrayValue( + ArrayValue.newBuilder() + .addValues(AnyValue.newBuilder().setStringValue("string").build()) + .addValues(AnyValue.newBuilder().setIntValue(123).build()) + .build()) + .build()) + .build(), + KeyValue.newBuilder() + .setKey("map") + .setValue( + AnyValue.newBuilder() + .setKvlistValue( + KeyValueList.newBuilder() + .addValues( + KeyValue.newBuilder() + .setKey("nested") + .setValue( + AnyValue.newBuilder().setStringValue("value").build()) + .build()) + .build()) + .build()) .build()); assertThat(protoSpan.getDroppedAttributesCount()).isEqualTo(1); assertThat(protoSpan.getEventsList()) diff --git a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverter.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverter.java index 5209f91e47b..a32db09916f 100644 --- a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverter.java +++ b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverter.java @@ -12,6 +12,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributeType; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.internal.ThrottlingLogger; @@ -673,6 +674,8 @@ private static String toLabelValue(AttributeType type, Object attributeValue) { "Unexpected label value of %s for %s", attributeValue.getClass().getName(), type.name())); } + case VALUE: + return ((Value) attributeValue).toProtoJson(); } throw new IllegalStateException("Unrecognized AttributeType: " + type); } diff --git a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverterTest.java b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverterTest.java index 77f215504cb..3c968e97761 100644 --- a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverterTest.java +++ b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverterTest.java @@ -13,13 +13,13 @@ 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 org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.opentelemetry.api.common.AttributeType; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.KeyValue; +import io.opentelemetry.api.common.Value; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; import io.opentelemetry.sdk.metrics.data.MetricData; @@ -68,7 +68,6 @@ class Otel2PrometheusConverterTest { "(.|\\n)*# HELP (?.*)\n# TYPE (?.*)\n(?.*)\\{" + "otel_scope_foo=\"bar\",otel_scope_name=\"scope\"," + "otel_scope_schema_url=\"schemaUrl\",otel_scope_version=\"version\"}(.|\\n)*"); - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private final Otel2PrometheusConverter converter = new Otel2PrometheusConverter( @@ -319,7 +318,7 @@ void prometheusNameCollisionTest_Issue6277() { @ParameterizedTest @MethodSource("labelValueSerializationArgs") - void labelValueSerialization(Attributes attributes) { + void labelValueSerialization(Attributes attributes, String expectedValue) { MetricData metricData = createSampleMetricData("sample", "1", MetricDataType.LONG_SUM, attributes, null); @@ -330,37 +329,37 @@ void labelValueSerialization(Attributes attributes) { assertThat(metricSnapshot).isPresent(); Labels labels = metricSnapshot.get().getDataPoints().get(0).getLabels(); - attributes.forEach( - (key, value) -> { - String labelValue = labels.get(key.getKey()); - try { - String expectedValue = - key.getType() == AttributeType.STRING - ? (String) value - : OBJECT_MAPPER.writeValueAsString(value); - assertThat(labelValue).isEqualTo(expectedValue); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - }); + String labelValue = labels.get("key"); + assertThat(labelValue).isEqualTo(expectedValue); } private static Stream labelValueSerializationArgs() { return Stream.of( - Arguments.of(Attributes.of(stringKey("key"), "stringValue")), - Arguments.of(Attributes.of(booleanKey("key"), true)), - Arguments.of(Attributes.of(longKey("key"), Long.MAX_VALUE)), - Arguments.of(Attributes.of(doubleKey("key"), 0.12345)), + Arguments.of(Attributes.of(stringKey("key"), "stringValue"), "stringValue"), + Arguments.of(Attributes.of(booleanKey("key"), true), "true"), + Arguments.of(Attributes.of(longKey("key"), Long.MAX_VALUE), "9223372036854775807"), + Arguments.of(Attributes.of(doubleKey("key"), 0.12345), "0.12345"), Arguments.of( Attributes.of( stringArrayKey("key"), - Arrays.asList("stringValue1", "\"+\\\\\\+\b+\f+\n+\r+\t+" + (char) 0))), - Arguments.of(Attributes.of(booleanArrayKey("key"), Arrays.asList(true, false))), + Arrays.asList("stringValue1", "\"+\\\\\\+\b+\f+\n+\r+\t+" + (char) 0)), + "[\"stringValue1\",\"\\\"+\\\\\\\\\\\\+\\b+\\f+\\n+\\r+\\t+\\u0000\"]"), Arguments.of( - Attributes.of(longArrayKey("key"), Arrays.asList(Long.MIN_VALUE, Long.MAX_VALUE))), + Attributes.of(booleanArrayKey("key"), Arrays.asList(true, false)), "[true,false]"), Arguments.of( - Attributes.of( - doubleArrayKey("key"), Arrays.asList(Double.MIN_VALUE, Double.MAX_VALUE)))); + Attributes.of(longArrayKey("key"), Arrays.asList(Long.MIN_VALUE, Long.MAX_VALUE)), + "[-9223372036854775808,9223372036854775807]"), + Arguments.of( + Attributes.of(doubleArrayKey("key"), Arrays.asList(Double.MIN_VALUE, Double.MAX_VALUE)), + "[4.9E-324,1.7976931348623157E308]"), + Arguments.of(Attributes.of(valueKey("key"), Value.of(new byte[] {1, 2, 3})), "\"AQID\""), + Arguments.of( + Attributes.of(valueKey("key"), Value.of(KeyValue.of("nested", Value.of("value")))), + "{\"nested\":\"value\"}"), + Arguments.of( + Attributes.of(valueKey("key"), Value.of(Value.of("string"), Value.of(123L))), + "[\"string\",123]"), + Arguments.of(Attributes.of(valueKey("key"), Value.empty()), "null")); } static MetricData createSampleMetricData( diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/EventDataToAnnotation.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/EventDataToAnnotation.java index 9374ddc3204..3365bd0d4bf 100644 --- a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/EventDataToAnnotation.java +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/EventDataToAnnotation.java @@ -8,6 +8,7 @@ import static java.util.stream.Collectors.joining; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; import io.opentelemetry.sdk.trace.data.EventData; import java.util.List; @@ -43,6 +44,9 @@ private static String toValue(Object o) { return ((List) o) .stream().map(EventDataToAnnotation::toValue).collect(joining(",", "[", "]")); } + if (o instanceof Value) { + return ((Value) o).toProtoJson(); + } return String.valueOf(o); } } diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformer.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformer.java index 7728d88463f..cead6809643 100644 --- a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformer.java +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformer.java @@ -12,6 +12,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributeType; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; @@ -223,6 +224,8 @@ private static String valueToString(AttributeKey key, Object attributeValue) case LONG_ARRAY: case DOUBLE_ARRAY: return commaSeparated((List) attributeValue); + case VALUE: + return ((Value) attributeValue).toProtoJson(); } throw new IllegalStateException("Unknown attribute type: " + type); } diff --git a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/EventDataToAnnotationTest.java b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/EventDataToAnnotationTest.java index fa2cad0f284..429ccd06bf1 100644 --- a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/EventDataToAnnotationTest.java +++ b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/EventDataToAnnotationTest.java @@ -5,9 +5,12 @@ package io.opentelemetry.exporter.zipkin; +import static io.opentelemetry.api.common.AttributeKey.valueKey; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.KeyValue; +import io.opentelemetry.api.common.Value; import io.opentelemetry.sdk.trace.data.EventData; import org.junit.jupiter.api.Test; @@ -18,17 +21,21 @@ void basicConversion() { Attributes attrs = Attributes.builder() - .put("v1", "v1") - .put("v2", 12L) - .put("v3", 123.45) - .put("v4", false) - .put("v5", "foo", "bar", "baz") - .put("v6", 1, 2, 3) - .put("v7", 1.23, 3.45) - .put("v8", true, false, true) + .put("v01", "v1") + .put("v02", 12L) + .put("v03", 123.45) + .put("v04", false) + .put("v05", "foo", "bar", "baz") + .put("v06", 1, 2, 3) + .put("v07", 1.23, 3.45) + .put("v08", true, false, true) + .put(valueKey("v09"), Value.of(new byte[] {1, 2, 3})) + .put(valueKey("v10"), Value.of(KeyValue.of("nested", Value.of("value")))) + .put(valueKey("v11"), Value.of(Value.of("string"), Value.of(123L))) + .put(valueKey("v12"), Value.empty()) .build(); String expected = - "\"cat\":{\"v1\":\"v1\",\"v2\":12,\"v3\":123.45,\"v4\":false,\"v5\":[\"foo\",\"bar\",\"baz\"],\"v6\":[1,2,3],\"v7\":[1.23,3.45],\"v8\":[true,false,true]}"; + "\"cat\":{\"v01\":\"v1\",\"v02\":12,\"v03\":123.45,\"v04\":false,\"v05\":[\"foo\",\"bar\",\"baz\"],\"v06\":[1,2,3],\"v07\":[1.23,3.45],\"v08\":[true,false,true],\"v09\":\"AQID\",\"v10\":{\"nested\":\"value\"},\"v11\":[\"string\",123],\"v12\":null}"; EventData eventData = EventData.create(0, "cat", attrs); String result = EventDataToAnnotation.apply(eventData); diff --git a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformerTest.java b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformerTest.java index ab3801804cf..36f696f0c17 100644 --- a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformerTest.java +++ b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformerTest.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 io.opentelemetry.exporter.zipkin.ZipkinTestUtil.spanBuilder; import static io.opentelemetry.exporter.zipkin.ZipkinTestUtil.zipkinSpan; import static io.opentelemetry.exporter.zipkin.ZipkinTestUtil.zipkinSpanBuilder; @@ -20,6 +21,8 @@ import static org.mockito.Mockito.mock; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.KeyValue; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; @@ -363,11 +366,15 @@ void generateSpan_WithAttributes() { .put(stringArrayKey("stringArray"), Collections.singletonList("Hello")) .put(doubleArrayKey("doubleArray"), Arrays.asList(32.33d, -98.3d)) .put(longArrayKey("longArray"), Arrays.asList(33L, 999L)) + .put(valueKey("bytes"), Value.of(new byte[] {1, 2, 3})) + .put(valueKey("map"), Value.of(KeyValue.of("nested", Value.of("value")))) + .put(valueKey("heterogeneousArray"), Value.of(Value.of("string"), Value.of(123L))) + .put(valueKey("empty"), Value.empty()) .build(); SpanData data = spanBuilder() .setAttributes(attributes) - .setTotalAttributeCount(28) + .setTotalAttributeCount(32) .setTotalRecordedEvents(3) .setKind(SpanKind.CLIENT) .build(); @@ -383,6 +390,10 @@ void generateSpan_WithAttributes() { .putTag("stringArray", "Hello") .putTag("doubleArray", "32.33,-98.3") .putTag("longArray", "33,999") + .putTag("bytes", "\"AQID\"") + .putTag("map", "{\"nested\":\"value\"}") + .putTag("heterogeneousArray", "[\"string\",123]") + .putTag("empty", "null") .putTag(OtelToZipkinSpanTransformer.OTEL_STATUS_CODE, "OK") .putTag(OtelToZipkinSpanTransformer.OTEL_DROPPED_ATTRIBUTES_COUNT, "20") .putTag(OtelToZipkinSpanTransformer.OTEL_DROPPED_EVENTS_COUNT, "1") diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java index 1654a001a96..62a2e2219f4 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java @@ -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; @@ -21,6 +22,7 @@ import io.opentelemetry.api.common.AttributeType; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.common.Value; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; @@ -167,6 +169,24 @@ void create_NullEmptyArray() { assertThat(resource.getAttributes().size()).isEqualTo(8); } + @Test + void create_NullEmptyValue() { + AttributesBuilder attributes = Attributes.builder(); + + // Empty values should be maintained + attributes.put(valueKey("value"), Value.empty()); + + Resource resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(1); + + // Null values should be dropped + attributes.put(valueKey("dropNullValue"), null); + resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(1); + } + @Test void testResourceEquals() { Attributes attribute1 = Attributes.of(stringKey("a"), "1", stringKey("b"), "2"); diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerTest.java index 76b15234b3d..44c0e7dce43 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerTest.java @@ -9,6 +9,7 @@ import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; import static io.opentelemetry.api.common.AttributeKey.longArrayKey; import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.valueKey; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -19,6 +20,8 @@ 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.internal.StringUtils; import io.opentelemetry.api.logs.LogRecordBuilder; import io.opentelemetry.api.logs.Severity; @@ -87,6 +90,10 @@ void logRecordBuilder_maxAttributeLength() { .put(booleanArrayKey("booleanArray"), Arrays.asList(true, false)) .put(longArrayKey("longArray"), Arrays.asList(1L, 2L)) .put(doubleArrayKey("doubleArray"), Arrays.asList(1.0, 2.0)) + .put(valueKey("bytes"), Value.of(new byte[] {1, 2, 3})) + .put(valueKey("map"), Value.of(KeyValue.of("nested", Value.of("value")))) + .put(valueKey("heterogeneousArray"), Value.of(Value.of("string"), Value.of(123L))) + .put(valueKey("empty"), Value.empty()) .build()) .emit(); @@ -100,7 +107,11 @@ void logRecordBuilder_maxAttributeLength() { .containsEntry("stringArray", strVal, strVal) .containsEntry("booleanArray", true, false) .containsEntry("longArray", 1L, 2L) - .containsEntry("doubleArray", 1.0, 2.0); + .containsEntry("doubleArray", 1.0, 2.0) + .containsEntry(valueKey("bytes"), Value.of(new byte[] {1, 2, 3})) + .containsEntry(valueKey("map"), Value.of(KeyValue.of("nested", Value.of("value")))) + .containsEntry(valueKey("heterogeneousArray"), Value.of(Value.of("string"), Value.of(123L))) + .containsEntry(valueKey("empty"), Value.empty()); } @Test 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..bcd55090f25 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 @@ -9,6 +9,7 @@ import com.google.auto.value.AutoValue; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Value; import java.util.List; import java.util.function.Consumer; import javax.annotation.Nullable; @@ -58,6 +59,8 @@ static AttributeAssertion create( case LONG_ARRAY: case DOUBLE_ARRAY: return assertThat((List) value); + case VALUE: + return assertThat((Value) value); } throw new IllegalArgumentException("Unknown type for key " + key); } diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributesAssert.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributesAssert.java index 9c3d346b10f..0f3e5b195d6 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributesAssert.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributesAssert.java @@ -9,6 +9,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.Collections; import java.util.List; @@ -90,6 +91,11 @@ public AttributesAssert containsEntry(String key, Double... value) { return containsEntry(AttributeKey.doubleArrayKey(key), Arrays.asList(value)); } + /** Asserts the attributes have the given key and {@link Value} value. */ + public AttributesAssert containsEntry(String key, Value value) { + return containsEntry(AttributeKey.valueKey(key), value); + } + /** Asserts the attributes have the given key and string array value. */ public AttributesAssert containsEntryWithStringValuesOf(String key, Iterable value) { isNotNull(); 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..a369a5e5575 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,6 +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); } return this; } diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/OpenTelemetryAssertions.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/OpenTelemetryAssertions.java index 6d2e99735c2..d8da5a84edd 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/OpenTelemetryAssertions.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/OpenTelemetryAssertions.java @@ -7,6 +7,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.trace.data.EventData; @@ -24,6 +25,7 @@ import org.assertj.core.api.AbstractStringAssert; import org.assertj.core.api.Assertions; import org.assertj.core.api.ListAssert; +import org.assertj.core.api.ObjectAssert; /** * Entry point for assertion methods for OpenTelemetry types. To use type-specific assertions, @@ -138,6 +140,15 @@ public static Map.Entry>, List> attributeEntry Arrays.stream(value).boxed().collect(Collectors.toList())); } + /** + * Returns an attribute entry with a Value for use with {@link + * AttributesAssert#containsOnly(java.util.Map.Entry[])}. + */ + public static Map.Entry>, Value> attributeEntry( + String key, Value value) { + return new AbstractMap.SimpleImmutableEntry<>(AttributeKey.valueKey(key), value); + } + /** * Returns an {@link AttributeAssertion} that asserts the given {@code key} is present with a * value satisfying {@code assertion}. @@ -212,6 +223,15 @@ public static AttributeAssertion satisfies( return AttributeAssertion.create(key, assertion); } + /** + * Returns an {@link AttributeAssertion} that asserts the given {@code key} is present with a + * value satisfying {@code assertion}. + */ + public static AttributeAssertion satisfies( + AttributeKey> key, ValueAssertConsumer assertion) { + return AttributeAssertion.create(key, assertion); + } + /** * Returns an {@link AttributeAssertion} that asserts the given {@code key} is present with the * given {@code value}. @@ -247,6 +267,8 @@ public interface LongListAssertConsumer extends Consumer> {} public interface DoubleListAssertConsumer extends Consumer> {} + public interface ValueAssertConsumer extends Consumer>> {} + private static List toList(boolean... values) { Boolean[] boxed = new Boolean[values.length]; for (int i = 0; i < values.length; i++) { diff --git a/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/AssertUtilTest.java b/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/AssertUtilTest.java index d2f66094057..afd5941c572 100644 --- a/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/AssertUtilTest.java +++ b/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/AssertUtilTest.java @@ -10,6 +10,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 org.junit.jupiter.api.Test; @@ -19,6 +20,7 @@ class AssertUtilTest { private static final AttributeKey TEMPERATURE = AttributeKey.longKey("temperature"); private static final AttributeKey LENGTH = AttributeKey.doubleKey("length"); private static final AttributeKey> COLORS = AttributeKey.stringArrayKey("colors"); + private static final AttributeKey> BYTES = AttributeKey.valueKey("bytes"); private static final Attributes ATTRIBUTES = Attributes.builder() @@ -26,6 +28,7 @@ class AssertUtilTest { .put(TEMPERATURE, 30) .put(LENGTH, 1.2) .put(COLORS, Arrays.asList("red", "blue")) + .put(BYTES, Value.of(new byte[] {1, 2, 3})) .build(); @Test @@ -50,7 +53,8 @@ void assertAttributesShouldNotThrowIfAllAttributesMatch() { equalTo(WARM, true), equalTo(TEMPERATURE, 30L), equalTo(LENGTH, 1.2), - equalTo(COLORS, Arrays.asList("red", "blue"))); + equalTo(COLORS, Arrays.asList("red", "blue")), + equalTo(BYTES, Value.of(new byte[] {1, 2, 3}))); AssertUtil.assertAttributes(ATTRIBUTES, assertions); } @@ -78,7 +82,8 @@ void assertAttributesExactlyShouldNotThrowIfAllAttributesMatch() { equalTo(WARM, true), equalTo(TEMPERATURE, 30L), equalTo(LENGTH, 1.2), - equalTo(COLORS, Arrays.asList("red", "blue"))); + equalTo(COLORS, Arrays.asList("red", "blue")), + equalTo(BYTES, Value.of(new byte[] {1, 2, 3}))); AssertUtil.assertAttributesExactly(ATTRIBUTES, assertions); } diff --git a/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/LogAssertionsTest.java b/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/LogAssertionsTest.java index d15e3878cb4..0a144a24eef 100644 --- a/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/LogAssertionsTest.java +++ b/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/LogAssertionsTest.java @@ -53,6 +53,7 @@ public class LogAssertionsTest { .put("conditions", false, true) .put("scores", 0L, 1L) .put("coins", 0.01, 0.05, 0.1) + .put("bytes", Value.of(new byte[] {1, 2, 3})) .build(); private static final LogRecordData LOG_DATA = @@ -124,11 +125,12 @@ void passing() { attributeEntry("colors", "red", "blue"), attributeEntry("conditions", false, true), attributeEntry("scores", 0L, 1L), - attributeEntry("coins", 0.01, 0.05, 0.1)) + attributeEntry("coins", 0.01, 0.05, 0.1), + attributeEntry("bytes", Value.of(new byte[] {1, 2, 3}))) .hasAttributesSatisfying( attributes -> OpenTelemetryAssertions.assertThat(attributes) - .hasSize(8) + .hasSize(9) .containsEntry(stringKey("bear"), "mya") .hasEntrySatisfying(stringKey("bear"), value -> assertThat(value).hasSize(3)) .containsEntry("bear", "mya") @@ -145,6 +147,7 @@ void passing() { .containsEntryWithLongValuesOf("scores", Arrays.asList(0L, 1L)) .containsEntry("coins", 0.01, 0.05, 0.1) .containsEntryWithDoubleValuesOf("coins", Arrays.asList(0.01, 0.05, 0.1)) + .containsEntry("bytes", Value.of(new byte[] {1, 2, 3})) .containsKey(stringKey("bear")) .containsKey("bear") .containsOnly( @@ -155,7 +158,8 @@ void passing() { attributeEntry("colors", "red", "blue"), attributeEntry("conditions", false, true), attributeEntry("scores", 0L, 1L), - attributeEntry("coins", 0.01, 0.05, 0.1))) + attributeEntry("coins", 0.01, 0.05, 0.1), + attributeEntry("bytes", Value.of(new byte[] {1, 2, 3})))) .hasAttributesSatisfying( equalTo(stringKey("bear"), "mya"), equalTo(AttributeKey.booleanArrayKey("conditions"), Arrays.asList(false, true))) @@ -167,7 +171,8 @@ void passing() { equalTo(AttributeKey.stringArrayKey("colors"), Arrays.asList("red", "blue")), equalTo(AttributeKey.booleanArrayKey("conditions"), Arrays.asList(false, true)), equalTo(AttributeKey.longArrayKey("scores"), Arrays.asList(0L, 1L)), - equalTo(AttributeKey.doubleArrayKey("coins"), Arrays.asList(0.01, 0.05, 0.1))) + equalTo(AttributeKey.doubleArrayKey("coins"), Arrays.asList(0.01, 0.05, 0.1)), + equalTo(AttributeKey.valueKey("bytes"), Value.of(new byte[] {1, 2, 3}))) .hasTotalAttributeCount(999); } @@ -313,7 +318,8 @@ void logBodyAssertions() { KeyValue.of( "fooboola", Value.of(Value.of(true), Value.of(true), Value.of(true), Value.of(false))), - KeyValue.of("fooany", Value.of("grim")))) + KeyValue.of("fooany", Value.of("grim")), + KeyValue.of("foobytes", Value.of(new byte[] {1, 2, 3})))) .emit(); List logs = exporter.getFinishedLogRecordItems(); assertThat(logs).hasSize(1); @@ -326,6 +332,46 @@ void logBodyAssertions() { .hasBodyField("foolonga", 9, 0, 2, 1, 0) .hasBodyField("foodbla", 9.1, 0.2, 2.3, 1.4, 0.5) .hasBodyField("fooboola", true, true, true, false) - .hasBodyField("fooany", Value.of("grim")); + .hasBodyField("fooany", Value.of("grim")) + .hasBodyField("foobytes", Value.of(new byte[] {1, 2, 3})); + } + + @Test + void logBodyAssertions_withAttributeKeys() { + InMemoryLogRecordExporter exporter = InMemoryLogRecordExporter.create(); + SdkLoggerProvider loggerProvider = + SdkLoggerProvider.builder() + .addLogRecordProcessor(SimpleLogRecordProcessor.create(exporter)) + .build(); + Logger logger = loggerProvider.get("test.test"); + + logger + .logRecordBuilder() + .setBody( + Value.of( + KeyValue.of("strField", Value.of("value1")), + KeyValue.of("boolField", Value.of(false)), + KeyValue.of("longField", Value.of(42L)), + KeyValue.of("doubleField", Value.of(3.14)), + KeyValue.of("strArrayField", Value.of(Value.of("a"), Value.of("b"))), + KeyValue.of("boolArrayField", Value.of(Value.of(true), Value.of(false))), + KeyValue.of("longArrayField", Value.of(Value.of(1L), Value.of(2L))), + KeyValue.of("doubleArrayField", Value.of(Value.of(1.1), Value.of(2.2))), + KeyValue.of("bytes", Value.of(new byte[] {1, 2, 3})))) + .emit(); + + List logs = exporter.getFinishedLogRecordItems(); + assertThat(logs).hasSize(1); + + assertThat(logs.get(0)) + .hasBodyField(AttributeKey.stringKey("strField"), "value1") + .hasBodyField(AttributeKey.booleanKey("boolField"), false) + .hasBodyField(AttributeKey.longKey("longField"), 42L) + .hasBodyField(AttributeKey.doubleKey("doubleField"), 3.14) + .hasBodyField(AttributeKey.stringArrayKey("strArrayField"), Arrays.asList("a", "b")) + .hasBodyField(AttributeKey.booleanArrayKey("boolArrayField"), Arrays.asList(true, false)) + .hasBodyField(AttributeKey.longArrayKey("longArrayField"), Arrays.asList(1L, 2L)) + .hasBodyField(AttributeKey.doubleArrayKey("doubleArrayField"), Arrays.asList(1.1, 2.2)) + .hasBodyField(AttributeKey.valueKey("bytes"), Value.of(new byte[] {1, 2, 3})); } } diff --git a/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/MetricAssertionsTest.java b/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/MetricAssertionsTest.java index 299e4ab36fa..a6c5d4def3d 100644 --- a/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/MetricAssertionsTest.java +++ b/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/MetricAssertionsTest.java @@ -6,6 +6,7 @@ package io.opentelemetry.sdk.testing.assertj; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.common.AttributeKey.valueKey; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; @@ -16,6 +17,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceState; @@ -72,6 +74,7 @@ class MetricAssertionsTest { AttributeKey.booleanArrayKey("conditions"); private static final AttributeKey> SCORES = AttributeKey.longArrayKey("scores"); private static final AttributeKey> COINS = AttributeKey.doubleArrayKey("coins"); + private static final AttributeKey> BYTES = valueKey("bytes"); private static final Attributes ATTRIBUTES = Attributes.builder() @@ -83,6 +86,7 @@ class MetricAssertionsTest { .put(CONDITIONS, Arrays.asList(false, true)) .put(SCORES, Arrays.asList(0L, 1L)) .put(COINS, Arrays.asList(0.01, 0.05, 0.1)) + .put(BYTES, Value.of(new byte[] {1, 2, 3})) .build(); private static final DoubleExemplarData DOUBLE_EXEMPLAR1 = @@ -374,7 +378,8 @@ void doubleGauge() { attributeEntry("colors", "red", "blue"), attributeEntry("conditions", false, true), attributeEntry("scores", 0L, 1L), - attributeEntry("coins", 0.01, 0.05, 0.1)) + attributeEntry("coins", 0.01, 0.05, 0.1), + attributeEntry("bytes", Value.of(new byte[] {1, 2, 3}))) .hasFilteredAttributesSatisfying( equalTo(BEAR, "mya"), equalTo(WARM, true), @@ -393,7 +398,11 @@ void doubleGauge() { val -> val.containsExactly(false, true)), satisfies(SCORES, val -> val.containsExactly(0L, 1L)), satisfies( - COINS, val -> val.containsExactly(0.01, 0.05, 0.1))) + COINS, val -> val.containsExactly(0.01, 0.05, 0.1)), + satisfies( + BYTES, + val -> + val.isEqualTo(Value.of(new byte[] {1, 2, 3})))) // Demonstrates common usage of many exact matches and one // needing a loose one. .hasFilteredAttributesSatisfying( @@ -408,6 +417,7 @@ void doubleGauge() { equalTo(CONDITIONS, Arrays.asList(false, true)), equalTo(SCORES, Arrays.asList(0L, 1L)), equalTo(COINS, Arrays.asList(0.01, 0.05, 0.1)), + equalTo(BYTES, Value.of(new byte[] {1, 2, 3})), satisfies( LENGTH, val -> val.isCloseTo(1, offset(0.3))))), point -> @@ -426,7 +436,8 @@ void doubleGauge() { attributeEntry("colors", "red", "blue"), attributeEntry("conditions", false, true), attributeEntry("scores", 0L, 1L), - attributeEntry("coins", 0.01, 0.05, 0.1)) + attributeEntry("coins", 0.01, 0.05, 0.1), + attributeEntry("bytes", Value.of(new byte[] {1, 2, 3}))) .hasAttributesSatisfying( equalTo(BEAR, "mya"), equalTo(WARM, true), @@ -435,7 +446,8 @@ void doubleGauge() { equalTo(COLORS, Arrays.asList("red", "blue")), equalTo(CONDITIONS, Arrays.asList(false, true)), equalTo(SCORES, Arrays.asList(0L, 1L)), - equalTo(COINS, Arrays.asList(0.01, 0.05, 0.1))) + equalTo(COINS, Arrays.asList(0.01, 0.05, 0.1)), + equalTo(BYTES, Value.of(new byte[] {1, 2, 3}))) .hasAttributesSatisfying( satisfies(BEAR, val -> val.startsWith("mya")), satisfies(WARM, val -> val.isTrue()), @@ -444,7 +456,9 @@ void doubleGauge() { satisfies(COLORS, val -> val.containsExactly("red", "blue")), satisfies(CONDITIONS, val -> val.containsExactly(false, true)), satisfies(SCORES, val -> val.containsExactly(0L, 1L)), - satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1))) + satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1)), + satisfies( + BYTES, val -> val.isEqualTo(Value.of(new byte[] {1, 2, 3})))) // Demonstrates common usage of many exact matches and one needing a // loose one. .hasAttributesSatisfying( @@ -455,11 +469,12 @@ void doubleGauge() { equalTo(CONDITIONS, Arrays.asList(false, true)), equalTo(SCORES, Arrays.asList(0L, 1L)), equalTo(COINS, Arrays.asList(0.01, 0.05, 0.1)), + equalTo(BYTES, Value.of(new byte[] {1, 2, 3})), satisfies(LENGTH, val -> val.isCloseTo(1, offset(0.3)))) .hasAttributesSatisfying( attributes -> assertThat(attributes) - .hasSize(8) + .hasSize(9) .containsEntry(stringKey("bear"), "mya") .containsEntry("warm", true) .containsEntry("temperature", 30L) @@ -467,6 +482,7 @@ void doubleGauge() { .containsEntry("conditions", false, true) .containsEntry("scores", 0L, 1L) .containsEntry("coins", 0.01, 0.05, 0.1) + .containsEntry("bytes", Value.of(new byte[] {1, 2, 3})) .containsEntry("length", 1.2)))); } @@ -710,7 +726,12 @@ void doubleGaugeFailure() { SCORES, val -> val.containsExactly(0L, 1L)), satisfies( COINS, - val -> val.containsExactly(0.01, 0.05, 0.1))), + val -> val.containsExactly(0.01, 0.05, 0.1)), + satisfies( + BYTES, + val -> + val.isEqualTo( + Value.of(new byte[] {1, 2, 3})))), exemplar -> {}), point -> {}))) .isInstanceOf(AssertionError.class); @@ -741,7 +762,12 @@ void doubleGaugeFailure() { SCORES, val -> val.containsExactly(0L, 1L)), satisfies( COINS, - val -> val.containsExactly(0.01, 0.05, 0.1))), + val -> val.containsExactly(0.01, 0.05, 0.1)), + satisfies( + BYTES, + val -> + val.isEqualTo( + Value.of(new byte[] {1, 2, 3})))), exemplar -> {}), point -> {}))) .isInstanceOf(AssertionError.class); @@ -785,7 +811,11 @@ void doubleGaugeFailure() { CONDITIONS, val -> val.containsExactly(false, true)), satisfies(SCORES, val -> val.containsExactly(0L, 1L)), satisfies( - COINS, val -> val.containsExactly(0.01, 0.05, 0.1)))))) + COINS, val -> val.containsExactly(0.01, 0.05, 0.1)), + satisfies( + BYTES, + val -> + val.isEqualTo(Value.of(new byte[] {1, 2, 3}))))))) .isInstanceOf(AssertionError.class); assertThatThrownBy( () -> @@ -798,7 +828,7 @@ void doubleGaugeFailure() { point.hasAttributesSatisfying( attributes -> assertThat(attributes) - .hasSize(8) + .hasSize(9) .containsEntry( stringKey("bear"), "WRONG BEAR NAME") // Failed here @@ -808,6 +838,8 @@ void doubleGaugeFailure() { .containsEntry("conditions", false, true) .containsEntry("scores", 0L, 1L) .containsEntry("coins", 0.01, 0.05, 0.1) + .containsEntry( + "bytes", Value.of(new byte[] {1, 2, 3})) .containsEntry("length", 1.2))))) .isInstanceOf(AssertionError.class); } @@ -844,6 +876,7 @@ void longGauge() { equalTo(CONDITIONS, Arrays.asList(false, true)), equalTo(SCORES, Arrays.asList(0L, 1L)), equalTo(COINS, Arrays.asList(0.01, 0.05, 0.1)), + equalTo(BYTES, Value.of(new byte[] {1, 2, 3})), satisfies( LENGTH, val -> val.isCloseTo(1, offset(0.3))))), point -> point.hasValue(1))); diff --git a/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/TraceAssertionsTest.java b/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/TraceAssertionsTest.java index feb5eb9462a..4ea7975ad87 100644 --- a/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/TraceAssertionsTest.java +++ b/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/TraceAssertionsTest.java @@ -6,6 +6,7 @@ package io.opentelemetry.sdk.testing.assertj; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.common.AttributeKey.valueKey; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; @@ -16,6 +17,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.StatusCode; @@ -28,6 +30,7 @@ import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; +import java.nio.ByteBuffer; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; @@ -59,6 +62,7 @@ class TraceAssertionsTest { AttributeKey.booleanArrayKey("conditions"); private static final AttributeKey> SCORES = AttributeKey.longArrayKey("scores"); private static final AttributeKey> COINS = AttributeKey.doubleArrayKey("coins"); + private static final AttributeKey> BYTES = valueKey("bytes"); private static final AttributeKey UNSET = stringKey("unset"); private static final Attributes ATTRIBUTES = @@ -71,6 +75,7 @@ class TraceAssertionsTest { .put(CONDITIONS, Arrays.asList(false, true)) .put(SCORES, Arrays.asList(0L, 1L)) .put(COINS, Arrays.asList(0.01, 0.05, 0.1)) + .put(BYTES, Value.of(new byte[] {1, 2, 3})) .build(); private static final List EVENTS = Arrays.asList( @@ -189,7 +194,8 @@ void passing() { attributeEntry("colors", "red", "blue"), attributeEntry("conditions", false, true), attributeEntry("scores", 0L, 1L), - attributeEntry("coins", 0.01, 0.05, 0.1)) + attributeEntry("coins", 0.01, 0.05, 0.1), + attributeEntry("bytes", Value.of(new byte[] {1, 2, 3}))) .hasAttributesSatisfying( equalTo(BEAR, "mya"), equalTo(WARM, true), equalTo(TEMPERATURE, 30)) .hasAttributesSatisfyingExactly( @@ -200,7 +206,8 @@ void passing() { equalTo(COLORS, Arrays.asList("red", "blue")), equalTo(CONDITIONS, Arrays.asList(false, true)), equalTo(SCORES, Arrays.asList(0L, 1L)), - equalTo(COINS, Arrays.asList(0.01, 0.05, 0.1))) + equalTo(COINS, Arrays.asList(0.01, 0.05, 0.1)), + equalTo(BYTES, Value.of(new byte[] {1, 2, 3}))) .hasAttributesSatisfyingExactly( satisfies(BEAR, val -> val.startsWith("mya")), satisfies(WARM, val -> val.isTrue()), @@ -209,7 +216,8 @@ void passing() { satisfies(COLORS, val -> val.containsExactly("red", "blue")), satisfies(CONDITIONS, val -> val.containsExactly(false, true)), satisfies(SCORES, val -> val.containsExactly(0L, 1L)), - satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1))) + satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1)), + satisfies(BYTES, val -> val.isEqualTo(Value.of(new byte[] {1, 2, 3})))) // Demonstrates common usage of many exact matches and one needing a loose one. .hasAttributesSatisfyingExactly( equalTo(BEAR, "mya"), @@ -219,11 +227,12 @@ void passing() { equalTo(CONDITIONS, Arrays.asList(false, true)), equalTo(SCORES, Arrays.asList(0L, 1L)), equalTo(COINS, Arrays.asList(0.01, 0.05, 0.1)), + equalTo(BYTES, Value.of(new byte[] {1, 2, 3})), satisfies(LENGTH, val -> val.isCloseTo(1, offset(0.3)))) .hasAttributesSatisfying( attributes -> assertThat(attributes) - .hasSize(8) + .hasSize(9) .containsEntry(stringKey("bear"), "mya") .hasEntrySatisfying(stringKey("bear"), value -> assertThat(value).hasSize(3)) .containsEntry("bear", "mya") @@ -240,6 +249,7 @@ void passing() { .containsEntryWithLongValuesOf("scores", Arrays.asList(0L, 1L)) .containsEntry("coins", 0.01, 0.05, 0.1) .containsEntryWithDoubleValuesOf("coins", Arrays.asList(0.01, 0.05, 0.1)) + .containsEntry("bytes", Value.of(new byte[] {1, 2, 3})) .containsKey(stringKey("bear")) .containsKey("bear") .doesNotContainKey(stringKey("cat")) @@ -252,7 +262,8 @@ void passing() { attributeEntry("colors", "red", "blue"), attributeEntry("conditions", false, true), attributeEntry("scores", 0L, 1L), - attributeEntry("coins", 0.01, 0.05, 0.1))) + attributeEntry("coins", 0.01, 0.05, 0.1), + attributeEntry("bytes", Value.of(new byte[] {1, 2, 3})))) .hasEvents(EVENTS) .hasEvents(EVENTS.toArray(new EventData[0])) .hasEventsSatisfying( @@ -419,7 +430,8 @@ void failure() { satisfies(COLORS, val -> val.containsExactly("red", "blue")), satisfies(CONDITIONS, val -> val.containsExactly(false, true)), satisfies(SCORES, val -> val.containsExactly(0L, 1L)), - satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1)))) + satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1)), + satisfies(BYTES, val -> val.isEqualTo(Value.of(new byte[] {1, 2, 3}))))) .isInstanceOf(AssertionError.class); assertThatThrownBy( () -> @@ -432,7 +444,8 @@ void failure() { satisfies(COLORS, val -> val.containsExactly("red", "blue")), satisfies(CONDITIONS, val -> val.containsExactly(false, true)), satisfies(SCORES, val -> val.containsExactly(0L, 1L)), - satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1)))) + satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1)), + satisfies(BYTES, val -> val.isEqualTo(Value.of(new byte[] {1, 2, 3}))))) .isInstanceOf(AssertionError.class); assertThatThrownBy( () -> @@ -447,7 +460,8 @@ void failure() { satisfies(COLORS, val -> val.containsExactly("red", "blue")), satisfies(CONDITIONS, val -> val.containsExactly(false, true)), satisfies(SCORES, val -> val.containsExactly(0L, 1L)), - satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1)))) + satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1)), + satisfies(BYTES, val -> val.isEqualTo(Value.of(new byte[] {1, 2, 3}))))) .isInstanceOf(AssertionError.class); assertThatThrownBy( () -> @@ -632,7 +646,11 @@ void optionalAttributes() { satisfies(COLORS, val -> val.containsExactly("red", "blue")), satisfies(CONDITIONS, val -> val.containsExactly(false, true)), satisfies(SCORES, val -> val.containsExactly(0L, 1L)), - satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1))); + satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1)), + satisfies( + BYTES, + val -> + val.extracting(v -> ((ByteBuffer) v.getValue()).get(0)).isEqualTo((byte) 1))); assertThatThrownBy( () -> @@ -651,7 +669,12 @@ void optionalAttributes() { satisfies(COLORS, val -> val.containsExactly("red", "blue")), satisfies(CONDITIONS, val -> val.containsExactly(false, true)), satisfies(SCORES, val -> val.containsExactly(0L, 1L)), - satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1)))) + satisfies(COINS, val -> val.containsExactly(0.01, 0.05, 0.1)), + satisfies( + BYTES, + val -> + val.extracting(v -> ((ByteBuffer) v.getValue()).get(0)) + .isEqualTo((byte) 1)))) .isInstanceOf(AssertionError.class); } diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java index 9c9ecdc7e9c..64b9a9483ea 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java @@ -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.emptyList; import static java.util.stream.Collectors.joining; import static org.assertj.core.api.Assertions.assertThat; @@ -20,6 +21,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.SpanContext; @@ -367,8 +369,10 @@ void setAttribute_nullAttributeValue() { spanBuilder.setAttribute(booleanArrayKey("boolArrayAttribute"), Arrays.asList(true, null)); spanBuilder.setAttribute(longArrayKey("longArrayAttribute"), Arrays.asList(12345L, null)); spanBuilder.setAttribute(doubleArrayKey("doubleArrayAttribute"), Arrays.asList(1.2345, null)); + spanBuilder.setAttribute(valueKey("emptyValue"), Value.empty()); + spanBuilder.setAttribute(valueKey("nullValue"), null); SdkSpan span = (SdkSpan) spanBuilder.startSpan(); - assertThat(span.toSpanData().getAttributes().size()).isEqualTo(9); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(10); } @Test @@ -383,8 +387,9 @@ void setAttribute_nullAttributeValue_afterEnd() { spanBuilder.setAttribute(booleanArrayKey("boolArrayAttribute"), Arrays.asList(true, null)); spanBuilder.setAttribute(longArrayKey("longArrayAttribute"), Arrays.asList(12345L, null)); spanBuilder.setAttribute(doubleArrayKey("doubleArrayAttribute"), Arrays.asList(1.2345, null)); + spanBuilder.setAttribute(valueKey("emptyValue"), Value.empty()); SdkSpan span = (SdkSpan) spanBuilder.startSpan(); - assertThat(span.toSpanData().getAttributes().size()).isEqualTo(9); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(10); span.end(); span.setAttribute("emptyString", null); span.setAttribute(stringKey("emptyStringAttributeValue"), null); @@ -395,7 +400,8 @@ void setAttribute_nullAttributeValue_afterEnd() { span.setAttribute(booleanArrayKey("boolArrayAttribute"), null); span.setAttribute(longArrayKey("longArrayAttribute"), null); span.setAttribute(doubleArrayKey("doubleArrayAttribute"), null); - assertThat(span.toSpanData().getAttributes().size()).isEqualTo(9); + span.setAttribute(valueKey("emptyValue"), null); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(10); } @Test diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java index 262bd10a63c..bf5628e962c 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.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 io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static java.util.Collections.singletonList; @@ -29,6 +30,7 @@ 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.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanId; @@ -553,16 +555,18 @@ void setAttribute() { span.setAttribute(longArrayKey("NullArrayLongKey"), null); span.setAttribute(doubleArrayKey("NullArrayDoubleKey"), null); span.setAttribute(booleanArrayKey("NullArrayBooleanKey"), null); + span.setAttribute(valueKey("NullValueKey"), null); // These should be maintained span.setAttribute(longArrayKey("ArrayWithNullLongKey"), singletonList(null)); span.setAttribute(stringArrayKey("ArrayWithNullStringKey"), singletonList(null)); span.setAttribute(doubleArrayKey("ArrayWithNullDoubleKey"), singletonList(null)); span.setAttribute(booleanArrayKey("ArrayWithNullBooleanKey"), singletonList(null)); + span.setAttribute(valueKey("ValueKey"), Value.of(new byte[] {0})); } finally { span.end(); } SpanData spanData = span.toSpanData(); - assertThat(spanData.getAttributes().size()).isEqualTo(16); + assertThat(spanData.getAttributes().size()).isEqualTo(17); assertThat(spanData.getAttributes().get(stringKey("StringKey"))).isNotNull(); assertThat(spanData.getAttributes().get(stringKey("EmptyStringKey"))).isNotNull(); assertThat(spanData.getAttributes().get(stringKey("EmptyStringAttributeValue"))).isNotNull(); @@ -580,6 +584,7 @@ void setAttribute() { assertThat(spanData.getAttributes().get(doubleArrayKey("ArrayWithNullDoubleKey"))).isNotNull(); assertThat(spanData.getAttributes().get(booleanArrayKey("ArrayWithNullBooleanKey"))) .isNotNull(); + assertThat(spanData.getAttributes().get(valueKey("ValueKey"))).isNotNull(); assertThat(spanData.getAttributes().get(stringArrayKey("ArrayStringKey")).size()).isEqualTo(4); assertThat(spanData.getAttributes().get(longArrayKey("ArrayLongKey")).size()).isEqualTo(5); assertThat(spanData.getAttributes().get(doubleArrayKey("ArrayDoubleKey")).size()).isEqualTo(5); @@ -612,6 +617,7 @@ void setAttribute_nullKeys() { span.setAttribute(null, Collections.emptyList()); span.setAttribute(null, Collections.emptyList()); span.setAttribute(null, Collections.emptyList()); + span.setAttribute(null, Value.empty()); assertThat(span.toSpanData().getAttributes().size()).isZero(); } @@ -660,7 +666,9 @@ void setAttribute_nullAttributeValue() { span.setAttribute(booleanArrayKey("boolArrayAttribute"), Arrays.asList(true, null)); span.setAttribute(longArrayKey("longArrayAttribute"), Arrays.asList(12345L, null)); span.setAttribute(doubleArrayKey("doubleArrayAttribute"), Arrays.asList(1.2345, null)); - assertThat(span.toSpanData().getAttributes().size()).isEqualTo(9); + span.setAttribute(valueKey("emptyValue"), Value.empty()); + span.setAttribute(valueKey("nullValue"), null); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(10); } @Test