From a7a3213d71cfc7f95b487d2d0b6bdd6909e587cd Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Wed, 2 Sep 2026 11:36:58 +0000 Subject: [PATCH 1/4] Prototype JSON string representation for attributes --- .../api/common/ArrayBackedAttributes.java | 13 ++++--- .../api/common/JsonEncoding.java | 14 ++++++++ .../api/common/AttributesTest.java | 35 +++++++++++++++++-- .../logging/LoggingSpanExporterTest.java | 6 ++-- .../SystemOutLogRecordExporterTest.java | 7 ++-- .../DeclarativeConfigurationCreateTest.java | 12 +++---- .../sdk/logs/SdkLoggerProviderTest.java | 2 +- ...ableExponentialHistogramPointDataTest.java | 2 +- .../data/MutableHistogramPointDataTest.java | 2 +- .../view/AttributesProcessorTest.java | 4 +-- .../sdk/trace/SdkSpanBuilderTest.java | 6 ++-- 11 files changed, 77 insertions(+), 26 deletions(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java index d44022965ab..70ff76028f4 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 @@ -43,6 +43,13 @@ public AttributesBuilder toBuilder() { return new ArrayBackedAttributesBuilder(new ArrayList<>(data())); } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + JsonEncoding.appendAttributes(sb, data()); + return sb.toString(); + } + @SuppressWarnings("unchecked") // safe cast: values are stored internally keyed by AttributeKey @Override @Nullable @@ -111,8 +118,7 @@ private Value getAsValue(String keyName) { } @SuppressWarnings("unchecked") - @Nullable - private static Value asValue(AttributeType type, Object value) { + static Value asValue(AttributeType type, Object value) { switch (type) { case STRING: return Value.of((String) value); @@ -154,8 +160,7 @@ private static Value asValue(AttributeType type, Object value) { // Already a Value return (Value) value; } - // Should not reach here - return null; + throw new IllegalArgumentException("Unknown attribute type: " + type); } static Attributes sortAndFilterToAttributes(Object... data) { 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 721e8fc5021..3b75f7769d7 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 @@ -45,6 +45,20 @@ static void append(StringBuilder sb, Value value) { } } + static void appendAttributes(StringBuilder sb, List data) { + sb.append('{'); + for (int i = 0; i < data.size(); i += 2) { + if (i > 0) { + sb.append(','); + } + AttributeKey key = (AttributeKey) data.get(i); + appendString(sb, key.getKey()); + sb.append(':'); + append(sb, ArrayBackedAttributes.asValue(key.getType(), data.get(i + 1))); + } + sb.append('}'); + } + private static void appendString(StringBuilder sb, String value) { sb.append('"'); for (int i = 0; i < value.length(); i++) { 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 d44eac6da8a..c85fc162e85 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 @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -815,8 +816,38 @@ void attributesToString() { assertThat(attributes.toString()) .isEqualTo( - "{error=true, http.response_size=100, " - + "otel.status_code=\"OK\", process.cpu_consumed=33.44, success=\"true\"}"); + "{\"error\":true,\"http.response_size\":100," + + "\"otel.status_code\":\"OK\",\"process.cpu_consumed\":33.44," + + "\"success\":\"true\"}"); + } + + @Test + void attributesToStringComplexValues() { + Attributes attributes = + Attributes.builder() + .put(valueKey("bytes"), Value.of("hello world".getBytes(StandardCharsets.UTF_8))) + .put("colors", "red", "blue") + .put(valueKey("empty"), Value.empty()) + .put(doubleKey("infinity"), Double.POSITIVE_INFINITY) + .put( + valueKey("nested"), + Value.of( + KeyValue.of("array", Value.of(Value.of("red"), Value.of("blue"))), + KeyValue.of("boolean", Value.of(true)))) + .build(); + + assertThat(attributes.toString()) + .isEqualTo( + "{\"bytes\":\"aGVsbG8gd29ybGQ=\",\"colors\":[\"red\",\"blue\"]," + + "\"empty\":null,\"infinity\":\"Infinity\"," + + "\"nested\":{\"array\":[\"red\",\"blue\"],\"boolean\":true}}"); + } + + @Test + void attributesToStringEscapesKeysAndValues() { + Attributes attributes = Attributes.of(stringKey("a\"key\n"), "a \\ value\t"); + + assertThat(attributes.toString()).isEqualTo("{\"a\\\"key\\n\":\"a \\\\ value\\t\"}"); } @Test 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 25e28b41632..71443e54882 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\":\"AQID\",\"empty\":null," + + "\"heterogeneousArray\":[\"string\",123],\"lives\":9," + + "\"map\":{\"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 44f24dee00c..a37238b87ad 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 @@ -48,9 +48,10 @@ void format() { 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\"}}}"); + + "[scopeInfo: logTest:1.0] {\"amount\":1,\"bytes\":\"AQID\"," + + "\"cheese\":\"cheddar\",\"empty\":null," + + "\"heterogeneousArray\":[\"string\",123]," + + "\"map\":{\"nested\":\"value\"}}"); } @Test diff --git a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationCreateTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationCreateTest.java index d0ab0fb0476..dac609eb2f1 100644 --- a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationCreateTest.java +++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationCreateTest.java @@ -186,12 +186,12 @@ void create_ModelCustomizer() { assertThat(sdk.toString()) .contains( "resource=Resource{schemaUrl=null, attributes={" - + "color=\"blue\", " - + "foo=\"bar\", " - + "service.name=\"unknown_service:java\", " - + "telemetry.sdk.language=\"java\", " - + "telemetry.sdk.name=\"opentelemetry\", " - + "telemetry.sdk.version=\""); + + "\"color\":\"blue\"," + + "\"foo\":\"bar\"," + + "\"service.name\":\"unknown_service:java\"," + + "\"telemetry.sdk.language\":\"java\"," + + "\"telemetry.sdk.name\":\"opentelemetry\"," + + "\"telemetry.sdk.version\":\""); } @Test diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java index aa58b8869c3..48f783e59eb 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java @@ -347,7 +347,7 @@ void toString_Valid() { .isEqualTo( "SdkLoggerProvider{" + "clock=SystemClock{}, " - + "resource=Resource{schemaUrl=null, attributes={key=\"value\"}}, " + + "resource=Resource{schemaUrl=null, attributes={\"key\":\"value\"}}, " + "logLimits=LogLimits{maxNumberOfAttributes=128, maxAttributeValueLength=2147483647}, " + "logRecordProcessor=MockLogRecordProcessor, " + "loggerConfigurator=ScopeConfiguratorImpl{conditions=[]}" diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/data/MutableExponentialHistogramPointDataTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/data/MutableExponentialHistogramPointDataTest.java index ca812dc73c6..beff6a10bf8 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/data/MutableExponentialHistogramPointDataTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/data/MutableExponentialHistogramPointDataTest.java @@ -66,7 +66,7 @@ public void testSanity() { assertThat(pointData.toString()) .isEqualTo( "MutableExponentialHistogramPointData{startEpochNanos=10, epochNanos=20, " - + "attributes={foo=\"bar\"}, scale=1, sum=2.0, count=43, zeroCount=10, hasMin=true, " + + "attributes={\"foo\":\"bar\"}, scale=1, sum=2.0, count=43, zeroCount=10, hasMin=true, " + "min=100.0, hasMax=true, max=1000.0, " + "positiveBuckets=MutableExponentialHistogramBuckets{scale=1, offset=2, " + "bucketCounts=[1, 2, 3], totalCount=3}, " diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/data/MutableHistogramPointDataTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/data/MutableHistogramPointDataTest.java index 514c5c84411..53929dc0908 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/data/MutableHistogramPointDataTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/data/MutableHistogramPointDataTest.java @@ -58,7 +58,7 @@ void testSanity() { .isEqualTo( "MutableHistogramPointData{startEpochNanos=10, " + "epochNanos=20, " - + "attributes={foo=\"bar\"}, " + + "attributes={\"foo\":\"bar\"}, " + "sum=2.0, " + "count=550, " + "hasMin=true, " diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/view/AttributesProcessorTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/view/AttributesProcessorTest.java index 69dab17738d..442b3909069 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/view/AttributesProcessorTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/view/AttributesProcessorTest.java @@ -64,7 +64,7 @@ void append_toString() { AttributesProcessor.append(Attributes.builder().put("key", "value").build()); assertThat(processor.toString()) - .isEqualTo("AppendingAttributesProcessor{additionalAttributes={key=\"value\"}}"); + .isEqualTo("AppendingAttributesProcessor{additionalAttributes={\"key\":\"value\"}}"); } @Test @@ -141,7 +141,7 @@ void joinedAttributes_toString() { .isEqualTo( "JoinedAttributesProcessor{processors=[" + "BaggageAppendingAttributesProcessor{nameFilter=IncludeExcludePredicate{globMatchingEnabled=false, included=[keep]}}, " - + "AppendingAttributesProcessor{additionalAttributes={key=\"value\"}}" + + "AppendingAttributesProcessor{additionalAttributes={\"key\":\"value\"}}" + "]}"); } } 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 b8f662e2791..4312b7d3126 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 @@ -1110,9 +1110,9 @@ void spanDataToString() { + "traceFlags=00, " + "traceState=ArrayBasedTraceState\\{entries=\\[]}, remote=false, valid=false}, " + "resource=Resource\\{schemaUrl=null, " - + "attributes=\\{service.name=\"unknown_service:java\", " - + "telemetry.sdk.language=\"java\", telemetry.sdk.name=\"opentelemetry\", " - + "telemetry.sdk.version=\"\\d+.\\d+.\\d+(-rc.\\d+)?(-SNAPSHOT)?\"}}, " + + "attributes=\\{\"service.name\":\"unknown_service:java\"," + + "\"telemetry.sdk.language\":\"java\",\"telemetry.sdk.name\":\"opentelemetry\"," + + "\"telemetry.sdk.version\":\"\\d+.\\d+.\\d+(-rc.\\d+)?(-SNAPSHOT)?\"}}, " + "instrumentationScopeInfo=InstrumentationScopeInfo\\{" + "name=SpanBuilderSdkTest, version=null, schemaUrl=null, attributes=\\{}}, " + "name=span_name, " From b3b1e4ecd82aa012e5adda4678324bb2715fa237 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Wed, 2 Sep 2026 11:47:48 +0000 Subject: [PATCH 2/4] Clarify attribute conversion invariant --- .../io/opentelemetry/api/common/ArrayBackedAttributes.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 70ff76028f4..2ce5d1b359c 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 @@ -117,6 +117,8 @@ private Value getAsValue(String keyName) { return null; } + // This method must return a non-null Value because every AttributeType has a Value + // representation and callers invoke it only for an existing, non-null attribute. @SuppressWarnings("unchecked") static Value asValue(AttributeType type, Object value) { switch (type) { @@ -160,7 +162,7 @@ static Value asValue(AttributeType type, Object value) { // Already a Value return (Value) value; } - throw new IllegalArgumentException("Unknown attribute type: " + type); + throw new IllegalStateException("Unknown attribute type: " + type); } static Attributes sortAndFilterToAttributes(Object... data) { From 0da37c1b74adc77005912a9a2d4a30967509bff4 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Wed, 2 Sep 2026 11:50:04 +0000 Subject: [PATCH 3/4] Move attribute invariant comment to fallback --- .../io/opentelemetry/api/common/ArrayBackedAttributes.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java b/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java index 2ce5d1b359c..34a337a1158 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 @@ -117,8 +117,6 @@ private Value getAsValue(String keyName) { return null; } - // This method must return a non-null Value because every AttributeType has a Value - // representation and callers invoke it only for an existing, non-null attribute. @SuppressWarnings("unchecked") static Value asValue(AttributeType type, Object value) { switch (type) { @@ -162,6 +160,7 @@ static Value asValue(AttributeType type, Object value) { // Already a Value return (Value) value; } + // Every AttributeType must have a non-null Value representation. throw new IllegalStateException("Unknown attribute type: " + type); } From 4e14bc0fcfb4c84e2c02e273d9bfa2988d746995 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Wed, 2 Sep 2026 21:13:57 +0000 Subject: [PATCH 4/4] Update sampler description expectation --- .../trace/samplers/ComposableAnnotatingSamplerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAnnotatingSamplerTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAnnotatingSamplerTest.java index 6c961c2c141..95e5c745ef3 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAnnotatingSamplerTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/samplers/ComposableAnnotatingSamplerTest.java @@ -29,7 +29,7 @@ void testDescription() { assertThat( ComposableSampler.annotating(ComposableSampler.alwaysOn(), ATTRIBUTES).getDescription()) .isEqualTo( - "ComposableAnnotatingSampler{ComposableAlwaysOnSampler,{http.route=\"/bear\", size=100}}"); + "ComposableAnnotatingSampler{ComposableAlwaysOnSampler,{\"http.route\":\"/bear\",\"size\":100}}"); } @Test