From 5468dc5195c36eea4a73bb730005ade36238778e Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 26 Jan 2026 13:15:27 -0800 Subject: [PATCH 1/4] test --- .../internal/otlp/ValueMarshalerTest.java | 18 +++++++++++++- .../traces/TraceRequestMarshalerTest.java | 24 ++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/ValueMarshalerTest.java b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/ValueMarshalerTest.java index d9e38900b7e..66e1b36f178 100644 --- a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/ValueMarshalerTest.java +++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/ValueMarshalerTest.java @@ -123,7 +123,23 @@ private static Stream serializeAnyValueArgs() { of("hello world".getBytes(StandardCharsets.UTF_8)), AnyValue.newBuilder() .setBytesValue(ByteString.copyFrom("hello world".getBytes(StandardCharsets.UTF_8))) - .build())); + .build()), + // empty values + arguments(of(""), AnyValue.newBuilder().setStringValue("").build()), + arguments(of(false), AnyValue.newBuilder().setBoolValue(false).build()), + arguments(of(0), AnyValue.newBuilder().setIntValue(0).build()), + arguments(of(0.0), AnyValue.newBuilder().setDoubleValue(0.0).build()), + arguments( + Value.of(Collections.emptyList()), + AnyValue.newBuilder().setArrayValue(ArrayValue.newBuilder().build()).build()), + arguments( + of(Collections.emptyMap()), + AnyValue.newBuilder().setKvlistValue(KeyValueList.newBuilder().build()).build()), + arguments(of(new byte[0]), AnyValue.newBuilder().setBytesValue(ByteString.EMPTY).build()) + // TODO add test for true empty value + // after https://github.com/open-telemetry/opentelemetry-java/pull/7973 + // arguments(Value.empty(), AnyValue.newBuilder().build()) + ); } @SuppressWarnings("unchecked") 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..b46f9b0604e 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 @@ -147,8 +147,14 @@ void toProtoSpan(MarshalerSource marshalerSource) { .put("long_array", 12L, 23L) .put("double_array", 12.3, 23.1) .put("boolean_array", true, false) + .put("empty_string", "") + .put("false_value", false) + .put("zero_int", 0L) + .put("zero_double", 0.0) + // TODO: add empty array, empty map, empty bytes, and true empty value + // after https://github.com/open-telemetry/opentelemetry-java/pull/7973 .build()) - .setTotalAttributeCount(9) + .setTotalAttributeCount(13) .setEvents( Collections.singletonList( EventData.create(12347, "my_event", Attributes.empty()))) @@ -231,6 +237,22 @@ void toProtoSpan(MarshalerSource marshalerSource) { .addValues(AnyValue.newBuilder().setBoolValue(false).build()) .build()) .build()) + .build(), + KeyValue.newBuilder() + .setKey("empty_string") + .setValue(AnyValue.newBuilder().setStringValue("").build()) + .build(), + KeyValue.newBuilder() + .setKey("false_value") + .setValue(AnyValue.newBuilder().setBoolValue(false).build()) + .build(), + KeyValue.newBuilder() + .setKey("zero_int") + .setValue(AnyValue.newBuilder().setIntValue(0).build()) + .build(), + KeyValue.newBuilder() + .setKey("zero_double") + .setValue(AnyValue.newBuilder().setDoubleValue(0.0).build()) .build()); assertThat(protoSpan.getDroppedAttributesCount()).isEqualTo(1); assertThat(protoSpan.getEventsList()) From 904396b563fc82ab116e8650eeeaced6ef1b8397 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 26 Jan 2026 13:56:57 -0800 Subject: [PATCH 2/4] fix --- .../internal/otlp/StringAnyValueMarshaler.java | 6 ------ .../otlp/StringAnyValueStatelessMarshaler.java | 10 +++++++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/StringAnyValueMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/StringAnyValueMarshaler.java index cc7bf4527c6..1b3db2da32c 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/StringAnyValueMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/StringAnyValueMarshaler.java @@ -33,16 +33,10 @@ static MarshalerWithSize create(String value) { @Override public void writeTo(Serializer output) throws IOException { - if (valueUtf8.length == 0) { - return; - } output.writeString(AnyValue.STRING_VALUE, valueUtf8); } private static int calculateSize(byte[] valueUtf8) { - if (valueUtf8.length == 0) { - return 0; - } return AnyValue.STRING_VALUE.getTagSize() + CodedOutputStream.computeByteArraySizeNoTag(valueUtf8); } diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/StringAnyValueStatelessMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/StringAnyValueStatelessMarshaler.java index 9d9af0b5d0c..b3637615964 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/StringAnyValueStatelessMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/StringAnyValueStatelessMarshaler.java @@ -5,10 +5,10 @@ package io.opentelemetry.exporter.internal.otlp; +import io.opentelemetry.exporter.internal.marshal.CodedOutputStream; import io.opentelemetry.exporter.internal.marshal.MarshalerContext; import io.opentelemetry.exporter.internal.marshal.Serializer; import io.opentelemetry.exporter.internal.marshal.StatelessMarshaler; -import io.opentelemetry.exporter.internal.marshal.StatelessMarshalerUtil; import io.opentelemetry.proto.common.v1.internal.AnyValue; import java.io.IOException; @@ -26,11 +26,15 @@ private StringAnyValueStatelessMarshaler() {} @Override public void writeTo(Serializer output, String value, MarshalerContext context) throws IOException { - output.serializeStringWithContext(AnyValue.STRING_VALUE, value, context); + output.writeString(AnyValue.STRING_VALUE, value, context.getSize(), context); } @Override public int getBinarySerializedSize(String value, MarshalerContext context) { - return StatelessMarshalerUtil.sizeStringWithContext(AnyValue.STRING_VALUE, value, context); + int utf8Size = context.getStringEncoder().getUtf8Size(value); + context.addSize(utf8Size); + return AnyValue.STRING_VALUE.getTagSize() + + CodedOutputStream.computeUInt32SizeNoTag(utf8Size) + + utf8Size; } } From 11b7188fdae3f57f5aecf5fd20d5a1abc6065248 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Tue, 27 Jan 2026 13:18:13 -0800 Subject: [PATCH 3/4] add back marshalStringNoAllocation condition --- .../exporter/internal/marshal/Serializer.java | 10 ++++++++++ .../internal/marshal/StatelessMarshalerUtil.java | 12 ++++++++++-- .../otlp/StringAnyValueStatelessMarshaler.java | 6 +++--- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/Serializer.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/Serializer.java index 8d78fbd01a8..d93a8f10fdf 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/Serializer.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/Serializer.java @@ -257,6 +257,16 @@ public void serializeStringWithContext( if (string == null || string.isEmpty()) { return; } + writeStringWithContext(field, string, context); + } + + /** + * Writes a protobuf {@code string} field, even if it matches the default value. This method reads + * elements from context, use together with {@link StatelessMarshalerUtil#getUtf8Size(String, + * MarshalerContext)}. + */ + public void writeStringWithContext(ProtoFieldInfo field, String string, MarshalerContext context) + throws IOException { if (context.marshalStringNoAllocation()) { writeString(field, string, context.getSize(), context); } else { diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/StatelessMarshalerUtil.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/StatelessMarshalerUtil.java index 715bcf9f649..55e234b2b4a 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/StatelessMarshalerUtil.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/StatelessMarshalerUtil.java @@ -93,14 +93,22 @@ public static int sizeStringWithContext( if (value == null || value.isEmpty()) { return sizeBytes(field, 0); } + return sizeBytes(field, getUtf8Size(value, context)); + } + + /** + * Returns the UTF-8 size of a string, adding data to the context for later serialization. Use + * together with {@link Serializer#writeString(ProtoFieldInfo, String, int, MarshalerContext)}. + */ + public static int getUtf8Size(String value, MarshalerContext context) { if (context.marshalStringNoAllocation()) { int utf8Size = context.getStringEncoder().getUtf8Size(value); context.addSize(utf8Size); - return sizeBytes(field, utf8Size); + return utf8Size; } else { byte[] valueUtf8 = MarshalerUtil.toBytes(value); context.addData(valueUtf8); - return sizeBytes(field, valueUtf8.length); + return valueUtf8.length; } } diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/StringAnyValueStatelessMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/StringAnyValueStatelessMarshaler.java index b3637615964..19fa98bc2e4 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/StringAnyValueStatelessMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/StringAnyValueStatelessMarshaler.java @@ -9,6 +9,7 @@ import io.opentelemetry.exporter.internal.marshal.MarshalerContext; import io.opentelemetry.exporter.internal.marshal.Serializer; import io.opentelemetry.exporter.internal.marshal.StatelessMarshaler; +import io.opentelemetry.exporter.internal.marshal.StatelessMarshalerUtil; import io.opentelemetry.proto.common.v1.internal.AnyValue; import java.io.IOException; @@ -26,13 +27,12 @@ private StringAnyValueStatelessMarshaler() {} @Override public void writeTo(Serializer output, String value, MarshalerContext context) throws IOException { - output.writeString(AnyValue.STRING_VALUE, value, context.getSize(), context); + output.writeStringWithContext(AnyValue.STRING_VALUE, value, context); } @Override public int getBinarySerializedSize(String value, MarshalerContext context) { - int utf8Size = context.getStringEncoder().getUtf8Size(value); - context.addSize(utf8Size); + int utf8Size = StatelessMarshalerUtil.getUtf8Size(value, context); return AnyValue.STRING_VALUE.getTagSize() + CodedOutputStream.computeUInt32SizeNoTag(utf8Size) + utf8Size; From 614e6629320f7a4baa2752b34efd53d3e56ca3ea Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 6 Feb 2026 18:39:25 -0800 Subject: [PATCH 4/4] add test for empty array, empty map, and empty bytes --- .../traces/TraceRequestMarshalerTest.java | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) 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 7f6046c37c6..7fd9a84400e 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 @@ -161,10 +161,11 @@ void toProtoSpan(MarshalerSource marshalerSource) { .put("false_value", false) .put("zero_int", 0L) .put("zero_double", 0.0) - // TODO: add empty array, empty map, empty bytes, and true empty value - // after https://github.com/open-telemetry/opentelemetry-java/pull/7973 + .put("empty_array", Value.of(Collections.emptyList())) + .put("empty_map", Value.of(Collections.emptyMap())) + .put("empty_bytes", Value.of(new byte[] {})) .build()) - .setTotalAttributeCount(17) + .setTotalAttributeCount(20) .setEvents( Collections.singletonList( EventData.create(12347, "my_event", Attributes.empty()))) @@ -297,6 +298,21 @@ void toProtoSpan(MarshalerSource marshalerSource) { KeyValue.newBuilder() .setKey("zero_double") .setValue(AnyValue.newBuilder().setDoubleValue(0.0).build()) + .build(), + KeyValue.newBuilder() + .setKey("empty_array") + .setValue( + AnyValue.newBuilder().setArrayValue(ArrayValue.newBuilder().build()).build()) + .build(), + KeyValue.newBuilder() + .setKey("empty_map") + .setValue( + AnyValue.newBuilder().setKvlistValue(KeyValueList.newBuilder().build()).build()) + .build(), + KeyValue.newBuilder() + .setKey("empty_bytes") + .setValue( + AnyValue.newBuilder().setBytesValue(ByteString.copyFrom(new byte[] {})).build()) .build()); assertThat(protoSpan.getDroppedAttributesCount()).isEqualTo(1); assertThat(protoSpan.getEventsList())