From bb51f67f94ebd3df9998e9b92902f6314e9fa79c Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Tue, 28 Jul 2026 08:14:45 +0200 Subject: [PATCH 1/3] fix(json): Fix JSON writer size computation Size is now exact and do no longer need separate counting --- .../main/java/datadog/json/JsonWriter.java | 27 ++++++------------- .../java/datadog/json/JsonWriterTest.java | 7 ++--- .../otlp/trace/OtlpTraceJsonCollector.java | 4 +-- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/components/json/src/main/java/datadog/json/JsonWriter.java b/components/json/src/main/java/datadog/json/JsonWriter.java index 0420f7b7f7b..1c0d778da6d 100644 --- a/components/json/src/main/java/datadog/json/JsonWriter.java +++ b/components/json/src/main/java/datadog/json/JsonWriter.java @@ -19,7 +19,6 @@ public final class JsonWriter implements Flushable, AutoCloseable { private final JsonStructure structure; private boolean requireComma; - private int bytesWritten; /** Creates a writer with structure check. */ public JsonWriter() { @@ -246,9 +245,14 @@ public void flush() { } } - /** Approximate number of bytes written so far. */ - public int sizeInBytes() { - return this.bytesWritten; + /** + * Returns the current JSON size in bytes. + * @return The JSON size in bytes. + * @see #toByteArray() + */ + public int size() { + flush(); + return this.outputStream.size(); } @Override @@ -274,7 +278,6 @@ private void endsValue() { private void write(char ch) { try { this.writer.write(ch); - this.bytesWritten++; } catch (IOException ignored) { } } @@ -282,7 +285,6 @@ private void write(char ch) { private void writeStringLiteral(String str) { try { this.writer.write('"'); - int count = 1; for (int i = 0; i < str.length(); ++i) { char c = str.charAt(i); @@ -294,7 +296,6 @@ private void writeStringLiteral(String str) { this.writer.write(HEX_DIGITS[(c >>> 8) & 0xF]); this.writer.write(HEX_DIGITS[(c >>> 4) & 0xF]); this.writer.write(HEX_DIGITS[c & 0xF]); - count += 6; } else { switch (c) { case '"': // Quotation mark @@ -302,32 +303,26 @@ private void writeStringLiteral(String str) { case '/': // Solidus this.writer.write('\\'); this.writer.write(c); - count += 2; break; case '\b': // Backspace this.writer.write('\\'); this.writer.write('b'); - count += 2; break; case '\f': // Form feed this.writer.write('\\'); this.writer.write('f'); - count += 2; break; case '\n': // Line feed this.writer.write('\\'); this.writer.write('n'); - count += 2; break; case '\r': // Carriage return this.writer.write('\\'); this.writer.write('r'); - count += 2; break; case '\t': // Horizontal tab this.writer.write('\\'); this.writer.write('t'); - count += 2; break; default: if (c < 0x20) { @@ -337,10 +332,8 @@ private void writeStringLiteral(String str) { this.writer.write('0'); this.writer.write(HEX_DIGITS[(c >>> 4) & 0xF]); this.writer.write(HEX_DIGITS[c & 0xF]); - count += 6; } else { this.writer.write(c); - count += 1; } break; } @@ -348,9 +341,6 @@ private void writeStringLiteral(String str) { } this.writer.write('"'); - count += 1; - - this.bytesWritten += count; } catch (IOException ignored) { } } @@ -358,7 +348,6 @@ private void writeStringLiteral(String str) { private void writeStringRaw(String str) { try { this.writer.write(str); - this.bytesWritten += str.length(); // exact if ASCII, estimate otherwise } catch (IOException ignored) { } } diff --git a/components/json/src/test/java/datadog/json/JsonWriterTest.java b/components/json/src/test/java/datadog/json/JsonWriterTest.java index 68624cfa936..b6a2dc9cd1c 100644 --- a/components/json/src/test/java/datadog/json/JsonWriterTest.java +++ b/components/json/src/test/java/datadog/json/JsonWriterTest.java @@ -166,9 +166,9 @@ void testCompleteObject() { } @Test - void testSizeInBytes() { + void testSize() { try (JsonWriter writer = new JsonWriter()) { - assertEquals(0, writer.sizeInBytes(), "Check empty writer size"); + assertEquals(0, writer.size(), "Check empty writer size"); writer.beginArray(); assertSizeInBytes(writer, "Check size after plain ASCII string"); @@ -210,7 +210,8 @@ void testSizeInBytes() { } private static void assertSizeInBytes(JsonWriter writer, String message) { - assertEquals(writer.toByteArray().length, writer.sizeInBytes(), message); + int size = writer.size(); + assertEquals(writer.toByteArray().length, size, message); } @Test diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollector.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollector.java index 94fe3ed5706..588c48b68e5 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollector.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollector.java @@ -68,7 +68,7 @@ public void addTrace(List> spans) { @Override public int sizeInBytes() { - return writer == null ? 0 : writer.sizeInBytes(); + return writer == null ? 0 : writer.size(); } /** @@ -191,7 +191,7 @@ private void completeSpan() { currentSpan = null; currentSpanLinks = Collections.emptyList(); - if (writer.sizeInBytes() > MAX_CAPACITY_BYTES) { + if (writer.size() > MAX_CAPACITY_BYTES) { throw new IllegalStateException( "OTLP payload exceeds maximum buffer size of " + MAX_CAPACITY_BYTES + " bytes"); } From 123e16ffc3f07fc64c0a7889f422a45297615d22 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Tue, 28 Jul 2026 08:56:42 +0200 Subject: [PATCH 2/3] feat(json): Allow mapper to be used with writer This avoids creating intermediate writer to map collection in existing json/writter --- .../main/java/datadog/json/JsonMapper.java | 128 +++++++++++++----- .../main/java/datadog/json/JsonWriter.java | 1 + .../java/datadog/json/JsonMapperTest.java | 51 +++++++ 3 files changed, 149 insertions(+), 31 deletions(-) diff --git a/components/json/src/main/java/datadog/json/JsonMapper.java b/components/json/src/main/java/datadog/json/JsonMapper.java index 1233f7ebde6..a997d6b9d5b 100644 --- a/components/json/src/main/java/datadog/json/JsonMapper.java +++ b/components/json/src/main/java/datadog/json/JsonMapper.java @@ -41,35 +41,57 @@ public static String toJson(Map map) { return "{}"; } try (JsonWriter writer = new JsonWriter()) { + writeMap(writer, map); + return writer.toString(); + } + } + + /** + * Writes the map as JSON value to the given mapper. + * + * @param writer The writer to write the map as value to. + * @param map The map to write. + */ + public static void writeAsJsonValue(JsonWriter writer, Map map) { + if (writer == null) { + throw new NullPointerException("writer cannot be null"); + } + if (map == null) { writer.beginObject(); - for (Map.Entry entry : map.entrySet()) { - writer.name(entry.getKey()); - Object value = entry.getValue(); - if (value == null) { - writer.nullValue(); - } else if (value instanceof String) { - writer.value((String) value); - } else if (value instanceof Double) { - writer.value((Double) value); - } else if (value instanceof Float) { - writer.value((Float) value); - } else if (value instanceof Long) { - writer.value((Long) value); - } else if (value instanceof Integer) { - writer.value((Integer) value); - } else if (value instanceof Boolean) { - writer.value((Boolean) value); - } else { - writer.value(value.toString()); - } - } writer.endObject(); - return writer.toString(); + } else { + writeMap(writer, map); } } + private static void writeMap(JsonWriter writer, Map map) { + writer.beginObject(); + for (Map.Entry entry : map.entrySet()) { + writer.name(entry.getKey()); + Object value = entry.getValue(); + if (value == null) { + writer.nullValue(); + } else if (value instanceof String) { + writer.value((String) value); + } else if (value instanceof Double) { + writer.value((Double) value); + } else if (value instanceof Float) { + writer.value((Float) value); + } else if (value instanceof Long) { + writer.value((Long) value); + } else if (value instanceof Integer) { + writer.value((Integer) value); + } else if (value instanceof Boolean) { + writer.value((Boolean) value); + } else { + writer.value(value.toString()); + } + } + writer.endObject(); + } + /** - * Converts a {@code Iterable} to a JSON array. + * Converts a {@code Collection} to a JSON array. * * @param items The iterable to convert. * @return The converted JSON array as Java string. @@ -80,15 +102,37 @@ public static String toJson(Collection items) { return "[]"; } try (JsonWriter writer = new JsonWriter()) { + writeArray(items, writer); + return writer.toString(); + } + } + + /** + * Writes the {@code Collection} as a JSON array to the given writer. + * + * @param items The collection to write. + * @param writer The writer to write the collection as JSON array to. + */ + public static void writeAsJsonValue(Collection items, JsonWriter writer) { + if (writer == null) { + throw new NullPointerException("writer cannot be null"); + } + if (items == null) { writer.beginArray(); - for (String item : items) { - writer.value(item); - } writer.endArray(); - return writer.toString(); + } else { + writeArray(items, writer); } } + private static void writeArray(Iterable items, JsonWriter writer) { + writer.beginArray(); + for (String item : items) { + writer.value(item); + } + writer.endArray(); + } + /** * Converts a String array to a JSON array. * @@ -101,13 +145,35 @@ public static String toJson(String[] items) { return "[]"; } try (JsonWriter writer = new JsonWriter()) { + writeArray(items, writer); + return writer.toString(); + } + } + + /** + * Writes the String array as a JSON array to the given writer. + * + * @param items The array to write. + * @param writer The writer to write the array as JSON array to. + */ + public static void writeAsJsonValue(String[] items, JsonWriter writer) { + if (writer == null) { + throw new NullPointerException("writer cannot be null"); + } + if (items == null) { writer.beginArray(); - for (String item : items) { - writer.value(item); - } writer.endArray(); - return writer.toString(); + } else { + writeArray(items, writer); + } + } + + private static void writeArray(String[] items, JsonWriter writer) { + writer.beginArray(); + for (String item : items) { + writer.value(item); } + writer.endArray(); } /** diff --git a/components/json/src/main/java/datadog/json/JsonWriter.java b/components/json/src/main/java/datadog/json/JsonWriter.java index 1c0d778da6d..ec476d574c1 100644 --- a/components/json/src/main/java/datadog/json/JsonWriter.java +++ b/components/json/src/main/java/datadog/json/JsonWriter.java @@ -247,6 +247,7 @@ public void flush() { /** * Returns the current JSON size in bytes. + * * @return The JSON size in bytes. * @see #toByteArray() */ diff --git a/components/json/src/test/java/datadog/json/JsonMapperTest.java b/components/json/src/test/java/datadog/json/JsonMapperTest.java index 926702881bc..46110942275 100644 --- a/components/json/src/test/java/datadog/json/JsonMapperTest.java +++ b/components/json/src/test/java/datadog/json/JsonMapperTest.java @@ -79,6 +79,23 @@ static Stream testMappingToJsonObjectArguments() { "{\"key1\":null,\"key2\":\"bar\",\"key3\":3,\"key4\":3456789123,\"key5\":3.142,\"key6\":3.141592653589793,\"key7\":true,\"key8\":\"toString\"}")); } + @TableTest({ + "Scenario | Map | Expected ", + "null | | '{}' ", + "empty | [:] | '{}' ", + "default | [key1: value1, key2: value2] | '{\"key1\":\"value1\",\"key2\":\"value2\"}'" + }) + void testWritingAsJsonValue( + @Scenario String ignoredScenario, Map map, String expected) { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject(); + writer.name("map"); + JsonMapper.writeAsJsonValue(writer, (Map) map); + writer.endObject(); + assertEquals("{\"map\":" + expected + "}", writer.toString()); + } + } + @ParameterizedTest(name = "test mapping to Map from empty JSON object: {0}") @NullSource @ValueSource(strings = {"null", "", "{}"}) @@ -110,6 +127,23 @@ void testMappingIterableToJsonArray(List input, String expected) throws assertEquals(input != null ? input : emptyList(), parsed); } + @TableTest({ + "Scenario | Collection | Expected ", + "null | | '[]' ", + "empty | [] | '[]' ", + "default | [value1, value2] | '[\"value1\",\"value2\"]'" + }) + void testWritingCollectionAsJsonValue( + @Scenario String ignoredScenario, List collection, String expected) { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject(); + writer.name("collection"); + JsonMapper.writeAsJsonValue(collection, writer); + writer.endObject(); + assertEquals("{\"collection\":" + expected + "}", writer.toString()); + } + } + @TableTest({ "Scenario | Input | Expected ", "null input | | '[]' ", @@ -128,6 +162,23 @@ void testMappingArrayToJsonArray(String ignoredScenario, String[] input, String assertArrayEquals(input != null ? input : new String[] {}, parsed); } + @TableTest({ + "Scenario | Array | Expected ", + "null | | '[]' ", + "empty | [] | '[]' ", + "default | [value1, value2] | '[\"value1\",\"value2\"]'" + }) + void testWritingArrayAsJsonValue( + @Scenario String ignoredScenario, String[] array, String expected) { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject(); + writer.name("array"); + JsonMapper.writeAsJsonValue(array, writer); + writer.endObject(); + assertEquals("{\"array\":" + expected + "}", writer.toString()); + } + } + @ParameterizedTest(name = "test mapping to List from empty JSON object: {0}") @NullSource @ValueSource(strings = {"null", "", "[]"}) From 3db42b68a9dd62ebcf06655b1e13f6289cd26e93 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Tue, 28 Jul 2026 09:04:30 +0200 Subject: [PATCH 3/3] fix(aws): Fix intermediate writer for tag map --- .../instrumentation/aws/v2/sfn/InputAttributeInjector.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sfn-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/sfn/InputAttributeInjector.java b/dd-java-agent/instrumentation/aws-java/aws-java-sfn-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/sfn/InputAttributeInjector.java index 7ffdc24323f..75b6db833f7 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sfn-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/sfn/InputAttributeInjector.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sfn-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/sfn/InputAttributeInjector.java @@ -8,12 +8,12 @@ public class InputAttributeInjector { private static final String DATADOG_KEY = "_datadog"; public static String buildTraceContext(AgentSpan span) { - String tagsJson = JsonMapper.toJson(span.getTags()); try (JsonWriter writer = new JsonWriter()) { writer.beginObject(); writer.name("x-datadog-trace-id").value(span.getTraceId().toString()); writer.name("x-datadog-parent-id").value(String.valueOf(span.getSpanId())); - writer.name("x-datadog-tags").jsonValue(tagsJson); + writer.name("x-datadog-tags"); + JsonMapper.writeAsJsonValue(writer, span.getTags()); writer.endObject(); return writer.toString(); } catch (Exception e) {