From eca5ee42b8095e84ef29f18a3754e3fd46286385 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 9 Jan 2026 12:44:26 -0800 Subject: [PATCH 01/22] Stabilize complex attributes --- .../api/common/ArrayBackedAttributes.java | 110 ++++++++++++++++ .../common/ArrayBackedAttributesBuilder.java | 112 ++++++++++++++++ .../api/common/AttributeKey.java | 15 +++ .../api/common/AttributeType.java | 3 +- .../opentelemetry/api/common/Attributes.java | 58 +++++++- .../api/common/AttributesBuilder.java | 47 +++++++ .../io/opentelemetry/api/common/Value.java | 7 + .../opentelemetry/api/common/ValueEmpty.java | 47 +++++++ .../opentelemetry/api/common/ValueType.java | 3 +- .../api/common/AttributesTest.java | 124 +++++++++++++++--- .../ArrayBackedExtendedAttributesBuilder.java | 2 + .../InternalExtendedAttributeKeyImpl.java | 6 +- .../common/ExtendedAttributeKeyTest.java | 5 +- .../logs/ExtendedLogsBridgeApiUsageTest.java | 5 + .../logging/otlp/TestDataExporter.java | 38 +++++- .../test/resources/expected-logs-wrapper.json | 41 ++++++ .../src/test/resources/expected-logs.json | 41 ++++++ .../resources/expected-metrics-wrapper.json | 40 ++++++ .../src/test/resources/expected-metrics.json | 40 ++++++ .../resources/expected-spans-wrapper.json | 41 ++++++ .../src/test/resources/expected-spans.json | 41 ++++++ .../logging/LoggingSpanExporterTest.java | 17 ++- .../SystemOutLogRecordExporterTest.java | 17 ++- .../internal/otlp/AnyValueMarshaler.java | 2 + .../otlp/AnyValueStatelessMarshaler.java | 5 + ...ributeArrayAnyValueStatelessMarshaler.java | 2 + .../AttributeKeyValueStatelessMarshaler.java | 7 + .../internal/otlp/EmptyAnyValueMarshaler.java | 30 +++++ .../internal/otlp/KeyValueMarshaler.java | 3 + .../LowAllocationLogRequestMarshalerTest.java | 11 ++ ...AllocationMetricsRequestMarshalerTest.java | 10 ++ ...owAllocationTraceRequestMarshalerTest.java | 11 ++ .../traces/TraceRequestMarshalerTest.java | 46 ++++++- .../prometheus/Otel2PrometheusConverter.java | 4 + .../Otel2PrometheusConverterTest.java | 22 +++- .../zipkin/OtelToZipkinSpanTransformer.java | 4 + .../zipkin/EventDataToAnnotationTest.java | 25 ++-- .../OtelToZipkinSpanTransformerTest.java | 13 +- .../sdk/resources/ResourceTest.java | 20 +++ .../opentelemetry/sdk/logs/SdkLoggerTest.java | 13 +- .../testing/assertj/AttributeAssertion.java | 3 + .../sdk/testing/assertj/AttributesAssert.java | 6 + .../testing/assertj/LogRecordDataAssert.java | 2 + .../assertj/OpenTelemetryAssertions.java | 22 ++++ .../sdk/testing/assertj/AssertUtilTest.java | 9 +- .../testing/assertj/LogAssertionsTest.java | 13 +- .../testing/assertj/MetricAssertionsTest.java | 53 ++++++-- .../testing/assertj/TraceAssertionsTest.java | 43 ++++-- .../sdk/trace/SdkSpanBuilderTest.java | 12 +- .../opentelemetry/sdk/trace/SdkSpanTest.java | 12 +- 50 files changed, 1185 insertions(+), 78 deletions(-) create mode 100644 api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java create mode 100644 exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/EmptyAnyValueMarshaler.java 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/Value.java b/api/all/src/main/java/io/opentelemetry/api/common/Value.java index a29be801e27..2dbffe47c09 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(); 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..742fdc0741f --- /dev/null +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java @@ -0,0 +1,47 @@ +/* + * 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 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/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..5b0963d940e 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,93 @@ void builder_arrayTypes() { booleanArrayKey("boolean"), Arrays.asList(false, true))); } + @Test + void builder_valueTypes() { + // Test Value type attributes with various Value kinds + // Note: simple Value types (string, long, double, boolean) are coerced to their + // corresponding primitive AttributeTypes for consistent storage + + // These Value types should be coerced to primitive types + AttributeKey> stringValueKey = valueKey("stringValue"); + AttributeKey> longValueKey = valueKey("longValue"); + AttributeKey> doubleValueKey = valueKey("doubleValue"); + AttributeKey> booleanValueKey = valueKey("booleanValue"); + + // These Value types cannot be coerced and remain as VALUE type + AttributeKey> bytesValueKey = valueKey("bytesValue"); + AttributeKey> kvListValueKey = valueKey("kvListValue"); + AttributeKey> heterogeneousArrayKey = valueKey("heterogeneousArray"); + AttributeKey> emptyValueKey = valueKey("emptyValue"); + + Attributes attributes = + Attributes.builder() + .put(stringValueKey, Value.of("stringVal")) + .put(longValueKey, Value.of(100L)) + .put(doubleValueKey, Value.of(3.14)) + .put(booleanValueKey, Value.of(true)) + .put(bytesValueKey, Value.of(new byte[] {1, 2, 3})) + .put(kvListValueKey, Value.of(KeyValue.of("nested", Value.of("value")))) + .put(heterogeneousArrayKey, Value.of(Value.of("elem1"), Value.of(42L))) + .put(emptyValueKey, Value.empty()) + .build(); + + // Verify coerced values can be retrieved with primitive keys + assertThat(attributes.get(stringKey("stringValue"))).isEqualTo("stringVal"); + assertThat(attributes.get(longKey("longValue"))).isEqualTo(100L); + assertThat(attributes.get(doubleKey("doubleValue"))).isEqualTo(3.14); + assertThat(attributes.get(booleanKey("booleanValue"))).isEqualTo(true); + + // Verify complex Value types that remain as VALUE type + assertThat(attributes.get(bytesValueKey)).isNotNull(); + assertThat(attributes.get(bytesValueKey).getType()).isEqualTo(ValueType.BYTES); + + assertThat(attributes.get(kvListValueKey)).isNotNull(); + assertThat(attributes.get(kvListValueKey).getType()).isEqualTo(ValueType.KEY_VALUE_LIST); + + assertThat(attributes.get(heterogeneousArrayKey)).isNotNull(); + assertThat(attributes.get(heterogeneousArrayKey).getType()).isEqualTo(ValueType.ARRAY); + + assertThat(attributes.get(emptyValueKey)).isNotNull(); + assertThat(attributes.get(emptyValueKey).getType()).isEqualTo(ValueType.EMPTY); + assertThat(attributes.get(emptyValueKey).getValue()).isNull(); + + // Verify the total size + assertThat(attributes.size()).isEqualTo(8); + + // Verify forEach sees the correct types + Map entriesSeen = new LinkedHashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).hasSize(8); + + // Verify coerced keys have primitive types + assertThat(entriesSeen.keySet()) + .filteredOn(key -> key.getKey().equals("stringValue")) + .allMatch(key -> key.getType() == AttributeType.STRING); + assertThat(entriesSeen.keySet()) + .filteredOn(key -> key.getKey().equals("longValue")) + .allMatch(key -> key.getType() == AttributeType.LONG); + assertThat(entriesSeen.keySet()) + .filteredOn(key -> key.getKey().equals("doubleValue")) + .allMatch(key -> key.getType() == AttributeType.DOUBLE); + assertThat(entriesSeen.keySet()) + .filteredOn(key -> key.getKey().equals("booleanValue")) + .allMatch(key -> key.getType() == AttributeType.BOOLEAN); + + // Verify complex types remain as VALUE type + assertThat(entriesSeen.keySet()) + .filteredOn(key -> key.getKey().equals("bytesValue")) + .allMatch(key -> key.getType() == AttributeType.VALUE); + assertThat(entriesSeen.keySet()) + .filteredOn(key -> key.getKey().equals("kvListValue")) + .allMatch(key -> key.getType() == AttributeType.VALUE); + assertThat(entriesSeen.keySet()) + .filteredOn(key -> key.getKey().equals("heterogeneousArray")) + .allMatch(key -> key.getType() == AttributeType.VALUE); + assertThat(entriesSeen.keySet()) + .filteredOn(key -> key.getKey().equals("emptyValue")) + .allMatch(key -> key.getType() == AttributeType.VALUE); + } + @Test @SuppressWarnings("unchecked") void get_Null() { @@ -379,7 +467,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 +476,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 +521,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 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/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/AttributeArrayAnyValueStatelessMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeArrayAnyValueStatelessMarshaler.java index db92ca1e7dc..7837b3e37a5 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeArrayAnyValueStatelessMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeArrayAnyValueStatelessMarshaler.java @@ -52,6 +52,7 @@ public void writeTo(Serializer output, AttributeType type, List list, Marshal DoubleAnyValueStatelessMarshaler.INSTANCE, context); return; + // TODO this class is named *ArrayAnyValue*, does that mean it covers List> as well? default: throw new IllegalArgumentException("Unsupported attribute type."); } @@ -82,6 +83,7 @@ public int getBinarySerializedSize(AttributeType type, List list, MarshalerCo (List) list, DoubleAnyValueStatelessMarshaler.INSTANCE, context); + // TODO this class is named *ArrayAnyValue*, does that mean it covers List> as well? default: throw new IllegalArgumentException("Unsupported attribute type."); } 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..257cab18db3 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,9 @@ private static String toLabelValue(AttributeType type, Object attributeValue) { "Unexpected label value of %s for %s", attributeValue.getClass().getName(), type.name())); } + case VALUE: + // TODO this should be json representation + return ((Value) attributeValue).asString(); } 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..70588c2220a 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,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 org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -20,6 +21,8 @@ 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; @@ -334,10 +337,14 @@ void labelValueSerialization(Attributes attributes) { (key, value) -> { String labelValue = labels.get(key.getKey()); try { - String expectedValue = - key.getType() == AttributeType.STRING - ? (String) value - : OBJECT_MAPPER.writeValueAsString(value); + String expectedValue; + if (key.getType() == AttributeType.STRING) { + expectedValue = (String) value; + } else if (key.getType() == AttributeType.VALUE) { + expectedValue = ((Value) value).asString(); + } else { + expectedValue = OBJECT_MAPPER.writeValueAsString(value); + } assertThat(labelValue).isEqualTo(expectedValue); } catch (JsonProcessingException e) { throw new RuntimeException(e); @@ -360,7 +367,12 @@ private static Stream labelValueSerializationArgs() { Attributes.of(longArrayKey("key"), Arrays.asList(Long.MIN_VALUE, Long.MAX_VALUE))), Arguments.of( Attributes.of( - doubleArrayKey("key"), Arrays.asList(Double.MIN_VALUE, Double.MAX_VALUE)))); + doubleArrayKey("key"), Arrays.asList(Double.MIN_VALUE, Double.MAX_VALUE))), + Arguments.of(Attributes.of(valueKey("key"), Value.of(new byte[] {1, 2, 3}))), + Arguments.of( + Attributes.of(valueKey("key"), Value.of(KeyValue.of("nested", Value.of("value"))))), + Arguments.of(Attributes.of(valueKey("key"), Value.of(Value.of("string"), Value.of(123L)))), + Arguments.of(Attributes.of(valueKey("key"), Value.empty()))); } static MetricData createSampleMetricData( 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..c00749f8d6d 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,9 @@ private static String valueToString(AttributeKey key, Object attributeValue) case LONG_ARRAY: case DOUBLE_ARRAY: return commaSeparated((List) attributeValue); + case VALUE: + // TODO this should be json representation + return ((Value) attributeValue).asString(); } 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..7323c1b1467 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\":ValueBytes{AQID},\"v10\":KeyValueList{[nested=value]},\"v11\":ValueArray{[string, 123]},\"v12\":ValueEmpty{}}"; 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..e01d97a178f 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", "") .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..f4ae727004a 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: + // TODO? } 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..29427308082 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); } 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 From 8c951bd4b8d0dbd3b83acd28601e58ac517c2526 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 9 Jan 2026 14:32:10 -0800 Subject: [PATCH 02/22] apidiffs --- .../current_vs_latest/opentelemetry-api.txt | 18 +++++++++++++++++- .../opentelemetry-sdk-testing.txt | 12 +++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index 9ca1c6c2e64..70a661af20f 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -1,2 +1,18 @@ 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() +*** 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 From 0e2af077837565e21978eff0ad63fbe2a352ff10 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 9 Jan 2026 15:20:44 -0800 Subject: [PATCH 03/22] updates --- .../api/common/AttributesTest.java | 327 ++++++++++++++---- ...ributeArrayAnyValueStatelessMarshaler.java | 2 - .../Otel2PrometheusConverterTest.java | 61 ++-- .../zipkin/OtelToZipkinSpanTransformer.java | 10 +- .../OtelToZipkinSpanTransformerTest.java | 10 +- .../testing/assertj/LogRecordDataAssert.java | 2 +- .../testing/assertj/LogAssertionsTest.java | 6 +- 7 files changed, 294 insertions(+), 124 deletions(-) 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 5b0963d940e..e8dae3d0c3a 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 @@ -359,90 +359,273 @@ void builder_arrayTypes() { } @Test - void builder_valueTypes() { - // Test Value type attributes with various Value kinds - // Note: simple Value types (string, long, double, boolean) are coerced to their - // corresponding primitive AttributeTypes for consistent storage - - // These Value types should be coerced to primitive types - AttributeKey> stringValueKey = valueKey("stringValue"); - AttributeKey> longValueKey = valueKey("longValue"); - AttributeKey> doubleValueKey = valueKey("doubleValue"); - AttributeKey> booleanValueKey = valueKey("booleanValue"); - - // These Value types cannot be coerced and remain as VALUE type - AttributeKey> bytesValueKey = valueKey("bytesValue"); - AttributeKey> kvListValueKey = valueKey("kvListValue"); - AttributeKey> heterogeneousArrayKey = valueKey("heterogeneousArray"); - AttributeKey> emptyValueKey = valueKey("emptyValue"); + 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 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 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 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 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(stringValueKey, Value.of("stringVal")) - .put(longValueKey, Value.of(100L)) - .put(doubleValueKey, Value.of(3.14)) - .put(booleanValueKey, Value.of(true)) - .put(bytesValueKey, Value.of(new byte[] {1, 2, 3})) - .put(kvListValueKey, Value.of(KeyValue.of("nested", Value.of("value")))) - .put(heterogeneousArrayKey, Value.of(Value.of("elem1"), Value.of(42L))) - .put(emptyValueKey, Value.empty()) + .put(valueKey("key"), Value.of(Arrays.asList(Value.of("a"), Value.of("b")))) .build(); - // Verify coerced values can be retrieved with primitive keys - assertThat(attributes.get(stringKey("stringValue"))).isEqualTo("stringVal"); - assertThat(attributes.get(longKey("longValue"))).isEqualTo(100L); - assertThat(attributes.get(doubleKey("doubleValue"))).isEqualTo(3.14); - assertThat(attributes.get(booleanKey("booleanValue"))).isEqualTo(true); + // 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")))); - // Verify complex Value types that remain as VALUE type - assertThat(attributes.get(bytesValueKey)).isNotNull(); - assertThat(attributes.get(bytesValueKey).getType()).isEqualTo(ValueType.BYTES); + // forEach should show STRING_ARRAY type + Map entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(stringArrayKey("key"), Arrays.asList("a", "b"))); - assertThat(attributes.get(kvListValueKey)).isNotNull(); - assertThat(attributes.get(kvListValueKey).getType()).isEqualTo(ValueType.KEY_VALUE_LIST); + // asMap should show STRING_ARRAY type + assertThat(attributes.asMap()) + .containsExactly(entry(stringArrayKey("key"), Arrays.asList("a", "b"))); + } - assertThat(attributes.get(heterogeneousArrayKey)).isNotNull(); - assertThat(attributes.get(heterogeneousArrayKey).getType()).isEqualTo(ValueType.ARRAY); + @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(); - assertThat(attributes.get(emptyValueKey)).isNotNull(); - assertThat(attributes.get(emptyValueKey).getType()).isEqualTo(ValueType.EMPTY); - assertThat(attributes.get(emptyValueKey).getValue()).isNull(); + // 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)))); - // Verify the total size - assertThat(attributes.size()).isEqualTo(8); + // forEach should show LONG_ARRAY type + Map entriesSeen = new HashMap<>(); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(longArrayKey("key"), Arrays.asList(1L, 2L))); - // Verify forEach sees the correct types - Map entriesSeen = new LinkedHashMap<>(); + // 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 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 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 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).hasSize(8); - - // Verify coerced keys have primitive types - assertThat(entriesSeen.keySet()) - .filteredOn(key -> key.getKey().equals("stringValue")) - .allMatch(key -> key.getType() == AttributeType.STRING); - assertThat(entriesSeen.keySet()) - .filteredOn(key -> key.getKey().equals("longValue")) - .allMatch(key -> key.getType() == AttributeType.LONG); - assertThat(entriesSeen.keySet()) - .filteredOn(key -> key.getKey().equals("doubleValue")) - .allMatch(key -> key.getType() == AttributeType.DOUBLE); - assertThat(entriesSeen.keySet()) - .filteredOn(key -> key.getKey().equals("booleanValue")) - .allMatch(key -> key.getType() == AttributeType.BOOLEAN); - - // Verify complex types remain as VALUE type - assertThat(entriesSeen.keySet()) - .filteredOn(key -> key.getKey().equals("bytesValue")) - .allMatch(key -> key.getType() == AttributeType.VALUE); - assertThat(entriesSeen.keySet()) - .filteredOn(key -> key.getKey().equals("kvListValue")) - .allMatch(key -> key.getType() == AttributeType.VALUE); - assertThat(entriesSeen.keySet()) - .filteredOn(key -> key.getKey().equals("heterogeneousArray")) - .allMatch(key -> key.getType() == AttributeType.VALUE); - assertThat(entriesSeen.keySet()) - .filteredOn(key -> key.getKey().equals("emptyValue")) - .allMatch(key -> key.getType() == AttributeType.VALUE); + assertThat(entriesSeen).containsExactly(entry(valueKey("key"), nestedArray)); } @Test diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeArrayAnyValueStatelessMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeArrayAnyValueStatelessMarshaler.java index 7837b3e37a5..db92ca1e7dc 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeArrayAnyValueStatelessMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/AttributeArrayAnyValueStatelessMarshaler.java @@ -52,7 +52,6 @@ public void writeTo(Serializer output, AttributeType type, List list, Marshal DoubleAnyValueStatelessMarshaler.INSTANCE, context); return; - // TODO this class is named *ArrayAnyValue*, does that mean it covers List> as well? default: throw new IllegalArgumentException("Unsupported attribute type."); } @@ -83,7 +82,6 @@ public int getBinarySerializedSize(AttributeType type, List list, MarshalerCo (List) list, DoubleAnyValueStatelessMarshaler.INSTANCE, context); - // TODO this class is named *ArrayAnyValue*, does that mean it covers List> as well? default: throw new IllegalArgumentException("Unsupported attribute 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 70588c2220a..2a500f6bd17 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 @@ -17,9 +17,6 @@ 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; @@ -71,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( @@ -322,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); @@ -333,46 +329,43 @@ 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; - if (key.getType() == AttributeType.STRING) { - expectedValue = (String) value; - } else if (key.getType() == AttributeType.VALUE) { - expectedValue = ((Value) value).asString(); - } else { - expectedValue = 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(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))), - Arguments.of(Attributes.of(valueKey("key"), Value.of(new byte[] {1, 2, 3}))), + 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.of(KeyValue.of("nested", Value.of("value"))))), - Arguments.of(Attributes.of(valueKey("key"), Value.of(Value.of("string"), Value.of(123L)))), - Arguments.of(Attributes.of(valueKey("key"), Value.empty()))); + Attributes.of(valueKey("key"), Value.empty()), + "")); } static MetricData createSampleMetricData( 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 c00749f8d6d..d5578675f73 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,7 +12,6 @@ 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; @@ -100,7 +99,11 @@ Span generateSpan(SpanData spanData) { Attributes spanAttributes = spanData.getAttributes(); spanAttributes.forEach( - (key, value) -> spanBuilder.putTag(key.getKey(), valueToString(key, value))); + (key, value) -> { + if (key.getType() != AttributeType.VALUE) { + spanBuilder.putTag(key.getKey(), valueToString(key, value)); + } + }); int droppedAttributes = spanData.getTotalAttributeCount() - spanAttributes.size(); if (droppedAttributes > 0) { spanBuilder.putTag(OTEL_DROPPED_ATTRIBUTES_COUNT, String.valueOf(droppedAttributes)); @@ -225,8 +228,7 @@ private static String valueToString(AttributeKey key, Object attributeValue) case DOUBLE_ARRAY: return commaSeparated((List) attributeValue); case VALUE: - // TODO this should be json representation - return ((Value) attributeValue).asString(); + throw new IllegalArgumentException("Unsupported attribute type: VALUE"); } throw new IllegalStateException("Unknown attribute type: " + type); } 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 e01d97a178f..a8025f6d9f3 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 @@ -21,7 +21,6 @@ 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; @@ -367,14 +366,11 @@ void generateSpan_WithAttributes() { .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(32) + .setTotalAttributeCount(29) .setTotalRecordedEvents(3) .setKind(SpanKind.CLIENT) .build(); @@ -390,10 +386,6 @@ 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", "") .putTag(OtelToZipkinSpanTransformer.OTEL_STATUS_CODE, "OK") .putTag(OtelToZipkinSpanTransformer.OTEL_DROPPED_ATTRIBUTES_COUNT, "20") .putTag(OtelToZipkinSpanTransformer.OTEL_DROPPED_EVENTS_COUNT, "1") 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 f4ae727004a..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 @@ -345,7 +345,7 @@ public LogRecordDataAssert hasBodyField(AttributeKey key, T value) { key.getKey(), Value.of(((List) value).stream().map(Value::of).collect(toList()))); case VALUE: - // TODO? + return hasBodyField(key.getKey(), (Value) value); } return this; } 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 29427308082..4168a128f79 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 @@ -318,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); @@ -331,6 +332,7 @@ 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})); } } From 103d13b79fde01ebd7977ecba20ac2e4b4bb132a Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 9 Jan 2026 16:08:39 -0800 Subject: [PATCH 04/22] json --- .../io/opentelemetry/api/common/JsonUtil.java | 136 ++++++++++++++++++ .../api/common/KeyValueList.java | 8 +- .../opentelemetry/api/common/ValueArray.java | 6 +- .../api/common/ValueBoolean.java | 4 +- .../opentelemetry/api/common/ValueBytes.java | 5 +- .../opentelemetry/api/common/ValueDouble.java | 4 +- .../opentelemetry/api/common/ValueEmpty.java | 2 +- .../opentelemetry/api/common/ValueLong.java | 4 +- .../opentelemetry/api/common/ValueString.java | 4 +- .../api/common/AttributesTest.java | 92 ++++++++++-- .../Otel2PrometheusConverterTest.java | 14 +- 11 files changed, 246 insertions(+), 33 deletions(-) create mode 100644 api/all/src/main/java/io/opentelemetry/api/common/JsonUtil.java diff --git a/api/all/src/main/java/io/opentelemetry/api/common/JsonUtil.java b/api/all/src/main/java/io/opentelemetry/api/common/JsonUtil.java new file mode 100644 index 00000000000..708eb4a0623 --- /dev/null +++ b/api/all/src/main/java/io/opentelemetry/api/common/JsonUtil.java @@ -0,0 +1,136 @@ +/* + * 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; + +/** Package-private utility for JSON encoding. */ +final class JsonUtil { + + 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 appendJsonValue(StringBuilder sb, Value value) { + switch (value.getType()) { + case STRING: + appendJsonString(sb, (String) value.getValue()); + break; + case LONG: + sb.append(value.getValue()); + break; + case DOUBLE: + appendJsonDouble(sb, (Double) value.getValue()); + break; + case BOOLEAN: + sb.append(value.getValue()); + break; + case ARRAY: + appendJsonArray(sb, (List>) value.getValue()); + break; + case KEY_VALUE_LIST: + appendJsonKeyValueList(sb, (List) value.getValue()); + break; + case BYTES: + appendJsonBytes(sb, (ByteBuffer) value.getValue()); + break; + case EMPTY: + sb.append("null"); + break; + } + } + + static void appendJsonString(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 appendJsonDouble(StringBuilder sb, double value) { + if (Double.isNaN(value)) { + // Encoding as string to match ProtoJSON: https://protobuf.dev/programming-guides/json/ + sb.append("\"NaN\""); + } else if (Double.isInfinite(value)) { + // Encoding as string to match ProtoJSON: https://protobuf.dev/programming-guides/json/ + sb.append(value > 0 ? "\"Infinity\"" : "\"-Infinity\""); + } else { + sb.append(value); + } + } + + private static void appendJsonBytes(StringBuilder sb, ByteBuffer value) { + // Encoding as base64 to match ProtoJSON: https://protobuf.dev/programming-guides/json/ + byte[] bytes = new byte[value.remaining()]; + value.duplicate().get(bytes); + sb.append('"').append(Base64.getEncoder().encodeToString(bytes)).append('"'); + } + + private static void appendJsonArray(StringBuilder sb, List> values) { + sb.append('['); + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + sb.append(','); + } + appendJsonValue(sb, values.get(i)); + } + sb.append(']'); + } + + private static void appendJsonKeyValueList(StringBuilder sb, List values) { + sb.append('{'); + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + sb.append(','); + } + KeyValue kv = values.get(i); + appendJsonString(sb, kv.getKey()); + sb.append(':'); + appendJsonValue(sb, kv.getValue()); + } + sb.append('}'); + } + + private JsonUtil() {} +} 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..ea77bb0789a 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 @@ -5,8 +5,6 @@ package io.opentelemetry.api.common; -import static java.util.stream.Collectors.joining; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -50,9 +48,9 @@ public List getValue() { @Override public String asString() { - return value.stream() - .map(item -> item.getKey() + "=" + item.getValue().asString()) - .collect(joining(", ", "[", "]")); + StringBuilder sb = new StringBuilder(); + JsonUtil.appendJsonValue(sb, this); + return sb.toString(); } @Override 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..0f9d67536c1 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 @@ -5,8 +5,6 @@ package io.opentelemetry.api.common; -import static java.util.stream.Collectors.joining; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -44,7 +42,9 @@ public List> getValue() { @Override public String asString() { - return value.stream().map(Value::asString).collect(joining(", ", "[", "]")); + StringBuilder sb = new StringBuilder(); + JsonUtil.appendJsonValue(sb, this); + return sb.toString(); } @Override 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..7080c6d03dd 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 @@ -31,7 +31,9 @@ public Boolean getValue() { @Override public String asString() { - return String.valueOf(value); + StringBuilder sb = new StringBuilder(); + JsonUtil.appendJsonValue(sb, this); + return sb.toString(); } @Override 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..4691a469bfa 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 @@ -7,7 +7,6 @@ import java.nio.ByteBuffer; import java.util.Arrays; -import java.util.Base64; import java.util.Objects; final class ValueBytes implements Value { @@ -35,7 +34,9 @@ public ByteBuffer getValue() { @Override public String asString() { - return Base64.getEncoder().encodeToString(raw); + StringBuilder sb = new StringBuilder(); + JsonUtil.appendJsonValue(sb, this); + return sb.toString(); } @Override 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..872ab66836e 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 @@ -31,7 +31,9 @@ public Double getValue() { @Override public String asString() { - return String.valueOf(value); + StringBuilder sb = new StringBuilder(); + JsonUtil.appendJsonValue(sb, this); + return sb.toString(); } @Override 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 index 742fdc0741f..17a3959719e 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java @@ -27,7 +27,7 @@ public Void getValue() { @Override public String asString() { - return ""; + return "null"; } @Override 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..94b235f0877 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 @@ -31,7 +31,9 @@ public Long getValue() { @Override public String asString() { - return String.valueOf(value); + StringBuilder sb = new StringBuilder(); + JsonUtil.appendJsonValue(sb, this); + return sb.toString(); } @Override 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..9d724d7559b 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 @@ -32,7 +32,9 @@ public String getValue() { @Override public String asString() { - return value; + StringBuilder sb = new StringBuilder(); + JsonUtil.appendJsonValue(sb, this); + return sb.toString(); } @Override 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 e8dae3d0c3a..7767fbfb80c 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 @@ -368,7 +368,7 @@ void valueStoredAsString() { assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of("test")); // forEach should show STRING type - Map entriesSeen = new HashMap<>(); + Map, Object> entriesSeen = new HashMap<>(); attributes.forEach(entriesSeen::put); assertThat(entriesSeen).containsExactly(entry(stringKey("key"), "test")); @@ -386,7 +386,7 @@ void valueStoredAsLong() { assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(123L)); // forEach should show LONG type - Map entriesSeen = new HashMap<>(); + Map, Object> entriesSeen = new HashMap<>(); attributes.forEach(entriesSeen::put); assertThat(entriesSeen).containsExactly(entry(longKey("key"), 123L)); @@ -404,7 +404,7 @@ void valueStoredAsDouble() { assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(1.23)); // forEach should show DOUBLE type - Map entriesSeen = new HashMap<>(); + Map, Object> entriesSeen = new HashMap<>(); attributes.forEach(entriesSeen::put); assertThat(entriesSeen).containsExactly(entry(doubleKey("key"), 1.23)); @@ -422,7 +422,7 @@ void valueStoredAsBoolean() { assertThat(attributes.get(valueKey("key"))).isEqualTo(Value.of(true)); // forEach should show BOOLEAN type - Map entriesSeen = new HashMap<>(); + Map, Object> entriesSeen = new HashMap<>(); attributes.forEach(entriesSeen::put); assertThat(entriesSeen).containsExactly(entry(booleanKey("key"), true)); @@ -445,7 +445,7 @@ void valueStoredAsStringArray() { .isEqualTo(Value.of(Arrays.asList(Value.of("a"), Value.of("b")))); // forEach should show STRING_ARRAY type - Map entriesSeen = new HashMap<>(); + Map, Object> entriesSeen = new HashMap<>(); attributes.forEach(entriesSeen::put); assertThat(entriesSeen).containsExactly(entry(stringArrayKey("key"), Arrays.asList("a", "b"))); @@ -469,7 +469,7 @@ void valueStoredAsLongArray() { .isEqualTo(Value.of(Arrays.asList(Value.of(1L), Value.of(2L)))); // forEach should show LONG_ARRAY type - Map entriesSeen = new HashMap<>(); + Map, Object> entriesSeen = new HashMap<>(); attributes.forEach(entriesSeen::put); assertThat(entriesSeen).containsExactly(entry(longArrayKey("key"), Arrays.asList(1L, 2L))); @@ -493,7 +493,7 @@ void valueStoredAsDoubleArray() { .isEqualTo(Value.of(Arrays.asList(Value.of(1.1), Value.of(2.2)))); // forEach should show DOUBLE_ARRAY type - Map entriesSeen = new HashMap<>(); + Map, Object> entriesSeen = new HashMap<>(); attributes.forEach(entriesSeen::put); assertThat(entriesSeen).containsExactly(entry(doubleArrayKey("key"), Arrays.asList(1.1, 2.2))); @@ -517,7 +517,7 @@ void valueStoredAsBooleanArray() { .isEqualTo(Value.of(Arrays.asList(Value.of(true), Value.of(false)))); // forEach should show BOOLEAN_ARRAY type - Map entriesSeen = new HashMap<>(); + Map, Object> entriesSeen = new HashMap<>(); attributes.forEach(entriesSeen::put); assertThat(entriesSeen) .containsExactly(entry(booleanArrayKey("key"), Arrays.asList(true, false))); @@ -527,6 +527,82 @@ void valueStoredAsBooleanArray() { .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 = 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 2a500f6bd17..d7d942713c7 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 @@ -345,27 +345,21 @@ private static Stream labelValueSerializationArgs() { Arrays.asList("stringValue1", "\"+\\\\\\+\b+\f+\n+\r+\t+" + (char) 0)), "[\"stringValue1\",\"\\\"+\\\\\\\\\\\\+\\b+\\f+\\n+\\r+\\t+\\u0000\"]"), Arguments.of( - Attributes.of(booleanArrayKey("key"), Arrays.asList(true, false)), - "[true,false]"), + Attributes.of(booleanArrayKey("key"), Arrays.asList(true, false)), "[true,false]"), Arguments.of( 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)), + 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(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()), - "")); + Arguments.of(Attributes.of(valueKey("key"), Value.empty()), "")); } static MetricData createSampleMetricData( From 0e1988597cbd8b4cf0e525c8a605acd60ade7f07 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 9 Jan 2026 16:38:10 -0800 Subject: [PATCH 05/22] toProtoJson --- .../api/common/KeyValueList.java | 11 +++++- .../common/{JsonUtil.java => ProtoJson.java} | 36 +++++++++---------- .../io/opentelemetry/api/common/Value.java | 28 ++++++++++++++- .../opentelemetry/api/common/ValueArray.java | 9 ++++- .../api/common/ValueBoolean.java | 7 +++- .../opentelemetry/api/common/ValueBytes.java | 8 ++++- .../opentelemetry/api/common/ValueDouble.java | 7 +++- .../opentelemetry/api/common/ValueEmpty.java | 5 +++ .../opentelemetry/api/common/ValueLong.java | 7 +++- .../opentelemetry/api/common/ValueString.java | 7 +++- .../current_vs_latest/opentelemetry-api.txt | 1 + .../prometheus/Otel2PrometheusConverter.java | 3 +- .../Otel2PrometheusConverterTest.java | 8 ++--- .../zipkin/EventDataToAnnotation.java | 4 +++ .../zipkin/OtelToZipkinSpanTransformer.java | 9 ++--- .../zipkin/EventDataToAnnotationTest.java | 2 +- .../OtelToZipkinSpanTransformerTest.java | 10 +++++- 17 files changed, 120 insertions(+), 42 deletions(-) rename api/all/src/main/java/io/opentelemetry/api/common/{JsonUtil.java => ProtoJson.java} (66%) 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 ea77bb0789a..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 @@ -5,6 +5,8 @@ package io.opentelemetry.api.common; +import static java.util.stream.Collectors.joining; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -48,8 +50,15 @@ public List getValue() { @Override public String asString() { + return value.stream() + .map(item -> item.getKey() + "=" + item.getValue().asString()) + .collect(joining(", ", "[", "]")); + } + + @Override + public String toProtoJson() { StringBuilder sb = new StringBuilder(); - JsonUtil.appendJsonValue(sb, this); + ProtoJson.append(sb, this); return sb.toString(); } diff --git a/api/all/src/main/java/io/opentelemetry/api/common/JsonUtil.java b/api/all/src/main/java/io/opentelemetry/api/common/ProtoJson.java similarity index 66% rename from api/all/src/main/java/io/opentelemetry/api/common/JsonUtil.java rename to api/all/src/main/java/io/opentelemetry/api/common/ProtoJson.java index 708eb4a0623..b757d955b63 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/JsonUtil.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ProtoJson.java @@ -9,36 +9,35 @@ import java.util.Base64; import java.util.List; -/** Package-private utility for JSON encoding. */ -final class JsonUtil { +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 appendJsonValue(StringBuilder sb, Value value) { + static void append(StringBuilder sb, Value value) { switch (value.getType()) { case STRING: - appendJsonString(sb, (String) value.getValue()); + appendString(sb, (String) value.getValue()); break; case LONG: sb.append(value.getValue()); break; case DOUBLE: - appendJsonDouble(sb, (Double) value.getValue()); + appendDouble(sb, (Double) value.getValue()); break; case BOOLEAN: sb.append(value.getValue()); break; case ARRAY: - appendJsonArray(sb, (List>) value.getValue()); + appendArray(sb, (List>) value.getValue()); break; case KEY_VALUE_LIST: - appendJsonKeyValueList(sb, (List) value.getValue()); + appendMap(sb, (List) value.getValue()); break; case BYTES: - appendJsonBytes(sb, (ByteBuffer) value.getValue()); + appendBytes(sb, (ByteBuffer) value.getValue()); break; case EMPTY: sb.append("null"); @@ -46,7 +45,7 @@ static void appendJsonValue(StringBuilder sb, Value value) { } } - static void appendJsonString(StringBuilder sb, String value) { + private static void appendString(StringBuilder sb, String value) { sb.append('"'); for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); @@ -88,49 +87,46 @@ static void appendJsonString(StringBuilder sb, String value) { sb.append('"'); } - private static void appendJsonDouble(StringBuilder sb, double value) { + private static void appendDouble(StringBuilder sb, double value) { if (Double.isNaN(value)) { - // Encoding as string to match ProtoJSON: https://protobuf.dev/programming-guides/json/ sb.append("\"NaN\""); } else if (Double.isInfinite(value)) { - // Encoding as string to match ProtoJSON: https://protobuf.dev/programming-guides/json/ sb.append(value > 0 ? "\"Infinity\"" : "\"-Infinity\""); } else { sb.append(value); } } - private static void appendJsonBytes(StringBuilder sb, ByteBuffer value) { - // Encoding as base64 to match ProtoJSON: https://protobuf.dev/programming-guides/json/ + 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 appendJsonArray(StringBuilder sb, List> values) { + private static void appendArray(StringBuilder sb, List> values) { sb.append('['); for (int i = 0; i < values.size(); i++) { if (i > 0) { sb.append(','); } - appendJsonValue(sb, values.get(i)); + append(sb, values.get(i)); } sb.append(']'); } - private static void appendJsonKeyValueList(StringBuilder sb, List values) { + 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); - appendJsonString(sb, kv.getKey()); + appendString(sb, kv.getKey()); sb.append(':'); - appendJsonValue(sb, kv.getValue()); + append(sb, kv.getValue()); } sb.append('}'); } - private JsonUtil() {} + 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 2dbffe47c09..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 @@ -120,6 +120,32 @@ static Value empty() { *

    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 0f9d67536c1..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 @@ -5,6 +5,8 @@ package io.opentelemetry.api.common; +import static java.util.stream.Collectors.joining; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -42,8 +44,13 @@ public List> getValue() { @Override public String asString() { + return value.stream().map(Value::asString).collect(joining(", ", "[", "]")); + } + + @Override + public String toProtoJson() { StringBuilder sb = new StringBuilder(); - JsonUtil.appendJsonValue(sb, this); + ProtoJson.append(sb, this); return sb.toString(); } 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 7080c6d03dd..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 @@ -31,8 +31,13 @@ public Boolean getValue() { @Override public String asString() { + return String.valueOf(value); + } + + @Override + public String toProtoJson() { StringBuilder sb = new StringBuilder(); - JsonUtil.appendJsonValue(sb, this); + ProtoJson.append(sb, this); return sb.toString(); } 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 4691a469bfa..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 @@ -7,6 +7,7 @@ import java.nio.ByteBuffer; import java.util.Arrays; +import java.util.Base64; import java.util.Objects; final class ValueBytes implements Value { @@ -34,8 +35,13 @@ public ByteBuffer getValue() { @Override public String asString() { + return Base64.getEncoder().encodeToString(raw); + } + + @Override + public String toProtoJson() { StringBuilder sb = new StringBuilder(); - JsonUtil.appendJsonValue(sb, this); + ProtoJson.append(sb, this); return sb.toString(); } 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 872ab66836e..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 @@ -31,8 +31,13 @@ public Double getValue() { @Override public String asString() { + return String.valueOf(value); + } + + @Override + public String toProtoJson() { StringBuilder sb = new StringBuilder(); - JsonUtil.appendJsonValue(sb, this); + ProtoJson.append(sb, this); return sb.toString(); } 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 index 17a3959719e..ec0e1d23db9 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java @@ -27,6 +27,11 @@ public Void getValue() { @Override public String asString() { + return ""; + } + + @Override + public String toProtoJson() { return "null"; } 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 94b235f0877..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 @@ -31,8 +31,13 @@ public Long getValue() { @Override public String asString() { + return String.valueOf(value); + } + + @Override + public String toProtoJson() { StringBuilder sb = new StringBuilder(); - JsonUtil.appendJsonValue(sb, this); + ProtoJson.append(sb, this); return sb.toString(); } 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 9d724d7559b..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 @@ -32,8 +32,13 @@ public String getValue() { @Override public String asString() { + return value; + } + + @Override + public String toProtoJson() { StringBuilder sb = new StringBuilder(); - JsonUtil.appendJsonValue(sb, this); + ProtoJson.append(sb, this); return sb.toString(); } diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index 70a661af20f..ce251801e76 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -13,6 +13,7 @@ Comparing source compatibility of opentelemetry-api-1.59.0-SNAPSHOT.jar against === 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/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverter.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverter.java index 257cab18db3..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 @@ -675,8 +675,7 @@ private static String toLabelValue(AttributeType type, Object attributeValue) { attributeValue.getClass().getName(), type.name())); } case VALUE: - // TODO this should be json representation - return ((Value) attributeValue).asString(); + 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 d7d942713c7..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 @@ -352,14 +352,14 @@ private static Stream labelValueSerializationArgs() { 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(new byte[] {1, 2, 3})), "\"AQID\""), Arguments.of( Attributes.of(valueKey("key"), Value.of(KeyValue.of("nested", Value.of("value")))), - "[nested=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()), "")); + "[\"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 d5578675f73..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; @@ -99,11 +100,7 @@ Span generateSpan(SpanData spanData) { Attributes spanAttributes = spanData.getAttributes(); spanAttributes.forEach( - (key, value) -> { - if (key.getType() != AttributeType.VALUE) { - spanBuilder.putTag(key.getKey(), valueToString(key, value)); - } - }); + (key, value) -> spanBuilder.putTag(key.getKey(), valueToString(key, value))); int droppedAttributes = spanData.getTotalAttributeCount() - spanAttributes.size(); if (droppedAttributes > 0) { spanBuilder.putTag(OTEL_DROPPED_ATTRIBUTES_COUNT, String.valueOf(droppedAttributes)); @@ -228,7 +225,7 @@ private static String valueToString(AttributeKey key, Object attributeValue) case DOUBLE_ARRAY: return commaSeparated((List) attributeValue); case VALUE: - throw new IllegalArgumentException("Unsupported attribute type: 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 7323c1b1467..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 @@ -35,7 +35,7 @@ void basicConversion() { .put(valueKey("v12"), Value.empty()) .build(); String expected = - "\"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\":ValueBytes{AQID},\"v10\":KeyValueList{[nested=value]},\"v11\":ValueArray{[string, 123]},\"v12\":ValueEmpty{}}"; + "\"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 a8025f6d9f3..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 @@ -21,6 +21,7 @@ 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; @@ -366,11 +367,14 @@ void generateSpan_WithAttributes() { .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(29) + .setTotalAttributeCount(32) .setTotalRecordedEvents(3) .setKind(SpanKind.CLIENT) .build(); @@ -386,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") From 0ad0d89f98fadf3d82675e7e32595808e086bcd5 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Sat, 10 Jan 2026 09:34:32 -0800 Subject: [PATCH 06/22] unit tests --- .../api/common/ValueProtoJsonTest.java | 368 ++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 api/all/src/test/java/io/opentelemetry/api/common/ValueProtoJsonTest.java diff --git a/api/all/src/test/java/io/opentelemetry/api/common/ValueProtoJsonTest.java b/api/all/src/test/java/io/opentelemetry/api/common/ValueProtoJsonTest.java new file mode 100644 index 00000000000..3c9c6f31c3f --- /dev/null +++ b/api/all/src/test/java/io/opentelemetry/api/common/ValueProtoJsonTest.java @@ -0,0 +1,368 @@ +/* + * 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 ValueProtoJsonTest { + + @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\":{}}"); + } +} From 3b6a9069097475369c7df89a25cf73a020c48f06 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Sat, 10 Jan 2026 10:47:15 -0800 Subject: [PATCH 07/22] test coverage --- .../api/common/AttributesTest.java | 39 +++++++++++++++++++ ...sonTest.java => ValueToProtoJsonTest.java} | 26 ++++++++++++- .../testing/assertj/LogAssertionsTest.java | 39 +++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) rename api/all/src/test/java/io/opentelemetry/api/common/{ValueProtoJsonTest.java => ValueToProtoJsonTest.java} (94%) 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 7767fbfb80c..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 @@ -943,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/ValueProtoJsonTest.java b/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java similarity index 94% rename from api/all/src/test/java/io/opentelemetry/api/common/ValueProtoJsonTest.java rename to api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java index 3c9c6f31c3f..52fba6279c6 100644 --- a/api/all/src/test/java/io/opentelemetry/api/common/ValueProtoJsonTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java @@ -12,7 +12,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; -class ValueProtoJsonTest { +class ValueToProtoJsonTest { @Test void valueString_basic() { @@ -365,4 +365,28 @@ void edgeCase_mapWithEmptyMap() { .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/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 4168a128f79..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 @@ -335,4 +335,43 @@ void logBodyAssertions() { .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})); + } } From bc77eedf2e74c2a0c3d532a305a8adfd24782d0e Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 19 Jan 2026 19:05:44 -0800 Subject: [PATCH 08/22] improve javadoc --- .../java/io/opentelemetry/api/common/AttributesBuilder.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 5548cdc4296..e46d099d12f 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 @@ -200,7 +200,9 @@ default AttributesBuilder put(String key, boolean... value) { } /** - * Puts a {@link Value} attribute into this. + * Puts a {@link Value} attribute into this. See {@link #put(AttributeKey, Object)} for details on + * how this method will automatically convert {@link Value} attributes to simple attributes when + * possible. * *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate * your keys, if possible. From 543e8eb6c31e28e81ac2f82a3d28f10978563cb7 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 19 Jan 2026 19:16:19 -0800 Subject: [PATCH 09/22] better empty value representation --- .../io/opentelemetry/api/common/Empty.java | 36 +++++++++++++++++++ .../io/opentelemetry/api/common/Value.java | 4 +-- .../opentelemetry/api/common/ValueEmpty.java | 8 ++--- 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 api/all/src/main/java/io/opentelemetry/api/common/Empty.java diff --git a/api/all/src/main/java/io/opentelemetry/api/common/Empty.java b/api/all/src/main/java/io/opentelemetry/api/common/Empty.java new file mode 100644 index 00000000000..a965feda5ce --- /dev/null +++ b/api/all/src/main/java/io/opentelemetry/api/common/Empty.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +/** + * A singleton class representing an empty value, used as the generic type parameter for {@link + * Value} when representing empty values. + */ +public final class Empty { + + private static final Empty INSTANCE = new Empty(); + + private Empty() {} + + public static Empty getInstance() { + return INSTANCE; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Empty; + } + + @Override + public int hashCode() { + return 0; + } + + @Override + public String toString() { + return "Empty"; + } +} 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 e68706d0769..d8654801af2 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 @@ -86,7 +86,7 @@ static Value> of(Map> value) { } /** Returns an empty {@link Value}. */ - static Value empty() { + static Value empty() { return ValueEmpty.create(); } @@ -107,7 +107,7 @@ static Value empty() { *

  • {@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} + *
  • {@link ValueType#EMPTY} returns {@link Empty} * */ T getValue(); 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 index ec0e1d23db9..81c56198fd5 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java @@ -5,13 +5,13 @@ package io.opentelemetry.api.common; -final class ValueEmpty implements Value { +final class ValueEmpty implements Value { private static final ValueEmpty INSTANCE = new ValueEmpty(); private ValueEmpty() {} - static Value create() { + static Value create() { return INSTANCE; } @@ -21,8 +21,8 @@ public ValueType getType() { } @Override - public Void getValue() { - return null; + public Empty getValue() { + return Empty.getInstance(); } @Override From c133eea1e6190bef3b105f776016c4dd4e140150 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 19 Jan 2026 19:22:27 -0800 Subject: [PATCH 10/22] deprecate extended attributes incubating api --- .../api/incubator/common/ExtendedAttributeKey.java | 4 ++++ .../api/incubator/common/ExtendedAttributeType.java | 5 +++++ .../api/incubator/common/ExtendedAttributes.java | 7 +++++++ .../incubator/common/ExtendedAttributesBuilder.java | 11 ++++++++++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java index 4f48dbd8973..8d03083449c 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java @@ -30,7 +30,11 @@ * * * @param The type of value that can be set with the key. + * @deprecated Use {@link io.opentelemetry.api.common.AttributeKey} instead. Complex attributes are + * now supported directly in the standard API via {@link + * io.opentelemetry.api.common.AttributeKey#valueKey(String)}. */ +@Deprecated @Immutable public interface ExtendedAttributeKey { /** Returns the underlying String representation of the key. */ diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java index 26e655e9ab2..614b0e54d81 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java @@ -10,7 +10,12 @@ * hence the types of values that are allowed for {@link ExtendedAttributes}. * *

    This is a superset of {@link io.opentelemetry.api.common.AttributeType}, + * + * @deprecated Use {@link io.opentelemetry.api.common.AttributeType} instead. Complex attributes are + * now supported directly in the standard API via {@link + * io.opentelemetry.api.common.AttributeType#VALUE}. */ +@Deprecated public enum ExtendedAttributeType { // Types copied AttributeType STRING, diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java index 14a16778d3d..44d10921f42 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java @@ -47,7 +47,14 @@ * {@link ExtendedAttributes} *

  • {@link #get(AttributeKey)} supports reading values using standard {@link AttributeKey} * + * + * @deprecated Use {@link io.opentelemetry.api.common.Attributes} instead. Complex attributes are + * now supported directly in the standard API via {@link + * io.opentelemetry.api.common.AttributeKey#valueKey(String)} and {@link + * io.opentelemetry.api.common.AttributesBuilder#put(io.opentelemetry.api.common.AttributeKey, + * Object)}. */ +@Deprecated @Immutable public interface ExtendedAttributes { diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java index 0f4b9c942e2..bc45736efe7 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java @@ -22,7 +22,16 @@ import java.util.List; import java.util.function.Predicate; -/** A builder of {@link ExtendedAttributes} supporting an arbitrary number of key-value pairs. */ +/** + * A builder of {@link ExtendedAttributes} supporting an arbitrary number of key-value pairs. + * + * @deprecated Use {@link io.opentelemetry.api.common.AttributesBuilder} instead. Complex attributes + * are now supported directly in the standard API via {@link + * io.opentelemetry.api.common.AttributeKey#valueKey(String)} and {@link + * io.opentelemetry.api.common.AttributesBuilder#put(io.opentelemetry.api.common.AttributeKey, + * Object)}. + */ +@Deprecated public interface ExtendedAttributesBuilder { /** Create the {@link ExtendedAttributes} from this. */ ExtendedAttributes build(); From 3a8b7962f05144b0612eeba512bdf9763ca90af5 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 19 Jan 2026 19:32:16 -0800 Subject: [PATCH 11/22] Use asString instead of toProtoJson --- .../api/common/KeyValueList.java | 9 -- .../io/opentelemetry/api/common/Value.java | 15 +-- .../opentelemetry/api/common/ValueArray.java | 7 -- .../api/common/ValueBoolean.java | 5 - .../opentelemetry/api/common/ValueBytes.java | 6 - .../opentelemetry/api/common/ValueDouble.java | 5 - .../opentelemetry/api/common/ValueEmpty.java | 5 - .../opentelemetry/api/common/ValueLong.java | 5 - .../opentelemetry/api/common/ValueString.java | 5 - .../api/common/ValueToProtoJsonTest.java | 119 +++++++----------- .../io/opentelemetry/api/logs/ValueTest.java | 17 +-- .../prometheus/Otel2PrometheusConverter.java | 2 +- .../zipkin/EventDataToAnnotation.java | 2 +- .../zipkin/OtelToZipkinSpanTransformer.java | 2 +- 14 files changed, 61 insertions(+), 143 deletions(-) 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 9035f39bb42..e74a6cebe01 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 @@ -5,8 +5,6 @@ package io.opentelemetry.api.common; -import static java.util.stream.Collectors.joining; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -50,13 +48,6 @@ public List getValue() { @Override public String asString() { - return value.stream() - .map(item -> item.getKey() + "=" + item.getValue().asString()) - .collect(joining(", ", "[", "]")); - } - - @Override - public String toProtoJson() { StringBuilder sb = new StringBuilder(); ProtoJson.append(sb, this); return sb.toString(); 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 d8654801af2..ddb7f04fa20 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 @@ -112,17 +112,6 @@ static Value empty() { */ T getValue(); - /** - * Return a string encoding of this {@link Value}. This is intended to be a fallback serialized - * representation in case there is no suitable encoding that can utilize {@link #getType()} / - * {@link #getValue()} to serialize specific types. - * - *

    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 deprecate in favor of toString() or toProtoJson()? - String asString(); - /** * Returns a JSON encoding of this {@link Value}. * @@ -145,7 +134,5 @@ static Value empty() { * * @return a JSON encoding of this value */ - default String toProtoJson() { - return "\"unimplemented\""; - } + String asString(); } 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 4f9173b379f..424154a36e2 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 @@ -5,8 +5,6 @@ package io.opentelemetry.api.common; -import static java.util.stream.Collectors.joining; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -44,11 +42,6 @@ public List> getValue() { @Override 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(); 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 ae1f3997361..bfbcc53f663 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 @@ -31,11 +31,6 @@ public Boolean getValue() { @Override public String asString() { - return String.valueOf(value); - } - - @Override - public String toProtoJson() { StringBuilder sb = new StringBuilder(); ProtoJson.append(sb, this); return sb.toString(); 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 6fa4f4cfaac..b06534e48d2 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 @@ -7,7 +7,6 @@ import java.nio.ByteBuffer; import java.util.Arrays; -import java.util.Base64; import java.util.Objects; final class ValueBytes implements Value { @@ -35,11 +34,6 @@ public ByteBuffer getValue() { @Override public String asString() { - return Base64.getEncoder().encodeToString(raw); - } - - @Override - public String toProtoJson() { StringBuilder sb = new StringBuilder(); ProtoJson.append(sb, this); return sb.toString(); 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 7bcf5162387..411ce58f18f 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 @@ -31,11 +31,6 @@ public Double getValue() { @Override public String asString() { - return String.valueOf(value); - } - - @Override - public String toProtoJson() { StringBuilder sb = new StringBuilder(); ProtoJson.append(sb, this); return sb.toString(); 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 index 81c56198fd5..06a346a1e2b 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java @@ -27,11 +27,6 @@ public Empty getValue() { @Override public String asString() { - return ""; - } - - @Override - public String toProtoJson() { return "null"; } 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 921f4cea48e..20c4b99c85a 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 @@ -31,11 +31,6 @@ public Long getValue() { @Override public String asString() { - return String.valueOf(value); - } - - @Override - public String toProtoJson() { StringBuilder sb = new StringBuilder(); ProtoJson.append(sb, this); return sb.toString(); 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 05b690bb6c3..0d338b7bf93 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 @@ -32,11 +32,6 @@ public String getValue() { @Override public String asString() { - return value; - } - - @Override - public String toProtoJson() { StringBuilder sb = new StringBuilder(); ProtoJson.append(sb, this); return sb.toString(); 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 index 52fba6279c6..368babb832b 100644 --- a/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java @@ -16,157 +16,156 @@ class ValueToProtoJsonTest { @Test void valueString_basic() { - assertThat(Value.of("hello").toProtoJson()).isEqualTo("\"hello\""); + assertThat(Value.of("hello").asString()).isEqualTo("\"hello\""); } @Test void valueString_empty() { - assertThat(Value.of("").toProtoJson()).isEqualTo("\"\""); + assertThat(Value.of("").asString()).isEqualTo("\"\""); } @Test void valueString_withEscapes() { - assertThat(Value.of("line1\nline2\ttab").toProtoJson()).isEqualTo("\"line1\\nline2\\ttab\""); + assertThat(Value.of("line1\nline2\ttab").asString()).isEqualTo("\"line1\\nline2\\ttab\""); } @Test void valueString_withQuotes() { - assertThat(Value.of("say \"hello\"").toProtoJson()).isEqualTo("\"say \\\"hello\\\"\""); + assertThat(Value.of("say \"hello\"").asString()).isEqualTo("\"say \\\"hello\\\"\""); } @Test void valueString_withBackslash() { - assertThat(Value.of("path\\to\\file").toProtoJson()).isEqualTo("\"path\\\\to\\\\file\""); + assertThat(Value.of("path\\to\\file").asString()).isEqualTo("\"path\\\\to\\\\file\""); } @Test void valueString_withControlCharacters() { - assertThat(Value.of("\u0000\u0001\u001F").toProtoJson()).isEqualTo("\"\\u0000\\u0001\\u001f\""); + assertThat(Value.of("\u0000\u0001\u001F").asString()).isEqualTo("\"\\u0000\\u0001\\u001f\""); } @Test void valueString_unicode() { - assertThat(Value.of("Hello δΈ–η•Œ 🌍").toProtoJson()).isEqualTo("\"Hello δΈ–η•Œ 🌍\""); + assertThat(Value.of("Hello δΈ–η•Œ 🌍").asString()).isEqualTo("\"Hello δΈ–η•Œ 🌍\""); } @Test void valueBoolean_true() { - assertThat(Value.of(true).toProtoJson()).isEqualTo("true"); + assertThat(Value.of(true).asString()).isEqualTo("true"); } @Test void valueBoolean_false() { - assertThat(Value.of(false).toProtoJson()).isEqualTo("false"); + assertThat(Value.of(false).asString()).isEqualTo("false"); } @Test void valueLong_positive() { - assertThat(Value.of(42L).toProtoJson()).isEqualTo("42"); + assertThat(Value.of(42L).asString()).isEqualTo("42"); } @Test void valueLong_negative() { - assertThat(Value.of(-123L).toProtoJson()).isEqualTo("-123"); + assertThat(Value.of(-123L).asString()).isEqualTo("-123"); } @Test void valueLong_zero() { - assertThat(Value.of(0L).toProtoJson()).isEqualTo("0"); + assertThat(Value.of(0L).asString()).isEqualTo("0"); } @Test void valueLong_maxValue() { - assertThat(Value.of(Long.MAX_VALUE).toProtoJson()).isEqualTo("9223372036854775807"); + assertThat(Value.of(Long.MAX_VALUE).asString()).isEqualTo("9223372036854775807"); } @Test void valueLong_minValue() { - assertThat(Value.of(Long.MIN_VALUE).toProtoJson()).isEqualTo("-9223372036854775808"); + assertThat(Value.of(Long.MIN_VALUE).asString()).isEqualTo("-9223372036854775808"); } @Test void valueDouble_regular() { - assertThat(Value.of(3.14).toProtoJson()).isEqualTo("3.14"); + assertThat(Value.of(3.14).asString()).isEqualTo("3.14"); } @Test void valueDouble_negative() { - assertThat(Value.of(-2.5).toProtoJson()).isEqualTo("-2.5"); + assertThat(Value.of(-2.5).asString()).isEqualTo("-2.5"); } @Test void valueDouble_zero() { - assertThat(Value.of(0.0).toProtoJson()).isEqualTo("0.0"); + assertThat(Value.of(0.0).asString()).isEqualTo("0.0"); } @Test void valueDouble_negativeZero() { - assertThat(Value.of(-0.0).toProtoJson()).isEqualTo("-0.0"); + assertThat(Value.of(-0.0).asString()).isEqualTo("-0.0"); } @Test void valueDouble_nan() { - assertThat(Value.of(Double.NaN).toProtoJson()).isEqualTo("\"NaN\""); + assertThat(Value.of(Double.NaN).asString()).isEqualTo("\"NaN\""); } @Test void valueDouble_positiveInfinity() { - assertThat(Value.of(Double.POSITIVE_INFINITY).toProtoJson()).isEqualTo("\"Infinity\""); + assertThat(Value.of(Double.POSITIVE_INFINITY).asString()).isEqualTo("\"Infinity\""); } @Test void valueDouble_negativeInfinity() { - assertThat(Value.of(Double.NEGATIVE_INFINITY).toProtoJson()).isEqualTo("\"-Infinity\""); + assertThat(Value.of(Double.NEGATIVE_INFINITY).asString()).isEqualTo("\"-Infinity\""); } @Test void valueDouble_scientificNotation() { - assertThat(Value.of(1.23e10).toProtoJson()).isEqualTo("1.23E10"); + assertThat(Value.of(1.23e10).asString()).isEqualTo("1.23E10"); } @Test void valueDouble_verySmall() { - assertThat(Value.of(1.23e-10).toProtoJson()).isEqualTo("1.23E-10"); + assertThat(Value.of(1.23e-10).asString()).isEqualTo("1.23E-10"); } @Test void valueBytes_empty() { - assertThat(Value.of(new byte[] {}).toProtoJson()).isEqualTo("\"\""); + assertThat(Value.of(new byte[] {}).asString()).isEqualTo("\"\""); } @Test void valueBytes_regular() { byte[] bytes = new byte[] {0, 1, 2, Byte.MAX_VALUE, Byte.MIN_VALUE}; - assertThat(Value.of(bytes).toProtoJson()) + assertThat(Value.of(bytes).asString()) .isEqualTo('"' + Base64.getEncoder().encodeToString(bytes) + '"'); } @Test void valueEmpty() { - assertThat(Value.empty().toProtoJson()).isEqualTo("null"); + assertThat(Value.empty().asString()).isEqualTo("null"); } @Test @SuppressWarnings("ExplicitArrayForVarargs") void valueArray_empty() { - assertThat(Value.of(new Value[] {}).toProtoJson()).isEqualTo("[]"); + assertThat(Value.of(new Value[] {}).asString()).isEqualTo("[]"); } @Test void valueArray_singleElement() { - assertThat(Value.of(Value.of("test")).toProtoJson()).isEqualTo("[\"test\"]"); + assertThat(Value.of(Value.of("test")).asString()).isEqualTo("[\"test\"]"); } @Test void valueArray_multipleStrings() { - assertThat(Value.of(Value.of("a"), Value.of("b"), Value.of("c")).toProtoJson()) + assertThat(Value.of(Value.of("a"), Value.of("b"), Value.of("c")).asString()) .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]"); + assertThat(Value.of(Value.of(1L), Value.of(2L), Value.of(3L)).asString()).isEqualTo("[1,2,3]"); } @Test @@ -179,7 +178,7 @@ void valueArray_mixedTypes() { Value.of(true), Value.of(false), Value.empty()) - .toProtoJson()) + .asString()) .isEqualTo("[\"string\",42,3.14,true,false,null]"); } @@ -190,7 +189,7 @@ void valueArray_nested() { Value.of("outer"), Value.of(Value.of("inner1"), Value.of("inner2")), Value.of(42L)) - .toProtoJson()) + .asString()) .isEqualTo("[\"outer\",[\"inner1\",\"inner2\"],42]"); } @@ -198,19 +197,19 @@ void valueArray_nested() { void valueArray_deeplyNested() { assertThat( Value.of(Value.of(Value.of(Value.of(Value.of(Value.of("deep"))))), Value.of("shallow")) - .toProtoJson()) + .asString()) .isEqualTo("[[[[[\"deep\"]]]],\"shallow\"]"); } @Test @SuppressWarnings("ExplicitArrayForVarargs") void valueKeyValueList_empty() { - assertThat(Value.of(new KeyValue[] {}).toProtoJson()).isEqualTo("{}"); + assertThat(Value.of(new KeyValue[] {}).asString()).isEqualTo("{}"); } @Test void valueKeyValueList_singleEntry() { - assertThat(Value.of(KeyValue.of("key", Value.of("value"))).toProtoJson()) + assertThat(Value.of(KeyValue.of("key", Value.of("value"))).asString()) .isEqualTo("{\"key\":\"value\"}"); } @@ -221,7 +220,7 @@ void valueKeyValueList_multipleEntries() { KeyValue.of("name", Value.of("Alice")), KeyValue.of("age", Value.of(30L)), KeyValue.of("active", Value.of(true))) - .toProtoJson()) + .asString()) .isEqualTo("{\"name\":\"Alice\",\"age\":30,\"active\":true}"); } @@ -235,7 +234,7 @@ void valueKeyValueList_nestedMap() { Value.of( KeyValue.of("nested1", Value.of("a")), KeyValue.of("nested2", Value.of("b"))))) - .toProtoJson()) + .asString()) .isEqualTo("{\"outer\":\"value\",\"inner\":{\"nested1\":\"a\",\"nested2\":\"b\"}}"); } @@ -245,7 +244,7 @@ void valueKeyValueList_withArray() { Value.of( KeyValue.of("name", Value.of("test")), KeyValue.of("items", Value.of(Value.of(1L), Value.of(2L), Value.of(3L)))) - .toProtoJson()) + .asString()) .isEqualTo("{\"name\":\"test\",\"items\":[1,2,3]}"); } @@ -260,7 +259,7 @@ void valueKeyValueList_allTypes() { 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()) + .asString()) .isEqualTo( "{\"string\":\"text\",\"long\":42,\"double\":3.14,\"bool\":true," + "\"empty\":null,\"bytes\":\"AQI=\",\"array\":[\"a\",\"b\"]}"); @@ -271,7 +270,7 @@ 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}"); + assertThat(Value.of(map).asString()).isEqualTo("{\"key1\":\"value1\",\"key2\":42}"); } @Test @@ -281,7 +280,7 @@ void valueKeyValueList_keyWithSpecialCharacters() { 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()) + .asString()) .isEqualTo( "{\"key with spaces\":\"value1\"," + "\"key\\\"with\\\"quotes\":\"value2\"," @@ -311,7 +310,7 @@ void complexNestedStructure() { Value.of( Value.of("important"), Value.of("reviewed"), Value.of("final")))))); - assertThat(complexValue.toProtoJson()) + assertThat(complexValue.asString()) .isEqualTo( "{\"user\":\"Alice\"," + "\"scores\":[95,87.5,92,\"NaN\",\"Infinity\"]," @@ -322,13 +321,13 @@ void complexNestedStructure() { @Test void edgeCase_emptyStringKey() { - assertThat(Value.of(KeyValue.of("", Value.of("value"))).toProtoJson()) + assertThat(Value.of(KeyValue.of("", Value.of("value"))).asString()) .isEqualTo("{\"\":\"value\"}"); } @Test void edgeCase_multipleEmptyValues() { - assertThat(Value.of(Value.empty(), Value.empty(), Value.empty()).toProtoJson()) + assertThat(Value.of(Value.empty(), Value.empty(), Value.empty()).asString()) .isEqualTo("[null,null,null]"); } @@ -339,7 +338,7 @@ void edgeCase_arrayOfMaps() { 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()) + .asString()) .isEqualTo( "[{\"id\":1,\"name\":\"A\"},{\"id\":2,\"name\":\"B\"},{\"id\":3,\"name\":\"C\"}]"); } @@ -351,7 +350,7 @@ void edgeCase_mapWithEmptyArray() { Value.of( KeyValue.of("data", Value.of("test")), KeyValue.of("items", Value.of(new Value[] {}))) - .toProtoJson()) + .asString()) .isEqualTo("{\"data\":\"test\",\"items\":[]}"); } @@ -362,31 +361,7 @@ void edgeCase_mapWithEmptyMap() { Value.of( KeyValue.of("data", Value.of("test")), KeyValue.of("metadata", Value.of(new KeyValue[] {}))) - .toProtoJson()) + .asString()) .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/all/src/test/java/io/opentelemetry/api/logs/ValueTest.java b/api/all/src/test/java/io/opentelemetry/api/logs/ValueTest.java index ae83e0dd44c..4fef6fc6fa1 100644 --- a/api/all/src/test/java/io/opentelemetry/api/logs/ValueTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/logs/ValueTest.java @@ -172,18 +172,18 @@ void asString(Value value, String expectedAsString) { private static Stream asStringArgs() { return Stream.of( // primitives - arguments(Value.of("str"), "str"), + arguments(Value.of("str"), "\"str\""), arguments(Value.of(true), "true"), arguments(Value.of(1), "1"), arguments(Value.of(1.1), "1.1"), // heterogeneous array arguments( Value.of(Value.of("str"), Value.of(true), Value.of(1), Value.of(1.1)), - "[str, true, 1, 1.1]"), + "[\"str\",true,1,1.1]"), // key value list from KeyValue array arguments( Value.of(KeyValue.of("key1", Value.of("val1")), KeyValue.of("key2", Value.of(2))), - "[key1=val1, key2=2]"), + "{\"key1\":\"val1\",\"key2\":2}"), // key value list from map arguments( Value.of( @@ -193,15 +193,16 @@ private static Stream asStringArgs() { put("key2", Value.of(2)); } }), - "[key1=val1, key2=2]"), + "{\"key1\":\"val1\",\"key2\":2}"), // map of map arguments( Value.of( Collections.singletonMap( "child", Value.of(Collections.singletonMap("grandchild", Value.of("str"))))), - "[child=[grandchild=str]]"), + "{\"child\":{\"grandchild\":\"str\"}}"), // bytes - arguments(Value.of("hello world".getBytes(StandardCharsets.UTF_8)), "aGVsbG8gd29ybGQ=")); + arguments( + Value.of("hello world".getBytes(StandardCharsets.UTF_8)), "\"aGVsbG8gd29ybGQ=\"")); } @Test @@ -209,7 +210,9 @@ void valueByteAsString() { // TODO: add more test cases String str = "hello world"; String base64Encoded = Value.of(str.getBytes(StandardCharsets.UTF_8)).asString(); - byte[] decodedBytes = Base64.getDecoder().decode(base64Encoded); + // Remove surrounding quotes from JSON string + String base64Value = base64Encoded.substring(1, base64Encoded.length() - 1); + byte[] decodedBytes = Base64.getDecoder().decode(base64Value); assertThat(new String(decodedBytes, StandardCharsets.UTF_8)).isEqualTo(str); } } 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 a32db09916f..d5889b23561 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 @@ -675,7 +675,7 @@ private static String toLabelValue(AttributeType type, Object attributeValue) { attributeValue.getClass().getName(), type.name())); } case VALUE: - return ((Value) attributeValue).toProtoJson(); + return ((Value) attributeValue).asString(); } throw new IllegalStateException("Unrecognized AttributeType: " + type); } 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 3365bd0d4bf..00474dcd48e 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 @@ -45,7 +45,7 @@ private static String toValue(Object o) { .stream().map(EventDataToAnnotation::toValue).collect(joining(",", "[", "]")); } if (o instanceof Value) { - return ((Value) o).toProtoJson(); + return ((Value) o).asString(); } 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 cead6809643..57d12ec5864 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 @@ -225,7 +225,7 @@ private static String valueToString(AttributeKey key, Object attributeValue) case DOUBLE_ARRAY: return commaSeparated((List) attributeValue); case VALUE: - return ((Value) attributeValue).toProtoJson(); + return ((Value) attributeValue).asString(); } throw new IllegalStateException("Unknown attribute type: " + type); } From 4fbab3562b45c48af560754fbc76b30722177c3f Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 19 Jan 2026 19:40:22 -0800 Subject: [PATCH 12/22] parameterized test --- .../api/common/ValueToProtoJsonTest.java | 416 +++++++----------- 1 file changed, 150 insertions(+), 266 deletions(-) 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 index 368babb832b..565d4f2e3bc 100644 --- a/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java @@ -10,135 +10,86 @@ import java.util.Base64; import java.util.LinkedHashMap; import java.util.Map; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class ValueToProtoJsonTest { - @Test - void valueString_basic() { - assertThat(Value.of("hello").asString()).isEqualTo("\"hello\""); - } - - @Test - void valueString_empty() { - assertThat(Value.of("").asString()).isEqualTo("\"\""); - } - - @Test - void valueString_withEscapes() { - assertThat(Value.of("line1\nline2\ttab").asString()).isEqualTo("\"line1\\nline2\\ttab\""); - } - - @Test - void valueString_withQuotes() { - assertThat(Value.of("say \"hello\"").asString()).isEqualTo("\"say \\\"hello\\\"\""); - } - - @Test - void valueString_withBackslash() { - assertThat(Value.of("path\\to\\file").asString()).isEqualTo("\"path\\\\to\\\\file\""); - } - - @Test - void valueString_withControlCharacters() { - assertThat(Value.of("\u0000\u0001\u001F").asString()).isEqualTo("\"\\u0000\\u0001\\u001f\""); - } - - @Test - void valueString_unicode() { - assertThat(Value.of("Hello δΈ–η•Œ 🌍").asString()).isEqualTo("\"Hello δΈ–η•Œ 🌍\""); - } - - @Test - void valueBoolean_true() { - assertThat(Value.of(true).asString()).isEqualTo("true"); - } - - @Test - void valueBoolean_false() { - assertThat(Value.of(false).asString()).isEqualTo("false"); - } - - @Test - void valueLong_positive() { - assertThat(Value.of(42L).asString()).isEqualTo("42"); - } - - @Test - void valueLong_negative() { - assertThat(Value.of(-123L).asString()).isEqualTo("-123"); - } - - @Test - void valueLong_zero() { - assertThat(Value.of(0L).asString()).isEqualTo("0"); - } - - @Test - void valueLong_maxValue() { - assertThat(Value.of(Long.MAX_VALUE).asString()).isEqualTo("9223372036854775807"); - } - - @Test - void valueLong_minValue() { - assertThat(Value.of(Long.MIN_VALUE).asString()).isEqualTo("-9223372036854775808"); - } - - @Test - void valueDouble_regular() { - assertThat(Value.of(3.14).asString()).isEqualTo("3.14"); - } - - @Test - void valueDouble_negative() { - assertThat(Value.of(-2.5).asString()).isEqualTo("-2.5"); + @ParameterizedTest + @MethodSource("stringValueProvider") + void valueString(String input, String expectedJson) { + assertThat(Value.of(input).asString()).isEqualTo(expectedJson); } - @Test - void valueDouble_zero() { - assertThat(Value.of(0.0).asString()).isEqualTo("0.0"); + private static Stream stringValueProvider() { + return Stream.of( + Arguments.of("hello", "\"hello\""), + Arguments.of("", "\"\""), + Arguments.of("line1\nline2\ttab", "\"line1\\nline2\\ttab\""), + Arguments.of("say \"hello\"", "\"say \\\"hello\\\"\""), + Arguments.of("path\\to\\file", "\"path\\\\to\\\\file\""), + Arguments.of("\u0000\u0001\u001F", "\"\\u0000\\u0001\\u001f\""), + Arguments.of("Hello δΈ–η•Œ 🌍", "\"Hello δΈ–η•Œ 🌍\"")); } - @Test - void valueDouble_negativeZero() { - assertThat(Value.of(-0.0).asString()).isEqualTo("-0.0"); + @ParameterizedTest + @MethodSource("booleanValueProvider") + void valueBoolean(boolean input, String expectedJson) { + assertThat(Value.of(input).asString()).isEqualTo(expectedJson); } - @Test - void valueDouble_nan() { - assertThat(Value.of(Double.NaN).asString()).isEqualTo("\"NaN\""); + private static Stream booleanValueProvider() { + return Stream.of(Arguments.of(true, "true"), Arguments.of(false, "false")); } - @Test - void valueDouble_positiveInfinity() { - assertThat(Value.of(Double.POSITIVE_INFINITY).asString()).isEqualTo("\"Infinity\""); + @ParameterizedTest + @MethodSource("longValueProvider") + void valueLong(long input, String expectedJson) { + assertThat(Value.of(input).asString()).isEqualTo(expectedJson); } - @Test - void valueDouble_negativeInfinity() { - assertThat(Value.of(Double.NEGATIVE_INFINITY).asString()).isEqualTo("\"-Infinity\""); + private static Stream longValueProvider() { + return Stream.of( + Arguments.of(42L, "42"), + Arguments.of(-123L, "-123"), + Arguments.of(0L, "0"), + Arguments.of(Long.MAX_VALUE, "9223372036854775807"), + Arguments.of(Long.MIN_VALUE, "-9223372036854775808")); } - @Test - void valueDouble_scientificNotation() { - assertThat(Value.of(1.23e10).asString()).isEqualTo("1.23E10"); + @ParameterizedTest + @MethodSource("doubleValueProvider") + void valueDouble(double input, String expectedJson) { + assertThat(Value.of(input).asString()).isEqualTo(expectedJson); } - @Test - void valueDouble_verySmall() { - assertThat(Value.of(1.23e-10).asString()).isEqualTo("1.23E-10"); + private static Stream doubleValueProvider() { + return Stream.of( + Arguments.of(3.14, "3.14"), + Arguments.of(-2.5, "-2.5"), + Arguments.of(0.0, "0.0"), + Arguments.of(-0.0, "-0.0"), + Arguments.of(Double.NaN, "\"NaN\""), + Arguments.of(Double.POSITIVE_INFINITY, "\"Infinity\""), + Arguments.of(Double.NEGATIVE_INFINITY, "\"-Infinity\""), + Arguments.of(1.23e10, "1.23E10"), + Arguments.of(1.23e-10, "1.23E-10")); } - @Test - void valueBytes_empty() { - assertThat(Value.of(new byte[] {}).asString()).isEqualTo("\"\""); + @ParameterizedTest + @MethodSource("bytesValueProvider") + void valueBytes(byte[] input, String expectedJson) { + assertThat(Value.of(input).asString()).isEqualTo(expectedJson); } - @Test - void valueBytes_regular() { - byte[] bytes = new byte[] {0, 1, 2, Byte.MAX_VALUE, Byte.MIN_VALUE}; - assertThat(Value.of(bytes).asString()) - .isEqualTo('"' + Base64.getEncoder().encodeToString(bytes) + '"'); + private static Stream bytesValueProvider() { + byte[] regularBytes = new byte[] {0, 1, 2, Byte.MAX_VALUE, Byte.MIN_VALUE}; + return Stream.of( + Arguments.of(new byte[] {}, "\"\""), + Arguments.of(regularBytes, '"' + Base64.getEncoder().encodeToString(regularBytes) + '"')); } @Test @@ -146,145 +97,94 @@ void valueEmpty() { assertThat(Value.empty().asString()).isEqualTo("null"); } - @Test + @ParameterizedTest + @MethodSource("arrayValueProvider") @SuppressWarnings("ExplicitArrayForVarargs") - void valueArray_empty() { - assertThat(Value.of(new Value[] {}).asString()).isEqualTo("[]"); + void valueArray(Value input, String expectedJson) { + assertThat(input.asString()).isEqualTo(expectedJson); } - @Test - void valueArray_singleElement() { - assertThat(Value.of(Value.of("test")).asString()).isEqualTo("[\"test\"]"); - } - - @Test - void valueArray_multipleStrings() { - assertThat(Value.of(Value.of("a"), Value.of("b"), Value.of("c")).asString()) - .isEqualTo("[\"a\",\"b\",\"c\"]"); - } - - @Test - void valueArray_multipleNumbers() { - assertThat(Value.of(Value.of(1L), Value.of(2L), Value.of(3L)).asString()).isEqualTo("[1,2,3]"); - } - - @Test - void valueArray_mixedTypes() { - assertThat( + @SuppressWarnings("ExplicitArrayForVarargs") + private static Stream arrayValueProvider() { + return Stream.of( + Arguments.of(Value.of(new Value[] {}), "[]"), + Arguments.of(Value.of(Value.of("test")), "[\"test\"]"), + Arguments.of(Value.of(Value.of("a"), Value.of("b"), Value.of("c")), "[\"a\",\"b\",\"c\"]"), + Arguments.of(Value.of(Value.of(1L), Value.of(2L), Value.of(3L)), "[1,2,3]"), + Arguments.of( Value.of( - Value.of("string"), - Value.of(42L), - Value.of(3.14), - Value.of(true), - Value.of(false), - Value.empty()) - .asString()) - .isEqualTo("[\"string\",42,3.14,true,false,null]"); - } - - @Test - void valueArray_nested() { - assertThat( + Value.of("string"), + Value.of(42L), + Value.of(3.14), + Value.of(true), + Value.of(false), + Value.empty()), + "[\"string\",42,3.14,true,false,null]"), + Arguments.of( Value.of( - Value.of("outer"), - Value.of(Value.of("inner1"), Value.of("inner2")), - Value.of(42L)) - .asString()) - .isEqualTo("[\"outer\",[\"inner1\",\"inner2\"],42]"); + Value.of("outer"), Value.of(Value.of("inner1"), Value.of("inner2")), Value.of(42L)), + "[\"outer\",[\"inner1\",\"inner2\"],42]"), + Arguments.of( + Value.of(Value.of(Value.of(Value.of(Value.of(Value.of("deep"))))), Value.of("shallow")), + "[[[[[\"deep\"]]]],\"shallow\"]")); } - @Test - void valueArray_deeplyNested() { - assertThat( - Value.of(Value.of(Value.of(Value.of(Value.of(Value.of("deep"))))), Value.of("shallow")) - .asString()) - .isEqualTo("[[[[[\"deep\"]]]],\"shallow\"]"); - } - - @Test + @ParameterizedTest + @MethodSource("keyValueListValueProvider") @SuppressWarnings("ExplicitArrayForVarargs") - void valueKeyValueList_empty() { - assertThat(Value.of(new KeyValue[] {}).asString()).isEqualTo("{}"); + void valueKeyValueList(Value input, String expectedJson) { + assertThat(input.asString()).isEqualTo(expectedJson); } - @Test - void valueKeyValueList_singleEntry() { - assertThat(Value.of(KeyValue.of("key", Value.of("value"))).asString()) - .isEqualTo("{\"key\":\"value\"}"); - } + @SuppressWarnings("ExplicitArrayForVarargs") + private static Stream keyValueListValueProvider() { + Map> map = new LinkedHashMap<>(); + map.put("key1", Value.of("value1")); + map.put("key2", Value.of(42L)); - @Test - void valueKeyValueList_multipleEntries() { - assertThat( + return Stream.of( + Arguments.of(Value.of(new KeyValue[] {}), "{}"), + Arguments.of(Value.of(KeyValue.of("key", Value.of("value"))), "{\"key\":\"value\"}"), + Arguments.of( Value.of( - KeyValue.of("name", Value.of("Alice")), - KeyValue.of("age", Value.of(30L)), - KeyValue.of("active", Value.of(true))) - .asString()) - .isEqualTo("{\"name\":\"Alice\",\"age\":30,\"active\":true}"); - } - - @Test - void valueKeyValueList_nestedMap() { - assertThat( + KeyValue.of("name", Value.of("Alice")), + KeyValue.of("age", Value.of(30L)), + KeyValue.of("active", Value.of(true))), + "{\"name\":\"Alice\",\"age\":30,\"active\":true}"), + Arguments.of( 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"))))) - .asString()) - .isEqualTo("{\"outer\":\"value\",\"inner\":{\"nested1\":\"a\",\"nested2\":\"b\"}}"); - } - - @Test - void valueKeyValueList_withArray() { - assertThat( + KeyValue.of("outer", Value.of("value")), + KeyValue.of( + "inner", + Value.of( + KeyValue.of("nested1", Value.of("a")), + KeyValue.of("nested2", Value.of("b"))))), + "{\"outer\":\"value\",\"inner\":{\"nested1\":\"a\",\"nested2\":\"b\"}}"), + Arguments.of( Value.of( - KeyValue.of("name", Value.of("test")), - KeyValue.of("items", Value.of(Value.of(1L), Value.of(2L), Value.of(3L)))) - .asString()) - .isEqualTo("{\"name\":\"test\",\"items\":[1,2,3]}"); - } - - @Test - void valueKeyValueList_allTypes() { - assertThat( + KeyValue.of("name", Value.of("test")), + KeyValue.of("items", Value.of(Value.of(1L), Value.of(2L), Value.of(3L)))), + "{\"name\":\"test\",\"items\":[1,2,3]}"), + Arguments.of( 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")))) - .asString()) - .isEqualTo( + 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")))), "{\"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).asString()).isEqualTo("{\"key1\":\"value1\",\"key2\":42}"); - } - - @Test - void valueKeyValueList_keyWithSpecialCharacters() { - assertThat( + + "\"empty\":null,\"bytes\":\"AQI=\",\"array\":[\"a\",\"b\"]}"), + Arguments.of(Value.of(map), "{\"key1\":\"value1\",\"key2\":42}"), + Arguments.of( 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"))) - .asString()) - .isEqualTo( + KeyValue.of("key with spaces", Value.of("value1")), + KeyValue.of("key\"with\"quotes", Value.of("value2")), + KeyValue.of("key\nwith\nnewlines", Value.of("value3"))), "{\"key with spaces\":\"value1\"," + "\"key\\\"with\\\"quotes\":\"value2\"," - + "\"key\\nwith\\nnewlines\":\"value3\"}"); + + "\"key\\nwith\\nnewlines\":\"value3\"}")); } @Test @@ -319,49 +219,33 @@ void complexNestedStructure() { + "\"tags\":[\"important\",\"reviewed\",\"final\"]}}"); } - @Test - void edgeCase_emptyStringKey() { - assertThat(Value.of(KeyValue.of("", Value.of("value"))).asString()) - .isEqualTo("{\"\":\"value\"}"); - } - - @Test - void edgeCase_multipleEmptyValues() { - assertThat(Value.of(Value.empty(), Value.empty(), Value.empty()).asString()) - .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")))) - .asString()) - .isEqualTo( - "[{\"id\":1,\"name\":\"A\"},{\"id\":2,\"name\":\"B\"},{\"id\":3,\"name\":\"C\"}]"); - } - - @Test + @ParameterizedTest + @MethodSource("edgeCaseProvider") @SuppressWarnings("ExplicitArrayForVarargs") - void edgeCase_mapWithEmptyArray() { - assertThat( - Value.of( - KeyValue.of("data", Value.of("test")), - KeyValue.of("items", Value.of(new Value[] {}))) - .asString()) - .isEqualTo("{\"data\":\"test\",\"items\":[]}"); + void edgeCases(Value input, String expectedJson) { + assertThat(input.asString()).isEqualTo(expectedJson); } - @Test @SuppressWarnings("ExplicitArrayForVarargs") - void edgeCase_mapWithEmptyMap() { - assertThat( + private static Stream edgeCaseProvider() { + return Stream.of( + Arguments.of(Value.of(KeyValue.of("", Value.of("value"))), "{\"\":\"value\"}"), + Arguments.of(Value.of(Value.empty(), Value.empty(), Value.empty()), "[null,null,null]"), + Arguments.of( + 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")))), + "[{\"id\":1,\"name\":\"A\"},{\"id\":2,\"name\":\"B\"},{\"id\":3,\"name\":\"C\"}]"), + Arguments.of( + Value.of( + KeyValue.of("data", Value.of("test")), + KeyValue.of("items", Value.of(new Value[] {}))), + "{\"data\":\"test\",\"items\":[]}"), + Arguments.of( Value.of( - KeyValue.of("data", Value.of("test")), - KeyValue.of("metadata", Value.of(new KeyValue[] {}))) - .asString()) - .isEqualTo("{\"data\":\"test\",\"metadata\":{}}"); + KeyValue.of("data", Value.of("test")), + KeyValue.of("metadata", Value.of(new KeyValue[] {}))), + "{\"data\":\"test\",\"metadata\":{}}")); } } From 7604d83b78a3e17660b893d1b63a1e6fe0391076 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 19 Jan 2026 20:04:56 -0800 Subject: [PATCH 13/22] Revert "deprecate extended attributes incubating api" This reverts commit c133eea1e6190bef3b105f776016c4dd4e140150. --- .../api/incubator/common/ExtendedAttributeKey.java | 4 ---- .../api/incubator/common/ExtendedAttributeType.java | 5 ----- .../api/incubator/common/ExtendedAttributes.java | 7 ------- .../incubator/common/ExtendedAttributesBuilder.java | 11 +---------- 4 files changed, 1 insertion(+), 26 deletions(-) diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java index 8d03083449c..4f48dbd8973 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java @@ -30,11 +30,7 @@ * * * @param The type of value that can be set with the key. - * @deprecated Use {@link io.opentelemetry.api.common.AttributeKey} instead. Complex attributes are - * now supported directly in the standard API via {@link - * io.opentelemetry.api.common.AttributeKey#valueKey(String)}. */ -@Deprecated @Immutable public interface ExtendedAttributeKey { /** Returns the underlying String representation of the key. */ diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java index 614b0e54d81..26e655e9ab2 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java @@ -10,12 +10,7 @@ * hence the types of values that are allowed for {@link ExtendedAttributes}. * *

    This is a superset of {@link io.opentelemetry.api.common.AttributeType}, - * - * @deprecated Use {@link io.opentelemetry.api.common.AttributeType} instead. Complex attributes are - * now supported directly in the standard API via {@link - * io.opentelemetry.api.common.AttributeType#VALUE}. */ -@Deprecated public enum ExtendedAttributeType { // Types copied AttributeType STRING, diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java index 44d10921f42..14a16778d3d 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java @@ -47,14 +47,7 @@ * {@link ExtendedAttributes} *

  • {@link #get(AttributeKey)} supports reading values using standard {@link AttributeKey} * - * - * @deprecated Use {@link io.opentelemetry.api.common.Attributes} instead. Complex attributes are - * now supported directly in the standard API via {@link - * io.opentelemetry.api.common.AttributeKey#valueKey(String)} and {@link - * io.opentelemetry.api.common.AttributesBuilder#put(io.opentelemetry.api.common.AttributeKey, - * Object)}. */ -@Deprecated @Immutable public interface ExtendedAttributes { diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java index bc45736efe7..0f4b9c942e2 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java @@ -22,16 +22,7 @@ import java.util.List; import java.util.function.Predicate; -/** - * A builder of {@link ExtendedAttributes} supporting an arbitrary number of key-value pairs. - * - * @deprecated Use {@link io.opentelemetry.api.common.AttributesBuilder} instead. Complex attributes - * are now supported directly in the standard API via {@link - * io.opentelemetry.api.common.AttributeKey#valueKey(String)} and {@link - * io.opentelemetry.api.common.AttributesBuilder#put(io.opentelemetry.api.common.AttributeKey, - * Object)}. - */ -@Deprecated +/** A builder of {@link ExtendedAttributes} supporting an arbitrary number of key-value pairs. */ public interface ExtendedAttributesBuilder { /** Create the {@link ExtendedAttributes} from this. */ ExtendedAttributes build(); From fd5fa9ddf4f1a74698ae23baca6adc133f3ff94a Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 19 Jan 2026 20:20:43 -0800 Subject: [PATCH 14/22] fixup! Use asString instead of toProtoJson --- .../exporter/logging/LoggingSpanExporterTest.java | 6 +++--- .../exporter/logging/SystemOutLogRecordExporterTest.java | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) 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 f849b97c570..da201e6e62e 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 @@ -115,9 +115,9 @@ void export() { .isEqualTo( "'testSpan1' : 12345678876543211234567887654321 8765432112345678 " + "INTERNAL [tracer: tracer1:] " - + "{animal=\"cat\", bytes=ValueBytes{AQID}, empty=ValueEmpty{}, " - + "heterogeneousArray=ValueArray{[string, 123]}, lives=9, " - + "map=KeyValueList{[nested=value]}}"); + + "{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 b9003c9cc34..c1469d76f9c 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 @@ -47,10 +47,10 @@ void format() { SystemOutLogRecordExporter.formatLog(output, log); assertThat(output.toString()) .isEqualTo( - "1970-08-07T10:00:00Z ERROR3 'message' : 00000000000000010000000000000002 0000000000000003 " - + "[scopeInfo: logTest:1.0] {amount=1, bytes=ValueBytes{AQID}, cheese=\"cheddar\", " - + "empty=ValueEmpty{}, heterogeneousArray=ValueArray{[string, 123]}, " - + "map=KeyValueList{[nested=value]}}"); + "1970-08-07T10:00:00Z ERROR3 '\"message\"' : 00000000000000010000000000000002 0000000000000003 " + + "[scopeInfo: logTest:1.0] {amount=1, bytes=ValueBytes{\"AQID\"}, cheese=\"cheddar\", " + + "empty=ValueEmpty{}, heterogeneousArray=ValueArray{[\"string\",123]}, " + + "map=KeyValueList{{\"nested\":\"value\"}}}"); } @Test From facd585970093425399d982fc08860174e02f4c1 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 19 Jan 2026 20:40:42 -0800 Subject: [PATCH 15/22] fixup! Use asString instead of toProtoJson --- .../opentelemetry/sdk/logs/ValueBodyTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/ValueBodyTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/ValueBodyTest.java index 954ba6bdb63..5f2ad1bc90c 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/ValueBodyTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/ValueBodyTest.java @@ -132,15 +132,15 @@ void valueBody() { })))); assertThat(body.asString()) .isEqualTo( - "[" - + "str_key=value, " - + "bool_key=true, " - + "long_key=1, " - + "double_key=1.1, " - + "bytes_key=Ynl0ZXM=, " - + "arr_key=[entry1, 2, 3.3], " - + "key_value_list_key=[child_str_key1=child_value1, child_str_key2=child_value2]" - + "]"); + "{" + + "\"str_key\":\"value\"," + + "\"bool_key\":true," + + "\"long_key\":1," + + "\"double_key\":1.1," + + "\"bytes_key\":\"Ynl0ZXM=\"," + + "\"arr_key\":[\"entry1\",2,3.3]," + + "\"key_value_list_key\":{\"child_str_key1\":\"child_value1\",\"child_str_key2\":\"child_value2\"}" + + "}"); }); }); exporter.reset(); From 2a8fd461105bc11a7dc1bd8d18eb299444dedca9 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Wed, 21 Jan 2026 12:39:23 -0800 Subject: [PATCH 16/22] Update to latest spec PR --- .../io/opentelemetry/api/common/Value.java | 34 +++++++++++-------- .../opentelemetry/api/common/ValueBytes.java | 6 ++-- .../opentelemetry/api/common/ValueEmpty.java | 2 +- .../opentelemetry/api/common/ValueString.java | 4 +-- .../api/common/ValueToProtoJsonTest.java | 20 +++++------ .../io/opentelemetry/api/logs/ValueTest.java | 9 ++--- .../logging/LoggingSpanExporterTest.java | 2 +- .../SystemOutLogRecordExporterTest.java | 4 +-- .../Otel2PrometheusConverterTest.java | 4 +-- .../zipkin/EventDataToAnnotation.java | 26 +++++++++++++- .../OtelToZipkinSpanTransformerTest.java | 4 +-- 11 files changed, 70 insertions(+), 45 deletions(-) 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 ddb7f04fa20..7ac3a34c01f 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 @@ -113,26 +113,32 @@ static Value empty() { T getValue(); /** - * Returns a JSON encoding of this {@link Value}. + * Returns a string representation of this {@link Value}. * - *

    The output follows the ProtoJSON - * specification: + *

    The output follows the + * string representation guidance for complex attribute value types: * *

      - *
    • {@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#STRING} String as-is without surrounding quotes. Examples: {@code hello + * world}, (empty string) + *
    • {@link ValueType#BOOLEAN} JSON boolean. Examples: {@code true}, {@code false} + *
    • {@link ValueType#LONG} JSON number. Examples: {@code 42}, {@code -123} *
    • {@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) + * "-Infinity"} for special values. Examples: {@code 3.14159}, {@code 1.23e10}, {@code + * "NaN"}, {@code "-Infinity"} + *
    • {@link ValueType#ARRAY} JSON array. Nested strings and byte arrays are encoded as JSON + * strings (with surrounding quotes). Nested empty values are encoded as JSON {@code null}. + * Examples: {@code []}, {@code [1, "a", true, {"nested": "aGVsbG8gd29ybGQ="}]} + *
    • {@link ValueType#KEY_VALUE_LIST} JSON object. Nested strings and byte arrays are encoded + * as JSON strings (with surrounding quotes). Nested empty values are encoded as JSON {@code + * null}. Examples: {@code {}}, {@code {"a": "1", "b": 2, "c": [3, null]}} + *
    • {@link ValueType#BYTES} Base64-encoded bytes without surrounding quotes. Example: {@code + * aGVsbG8gd29ybGQ=} + *
    • {@link ValueType#EMPTY} The empty string. *
    * - * @return a JSON encoding of this value + * @return a string representation of this value */ String 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 b06534e48d2..24d7df49c56 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 @@ -7,6 +7,7 @@ import java.nio.ByteBuffer; import java.util.Arrays; +import java.util.Base64; import java.util.Objects; final class ValueBytes implements Value { @@ -34,9 +35,8 @@ public ByteBuffer getValue() { @Override public String asString() { - StringBuilder sb = new StringBuilder(); - ProtoJson.append(sb, this); - return sb.toString(); + byte[] bytes = raw; + return Base64.getEncoder().encodeToString(bytes); } @Override 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 index 06a346a1e2b..47dc99ccd06 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/ValueEmpty.java @@ -27,7 +27,7 @@ public Empty getValue() { @Override public String asString() { - return "null"; + return ""; } @Override 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 0d338b7bf93..726cb27dee3 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 @@ -32,9 +32,7 @@ public String getValue() { @Override public String asString() { - StringBuilder sb = new StringBuilder(); - ProtoJson.append(sb, this); - return sb.toString(); + return value; } @Override 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 index 565d4f2e3bc..829336be7ab 100644 --- a/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java @@ -26,13 +26,13 @@ void valueString(String input, String expectedJson) { private static Stream stringValueProvider() { return Stream.of( - Arguments.of("hello", "\"hello\""), - Arguments.of("", "\"\""), - Arguments.of("line1\nline2\ttab", "\"line1\\nline2\\ttab\""), - Arguments.of("say \"hello\"", "\"say \\\"hello\\\"\""), - Arguments.of("path\\to\\file", "\"path\\\\to\\\\file\""), - Arguments.of("\u0000\u0001\u001F", "\"\\u0000\\u0001\\u001f\""), - Arguments.of("Hello δΈ–η•Œ 🌍", "\"Hello δΈ–η•Œ 🌍\"")); + Arguments.of("hello", "hello"), + Arguments.of("", ""), + Arguments.of("line1\nline2\ttab", "line1\nline2\ttab"), + Arguments.of("say \"hello\"", "say \"hello\""), + Arguments.of("path\\to\\file", "path\\to\\file"), + Arguments.of("\u0000\u0001\u001F", "\u0000\u0001\u001F"), + Arguments.of("Hello δΈ–η•Œ 🌍", "Hello δΈ–η•Œ 🌍")); } @ParameterizedTest @@ -88,13 +88,13 @@ void valueBytes(byte[] input, String expectedJson) { private static Stream bytesValueProvider() { byte[] regularBytes = new byte[] {0, 1, 2, Byte.MAX_VALUE, Byte.MIN_VALUE}; return Stream.of( - Arguments.of(new byte[] {}, "\"\""), - Arguments.of(regularBytes, '"' + Base64.getEncoder().encodeToString(regularBytes) + '"')); + Arguments.of(new byte[] {}, ""), + Arguments.of(regularBytes, Base64.getEncoder().encodeToString(regularBytes))); } @Test void valueEmpty() { - assertThat(Value.empty().asString()).isEqualTo("null"); + assertThat(Value.empty().asString()).isEqualTo(""); } @ParameterizedTest diff --git a/api/all/src/test/java/io/opentelemetry/api/logs/ValueTest.java b/api/all/src/test/java/io/opentelemetry/api/logs/ValueTest.java index 4fef6fc6fa1..0f449c2ecb0 100644 --- a/api/all/src/test/java/io/opentelemetry/api/logs/ValueTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/logs/ValueTest.java @@ -172,7 +172,7 @@ void asString(Value value, String expectedAsString) { private static Stream asStringArgs() { return Stream.of( // primitives - arguments(Value.of("str"), "\"str\""), + arguments(Value.of("str"), "str"), arguments(Value.of(true), "true"), arguments(Value.of(1), "1"), arguments(Value.of(1.1), "1.1"), @@ -201,8 +201,7 @@ private static Stream asStringArgs() { "child", Value.of(Collections.singletonMap("grandchild", Value.of("str"))))), "{\"child\":{\"grandchild\":\"str\"}}"), // bytes - arguments( - Value.of("hello world".getBytes(StandardCharsets.UTF_8)), "\"aGVsbG8gd29ybGQ=\"")); + arguments(Value.of("hello world".getBytes(StandardCharsets.UTF_8)), "aGVsbG8gd29ybGQ=")); } @Test @@ -210,9 +209,7 @@ void valueByteAsString() { // TODO: add more test cases String str = "hello world"; String base64Encoded = Value.of(str.getBytes(StandardCharsets.UTF_8)).asString(); - // Remove surrounding quotes from JSON string - String base64Value = base64Encoded.substring(1, base64Encoded.length() - 1); - byte[] decodedBytes = Base64.getDecoder().decode(base64Value); + byte[] decodedBytes = Base64.getDecoder().decode(base64Encoded); assertThat(new String(decodedBytes, StandardCharsets.UTF_8)).isEqualTo(str); } } 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 da201e6e62e..0a4c504f39e 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 @@ -115,7 +115,7 @@ void export() { .isEqualTo( "'testSpan1' : 12345678876543211234567887654321 8765432112345678 " + "INTERNAL [tracer: tracer1:] " - + "{animal=\"cat\", bytes=ValueBytes{\"AQID\"}, empty=ValueEmpty{}, " + + "{animal=\"cat\", bytes=ValueBytes{AQID}, empty=ValueEmpty{}, " + "heterogeneousArray=ValueArray{[\"string\",123]}, lives=9, " + "map=KeyValueList{{\"nested\":\"value\"}}}"); assertThat(logs.getEvents().get(1).getMessage()) 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 c1469d76f9c..74c12d2967a 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 @@ -47,8 +47,8 @@ void format() { SystemOutLogRecordExporter.formatLog(output, log); assertThat(output.toString()) .isEqualTo( - "1970-08-07T10:00:00Z ERROR3 '\"message\"' : 00000000000000010000000000000002 0000000000000003 " - + "[scopeInfo: logTest:1.0] {amount=1, bytes=ValueBytes{\"AQID\"}, cheese=\"cheddar\", " + "1970-08-07T10:00:00Z ERROR3 'message' : 00000000000000010000000000000002 0000000000000003 " + + "[scopeInfo: logTest:1.0] {amount=1, bytes=ValueBytes{AQID}, cheese=\"cheddar\", " + "empty=ValueEmpty{}, heterogeneousArray=ValueArray{[\"string\",123]}, " + "map=KeyValueList{{\"nested\":\"value\"}}}"); } 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 3c968e97761..48c0ce751bd 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 @@ -352,14 +352,14 @@ private static Stream labelValueSerializationArgs() { 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(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")); + Arguments.of(Attributes.of(valueKey("key"), Value.empty()), "")); } 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 00474dcd48e..855488a8b52 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 @@ -9,6 +9,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.common.ValueType; import io.opentelemetry.sdk.trace.data.EventData; import java.util.List; @@ -45,8 +46,31 @@ private static String toValue(Object o) { .stream().map(EventDataToAnnotation::toValue).collect(joining(",", "[", "]")); } if (o instanceof Value) { - return ((Value) o).asString(); + return toJsonValue((Value) o); } return String.valueOf(o); } + + // note: simple types (STRING, BOOLEAN, LONG, DOUBLE) won't actually come here + // but handling here for completeness + private static String toJsonValue(Value value) { + ValueType type = value.getType(); + switch (type) { + case STRING: + case BYTES: + // For JSON encoding, strings and bytes need to be quoted + return "\"" + value.asString() + "\""; + case EMPTY: + // For JSON encoding, empty values should be null + return "null"; + case ARRAY: + case KEY_VALUE_LIST: + case BOOLEAN: + case LONG: + case DOUBLE: + // Arrays, maps, and primitives are already valid JSON from asString() + return value.asString(); + } + throw new IllegalStateException("Unknown value type: " + type); + } } 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 36f696f0c17..e81e3a3c0e1 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 @@ -390,10 +390,10 @@ void generateSpan_WithAttributes() { .putTag("stringArray", "Hello") .putTag("doubleArray", "32.33,-98.3") .putTag("longArray", "33,999") - .putTag("bytes", "\"AQID\"") + .putTag("bytes", "AQID") .putTag("map", "{\"nested\":\"value\"}") .putTag("heterogeneousArray", "[\"string\",123]") - .putTag("empty", "null") + .putTag("empty", "") .putTag(OtelToZipkinSpanTransformer.OTEL_STATUS_CODE, "OK") .putTag(OtelToZipkinSpanTransformer.OTEL_DROPPED_ATTRIBUTES_COUNT, "20") .putTag(OtelToZipkinSpanTransformer.OTEL_DROPPED_EVENTS_COUNT, "1") From 83dd60046acfbc52c942fcdd58acd977c5df8e02 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Wed, 21 Jan 2026 18:41:37 -0800 Subject: [PATCH 17/22] jApiCmp --- docs/apidiffs/current_vs_latest/opentelemetry-api.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index ce251801e76..997ca653b87 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -9,11 +9,17 @@ Comparing source compatibility of opentelemetry-api-1.59.0-SNAPSHOT.jar against *** 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 ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.common.Empty (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) boolean equals(java.lang.Object) + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.common.Empty getInstance() + +++ NEW METHOD: PUBLIC(+) int hashCode() + +++ NEW METHOD: PUBLIC(+) java.lang.String toString() *** 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() + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.common.Value empty() *** 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 From 04c4a6f1fd69b58462a05b82fe75004f794f4bef Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 22 Jan 2026 07:51:07 -0800 Subject: [PATCH 18/22] Fix Nan, Infinity, -Infinity --- .../java/io/opentelemetry/api/common/ValueDouble.java | 9 ++++++--- .../opentelemetry/api/common/ValueToProtoJsonTest.java | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) 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 411ce58f18f..022a895b850 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 @@ -31,9 +31,12 @@ public Double getValue() { @Override public String asString() { - StringBuilder sb = new StringBuilder(); - ProtoJson.append(sb, this); - return sb.toString(); + if (Double.isNaN(value)) { + return "NaN"; + } else if (Double.isInfinite(value)) { + return value > 0 ? "Infinity" : "-Infinity"; + } + return String.valueOf(value); } @Override 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 index 829336be7ab..a6366a74b92 100644 --- a/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java @@ -72,9 +72,9 @@ private static Stream doubleValueProvider() { Arguments.of(-2.5, "-2.5"), Arguments.of(0.0, "0.0"), Arguments.of(-0.0, "-0.0"), - Arguments.of(Double.NaN, "\"NaN\""), - Arguments.of(Double.POSITIVE_INFINITY, "\"Infinity\""), - Arguments.of(Double.NEGATIVE_INFINITY, "\"-Infinity\""), + Arguments.of(Double.NaN, "NaN"), + Arguments.of(Double.POSITIVE_INFINITY, "Infinity"), + Arguments.of(Double.NEGATIVE_INFINITY, "-Infinity"), Arguments.of(1.23e10, "1.23E10"), Arguments.of(1.23e-10, "1.23E-10")); } @@ -213,7 +213,7 @@ void complexNestedStructure() { assertThat(complexValue.asString()) .isEqualTo( "{\"user\":\"Alice\"," - + "\"scores\":[95,87.5,92,\"NaN\",\"Infinity\"]," + + "\"scores\":[95,87.5,92,NaN,Infinity]," + "\"passed\":true," + "\"metadata\":{\"timestamp\":1234567890," + "\"tags\":[\"important\",\"reviewed\",\"final\"]}}"); From a98199a49088ec8b2f5ec323704abb543490acf6 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 22 Jan 2026 07:53:16 -0800 Subject: [PATCH 19/22] update javadoc --- .../io/opentelemetry/api/common/Value.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) 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 7ac3a34c01f..7ed25f13de6 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 @@ -124,15 +124,19 @@ static Value empty() { * world}, (empty string) *
  • {@link ValueType#BOOLEAN} JSON boolean. Examples: {@code true}, {@code false} *
  • {@link ValueType#LONG} JSON number. Examples: {@code 42}, {@code -123} - *
  • {@link ValueType#DOUBLE} JSON number, or {@code "NaN"}, {@code "Infinity"}, {@code - * "-Infinity"} for special values. Examples: {@code 3.14159}, {@code 1.23e10}, {@code - * "NaN"}, {@code "-Infinity"} - *
  • {@link ValueType#ARRAY} JSON array. Nested strings and byte arrays are encoded as JSON - * strings (with surrounding quotes). Nested empty values are encoded as JSON {@code null}. - * Examples: {@code []}, {@code [1, "a", true, {"nested": "aGVsbG8gd29ybGQ="}]} - *
  • {@link ValueType#KEY_VALUE_LIST} JSON object. Nested strings and byte arrays are encoded - * as JSON strings (with surrounding quotes). Nested empty values are encoded as JSON {@code - * null}. Examples: {@code {}}, {@code {"a": "1", "b": 2, "c": [3, null]}} + *
  • {@link ValueType#DOUBLE} JSON number, or {@code NaN}, {@code Infinity}, {@code -Infinity} + * for special values (without surrounding quotes). Examples: {@code 3.14159}, {@code + * 1.23e10}, {@code NaN}, {@code -Infinity} + *
  • {@link ValueType#ARRAY} JSON array. Nested byte arrays are encoded as Base64-encoded JSON + * strings. Nested empty values are encoded as JSON {@code null}. The special floating point + * values NaN and Infinity are encoded as JSON strings {@code "NaN"}, {@code "Infinity"}, + * and {@code "-Infinity"}. Examples: {@code []}, {@code [1, "-Infinity", "a", true, + * {"nested": "aGVsbG8gd29ybGQ="}]} + *
  • {@link ValueType#KEY_VALUE_LIST} JSON object. Nested byte arrays are encoded as + * Base64-encoded JSON strings. Nested empty values are encoded as JSON {@code null}. The + * special floating point values NaN and Infinity are encoded as JSON strings {@code "NaN"}, + * {@code "Infinity"}, and {@code "-Infinity"}. Examples: {@code {}}, {@code {"a": + * "-Infinity", "b": 2, "c": [3, null]}} *
  • {@link ValueType#BYTES} Base64-encoded bytes without surrounding quotes. Example: {@code * aGVsbG8gd29ybGQ=} *
  • {@link ValueType#EMPTY} The empty string. From d31abc4e1ae20efd3c9d97a56e6bae23347ad168 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 22 Jan 2026 10:05:30 -0800 Subject: [PATCH 20/22] fixup! Fix Nan, Infinity, -Infinity --- .../java/io/opentelemetry/api/common/ValueToProtoJsonTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a6366a74b92..ba28d5f1c35 100644 --- a/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java @@ -213,7 +213,7 @@ void complexNestedStructure() { assertThat(complexValue.asString()) .isEqualTo( "{\"user\":\"Alice\"," - + "\"scores\":[95,87.5,92,NaN,Infinity]," + + "\"scores\":[95,87.5,92,\"NaN\",\"Infinity\"]," + "\"passed\":true," + "\"metadata\":{\"timestamp\":1234567890," + "\"tags\":[\"important\",\"reviewed\",\"final\"]}}"); From cdd9ada3eadd28923725b988ededd5ead3529fdc Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 26 Jan 2026 19:05:09 -0800 Subject: [PATCH 21/22] rename ProtoJson to JsonEncoding --- .../api/common/{ProtoJson.java => JsonEncoding.java} | 4 ++-- .../main/java/io/opentelemetry/api/common/KeyValueList.java | 2 +- .../src/main/java/io/opentelemetry/api/common/ValueArray.java | 2 +- .../main/java/io/opentelemetry/api/common/ValueBoolean.java | 2 +- .../src/main/java/io/opentelemetry/api/common/ValueLong.java | 2 +- .../{ValueToProtoJsonTest.java => ValueAsStringTest.java} | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) rename api/all/src/main/java/io/opentelemetry/api/common/{ProtoJson.java => JsonEncoding.java} (98%) rename api/all/src/test/java/io/opentelemetry/api/common/{ValueToProtoJsonTest.java => ValueAsStringTest.java} (99%) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ProtoJson.java b/api/all/src/main/java/io/opentelemetry/api/common/JsonEncoding.java similarity index 98% rename from api/all/src/main/java/io/opentelemetry/api/common/ProtoJson.java rename to api/all/src/main/java/io/opentelemetry/api/common/JsonEncoding.java index b757d955b63..7f19f9a0428 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/ProtoJson.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/JsonEncoding.java @@ -9,7 +9,7 @@ import java.util.Base64; import java.util.List; -final class ProtoJson { +final class JsonEncoding { private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' @@ -128,5 +128,5 @@ private static void appendMap(StringBuilder sb, List values) { sb.append('}'); } - private ProtoJson() {} + private JsonEncoding() {} } 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 e74a6cebe01..7720d24ac44 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 @@ -49,7 +49,7 @@ public List getValue() { @Override public String asString() { StringBuilder sb = new StringBuilder(); - ProtoJson.append(sb, this); + JsonEncoding.append(sb, this); return sb.toString(); } 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 424154a36e2..64bdeb23a30 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 @@ -43,7 +43,7 @@ public List> getValue() { @Override public String asString() { StringBuilder sb = new StringBuilder(); - ProtoJson.append(sb, this); + JsonEncoding.append(sb, this); return sb.toString(); } 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 bfbcc53f663..98caa7a27f4 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 @@ -32,7 +32,7 @@ public Boolean getValue() { @Override public String asString() { StringBuilder sb = new StringBuilder(); - ProtoJson.append(sb, this); + JsonEncoding.append(sb, this); return sb.toString(); } 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 20c4b99c85a..6d99a6c4749 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 @@ -32,7 +32,7 @@ public Long getValue() { @Override public String asString() { StringBuilder sb = new StringBuilder(); - ProtoJson.append(sb, this); + JsonEncoding.append(sb, this); return sb.toString(); } diff --git a/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java b/api/all/src/test/java/io/opentelemetry/api/common/ValueAsStringTest.java similarity index 99% rename from api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java rename to api/all/src/test/java/io/opentelemetry/api/common/ValueAsStringTest.java index ba28d5f1c35..a5919d2269c 100644 --- a/api/all/src/test/java/io/opentelemetry/api/common/ValueToProtoJsonTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/common/ValueAsStringTest.java @@ -16,7 +16,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -class ValueToProtoJsonTest { +class ValueAsStringTest { @ParameterizedTest @MethodSource("stringValueProvider") From 99bb2f1388568cb51ed11c7dec6ccf39ed87e78c Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 26 Jan 2026 19:30:15 -0800 Subject: [PATCH 22/22] remove ByteBuffer duplicate --- .../src/main/java/io/opentelemetry/api/common/JsonEncoding.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/JsonEncoding.java b/api/all/src/main/java/io/opentelemetry/api/common/JsonEncoding.java index 7f19f9a0428..49fe90a6e1a 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/JsonEncoding.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/JsonEncoding.java @@ -99,7 +99,7 @@ private static void appendDouble(StringBuilder sb, double value) { private static void appendBytes(StringBuilder sb, ByteBuffer value) { byte[] bytes = new byte[value.remaining()]; - value.duplicate().get(bytes); + value.get(bytes); sb.append('"').append(Base64.getEncoder().encodeToString(bytes)).append('"'); }