From 686f51ad82571f5a472b4bb7352791ed127d70c4 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Wed, 4 Dec 2024 12:54:52 +0100 Subject: [PATCH] =?UTF-8?q?Add=20JSON=E2=80=AFcomponent=20(#7973)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(json): Add JSON component * feat(internal-api): Remove Strings toJson and escapeToJson methods * feat(ci-visibility): Migrate to JSON component * feat(junit-4.10): Migrate to JSON component * feat(junit-5.3): Migrate to JSON component * feat(karate): Migrate to JSON component * feat(testng): Migrate to JSON component * feat(bootstrap): Migrate to JSON component * feat(json): Improve structure performance --- components/json/build.gradle.kts | 9 + .../datadog/json/JsonWriterBenchmark.java | 106 ++++++ .../main/java/datadog/json/JsonMapper.java | 106 ++++++ .../main/java/datadog/json/JsonStructure.java | 56 +++ .../main/java/datadog/json/JsonWriter.java | 339 ++++++++++++++++++ .../datadog/json/LenientJsonStructure.java | 34 ++ .../java/datadog/json/SafeJsonStructure.java | 89 +++++ .../groovy/datadog/json/JsonMapperTest.groovy | 89 +++++ .../java/datadog/json/JsonStructureTest.java | 33 ++ .../java/datadog/json/JsonWriterTest.java | 168 +++++++++ .../java/datadog/json/SafeJsonWriterTest.java | 132 +++++++ dd-java-agent/agent-bootstrap/build.gradle | 1 + .../agent-ci-visibility/build.gradle | 1 + .../trace/civisibility/ci/BuildkiteInfo.java | 2 +- .../trace/civisibility/ci/CITagsProvider.java | 2 +- .../trace/civisibility/ci/JenkinsInfo.java | 7 +- .../trace/civisibility/domain/TestImpl.java | 2 +- .../civisibility/domain/TestSuiteImpl.java | 2 +- .../events/TestEventsHandlerImpl.java | 21 +- dd-java-agent/build.gradle | 1 + .../instrumentation/junit4/JUnit4Utils.java | 5 +- .../junit5/JUnitPlatformUtils.java | 5 +- .../instrumentation/karate/KarateUtils.java | 4 +- .../karate/src/test/groovy/KarateTest.groovy | 2 +- .../events.ftl | 14 +- .../events.ftl | 4 +- .../events.ftl | 12 +- .../events.ftl | 4 +- .../resources/test-parameterized/events.ftl | 4 +- .../test-retry-parameterized/events.ftl | 12 +- .../instrumentation/testng/TestNGUtils.java | 19 +- .../BootstrapInitializationTelemetry.java | 113 +++--- .../datadog/trace/bootstrap/JsonBuffer.java | 264 -------------- ...ootstrapInitializationTelemetryTest.groovy | 14 +- .../trace/bootstrap/JsonBufferTest.groovy | 126 ------- dd-trace-core/build.gradle | 1 + .../writer/ddintake/CiTestCycleMapperV1.java | 4 +- gradle/dependencies.gradle | 1 + .../main/java/datadog/trace/util/Strings.java | 122 +------ .../datadog/trace/util/StringsTest.groovy | 59 --- settings.gradle | 1 + 41 files changed, 1291 insertions(+), 699 deletions(-) create mode 100644 components/json/build.gradle.kts create mode 100644 components/json/src/jmh/java/datadog/json/JsonWriterBenchmark.java create mode 100644 components/json/src/main/java/datadog/json/JsonMapper.java create mode 100644 components/json/src/main/java/datadog/json/JsonStructure.java create mode 100644 components/json/src/main/java/datadog/json/JsonWriter.java create mode 100644 components/json/src/main/java/datadog/json/LenientJsonStructure.java create mode 100644 components/json/src/main/java/datadog/json/SafeJsonStructure.java create mode 100644 components/json/src/test/groovy/datadog/json/JsonMapperTest.groovy create mode 100644 components/json/src/test/java/datadog/json/JsonStructureTest.java create mode 100644 components/json/src/test/java/datadog/json/JsonWriterTest.java create mode 100644 components/json/src/test/java/datadog/json/SafeJsonWriterTest.java delete mode 100644 dd-java-agent/src/main/java/datadog/trace/bootstrap/JsonBuffer.java delete mode 100644 dd-java-agent/src/test/groovy/datadog/trace/bootstrap/JsonBufferTest.groovy diff --git a/components/json/build.gradle.kts b/components/json/build.gradle.kts new file mode 100644 index 00000000000..4dca7fc3036 --- /dev/null +++ b/components/json/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("me.champeau.jmh") +} + +apply(from = "$rootDir/gradle/java.gradle") + +jmh { + version = "1.28" +} diff --git a/components/json/src/jmh/java/datadog/json/JsonWriterBenchmark.java b/components/json/src/jmh/java/datadog/json/JsonWriterBenchmark.java new file mode 100644 index 00000000000..6e9adc62efc --- /dev/null +++ b/components/json/src/jmh/java/datadog/json/JsonWriterBenchmark.java @@ -0,0 +1,106 @@ +package datadog.json; + +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static org.openjdk.jmh.annotations.Mode.AverageTime; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(AverageTime) +@OutputTimeUnit(MICROSECONDS) +@Fork(value = 1) +@SuppressWarnings("unused") +public class JsonWriterBenchmark { + @Benchmark + public void writeSimpleArray(Blackhole blackhole) { + try (JsonWriter writer = new JsonWriter()) { + writer + .beginArray() + .beginObject() + .name("true") + .value(true) + .endObject() + .beginObject() + .name("false") + .value(false) + .endObject() + .endArray(); + blackhole.consume(writer.toString()); + } + } + + @Benchmark + public void writeComplexArray(Blackhole blackhole) { + try (JsonWriter writer = new JsonWriter()) { + writer + .beginArray() + .value("first level") + .beginArray() + .value("second level") + .beginArray() + .value("third level") + .beginObject() + .name("key") + .value("value") + .endObject() + .beginObject() + .name("key") + .value("value") + .endObject() + .endArray() // second level + .beginObject() + .name("key") + .value("value") + .endObject() + .endArray() // first level + .beginObject() + .name("key") + .value("value") + .endObject() + .value("last value") + .endArray(); + blackhole.consume(writer.toString()); + } + } + + @Benchmark + public void writeComplexObject(Blackhole blackhole) { + try (JsonWriter writer = new JsonWriter()) { + writer + .beginObject() + .name("attrs") + .beginObject() + .name("attr1") + .value("value1") + .name("attr2") + .value("value2") + .endObject() + .name("data") + .beginArray() + .beginObject() + .name("x") + .value(1) + .name("y") + .value(12.3) + .endObject() + .beginObject() + .name("x") + .value(2) + .name("y") + .value(4.56) + .endObject() + .beginObject() + .name("x") + .value(3) + .name("y") + .value(789) + .endObject() + .endArray() + .endObject(); + blackhole.consume(writer.toString()); + } + } +} diff --git a/components/json/src/main/java/datadog/json/JsonMapper.java b/components/json/src/main/java/datadog/json/JsonMapper.java new file mode 100644 index 00000000000..83b8c7f59b8 --- /dev/null +++ b/components/json/src/main/java/datadog/json/JsonMapper.java @@ -0,0 +1,106 @@ +package datadog.json; + +import java.util.Collection; +import java.util.Map; + +/** Utility class for simple Java structure mapping into JSON strings. */ +public final class JsonMapper { + + private JsonMapper() {} + + /** + * Converts a {@link String} to a JSON string. + * + * @param string The string to convert. + * @return The converted JSON string. + */ + public static String toJson(String string) { + if (string == null || string.isEmpty()) { + return ""; + } + try (JsonWriter writer = new JsonWriter()) { + writer.value(string); + return writer.toString(); + } + } + + /** + * Converts a {@link Map} to a JSON object. + * + * @param map The map to convert. + * @return The converted JSON object as Java string. + */ + public static String toJson(Map map) { + if (map == null || map.isEmpty()) { + return "{}"; + } + try (JsonWriter writer = new JsonWriter()) { + 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(); + } + } + + /** + * Converts a {@link Iterable} to a JSON array. + * + * @param items The iterable to convert. + * @return The converted JSON array as Java string. + */ + @SuppressWarnings("DuplicatedCode") + public static String toJson(Collection items) { + if (items == null || items.isEmpty()) { + return "[]"; + } + try (JsonWriter writer = new JsonWriter()) { + writer.beginArray(); + for (String item : items) { + writer.value(item); + } + writer.endArray(); + return writer.toString(); + } + } + + /** + * Converts a String array to a JSON array. + * + * @param items The array to convert. + * @return The converted JSON array as Java string. + */ + @SuppressWarnings("DuplicatedCode") + public static String toJson(String[] items) { + if (items == null) { + return "[]"; + } + try (JsonWriter writer = new JsonWriter()) { + writer.beginArray(); + for (String item : items) { + writer.value(item); + } + writer.endArray(); + return writer.toString(); + } + } +} diff --git a/components/json/src/main/java/datadog/json/JsonStructure.java b/components/json/src/main/java/datadog/json/JsonStructure.java new file mode 100644 index 00000000000..72efe81ec31 --- /dev/null +++ b/components/json/src/main/java/datadog/json/JsonStructure.java @@ -0,0 +1,56 @@ +package datadog.json; + +/** The {@link JsonStructure} keeps track of JSON value being built. */ +interface JsonStructure { + /** + * Begins an object. + * + * @throws IllegalStateException if the object can not be started at this position. + */ + void beginObject(); + + /** + * Checks whether the current position is within an object. + * + * @return {@code true} if the current position is within an object, {@code false} otherwise. + */ + boolean objectStarted(); + + /** + * Ends the current object. + * + * @throws IllegalStateException if the current position is not within an object. + */ + void endObject(); + + /** Begins an array. */ + void beginArray(); + + /** + * Checks whether the current position is within an array. + * + * @return {@code true} if the current position is within an array, {@code false} otherwise. + */ + boolean arrayStarted(); + + /** + * Ends the current array. + * + * @throws IllegalStateException if the current position is not within an array. + */ + void endArray(); + + /** + * Adds a name to the current object. + * + * @throws IllegalStateException if the current position is not within an object. + */ + void addName(); + + /** + * Adds a value. + * + * @throws IllegalStateException if the current position can not have a value. + */ + void addValue(); +} diff --git a/components/json/src/main/java/datadog/json/JsonWriter.java b/components/json/src/main/java/datadog/json/JsonWriter.java new file mode 100644 index 00000000000..7e09b800717 --- /dev/null +++ b/components/json/src/main/java/datadog/json/JsonWriter.java @@ -0,0 +1,339 @@ +package datadog.json; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Locale.ROOT; + +import java.io.ByteArrayOutputStream; +import java.io.Flushable; +import java.io.IOException; +import java.io.OutputStreamWriter; + +/** + * A lightweight JSON writer without dependencies. It performs minimal JSON structure checks unless + * using the lenient mode. + */ +public final class JsonWriter implements Flushable, AutoCloseable { + private static final int INITIAL_CAPACITY = 256; + private final ByteArrayOutputStream outputStream; + private final OutputStreamWriter writer; + private final JsonStructure structure; + + private boolean requireComma; + + /** Creates a writer with structure check. */ + public JsonWriter() { + this(true); + } + + /** + * Creates a writer. + * + * @param safe {@code true} to use safe structure check, {@code false} for lenient mode. + */ + public JsonWriter(boolean safe) { + this.outputStream = new ByteArrayOutputStream(INITIAL_CAPACITY); + this.writer = new OutputStreamWriter(this.outputStream, UTF_8); + this.structure = safe ? new SafeJsonStructure() : new LenientJsonStructure(); + this.requireComma = false; + } + + /** + * Starts a JSON object. + * + * @return This writer instance. + */ + public JsonWriter beginObject() { + this.structure.beginObject(); + injectCommaIfNeeded(); + write('{'); + return this; + } + + /** + * * Ends the current JSON object. + * + * @return This writer. + */ + public JsonWriter endObject() { + this.structure.endObject(); + write('}'); + endsValue(); + return this; + } + + /** + * Writes an object property name. + * + * @param name The property name. + * @return This writer. + */ + public JsonWriter name(String name) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null"); + } + this.structure.addName(); + injectCommaIfNeeded(); + writeStringLiteral(name); + write(':'); + return this; + } + + /** + * Writes a {@code null} value. + * + * @return This writer. + */ + public JsonWriter nullValue() { + this.structure.addValue(); + injectCommaIfNeeded(); + writeStringRaw("null"); + endsValue(); + return this; + } + + /** + * Writes JSON value without escaping it. + * + * @param value The JSON value to write. + * @return This writer. + */ + public JsonWriter jsonValue(String value) { + // No structure check here assuming raw JSON is safe to write + injectCommaIfNeeded(); + writeStringRaw(value); + endsValue(); + return this; + } + + /** + * Writes a boolean value. + * + * @param value The value to write. + * @return This writer. + */ + public JsonWriter value(boolean value) { + this.structure.addValue(); + injectCommaIfNeeded(); + writeStringRaw(value ? "true" : "false"); + endsValue(); + return this; + } + + /** + * Writes a string value. + * + * @param value The value to write. + * @return This writer. + */ + public JsonWriter value(String value) { + if (value == null) { + return nullValue(); + } + this.structure.addValue(); + injectCommaIfNeeded(); + writeStringLiteral(value); + endsValue(); + return this; + } + + /** + * Writes an integer as a number value. + * + * @param value The integer to write. + * @return This writer. + */ + public JsonWriter value(int value) { + this.structure.addValue(); + injectCommaIfNeeded(); + writeStringRaw(Integer.toString(value)); + endsValue(); + return this; + } + + /** + * Writes a long as a number value. + * + * @param value The long to write. + * @return This writer. + */ + public JsonWriter value(long value) { + this.structure.addValue(); + injectCommaIfNeeded(); + writeStringRaw(Long.toString(value)); + endsValue(); + return this; + } + + /** + * Writes a float as a number value. + * + * @param value The float to write. + * @return This writer. + */ + public JsonWriter value(float value) { + if (Float.isNaN(value)) { + return nullValue(); + } + this.structure.addValue(); + injectCommaIfNeeded(); + writeStringRaw(Float.toString(value)); + endsValue(); + return this; + } + + /** + * Writes a double as a number value. + * + * @param value The value to write. + * @return This writer. + */ + public JsonWriter value(double value) { + if (Double.isNaN(value)) { + return nullValue(); + } + this.structure.addValue(); + injectCommaIfNeeded(); + writeStringRaw(Double.toString(value)); + endsValue(); + return this; + } + + /** + * Starts a JSON array. + * + * @return This writer. + */ + public JsonWriter beginArray() { + this.structure.beginArray(); + injectCommaIfNeeded(); + write('['); + return this; + } + + /** + * Ends the current JSON array. + * + * @return This writer. + */ + public JsonWriter endArray() { + this.structure.endArray(); + endsValue(); + write(']'); + return this; + } + + /** + * Gets the JSON String as a UTF-8 byte array. + * + * @return The JSON String as a UTF-8 byte array. + */ + public byte[] toByteArray() { + flush(); + return this.outputStream.toByteArray(); + } + + @Override + public String toString() { + return new String(toByteArray(), UTF_8); + } + + @Override + public void flush() { + try { + this.writer.flush(); + } catch (IOException ignored) { + } + } + + @Override + public void close() { + try { + this.outputStream.close(); + this.writer.close(); + } catch (IOException ignored) { + } + } + + private void injectCommaIfNeeded() { + if (this.requireComma) { + write(','); + } + this.requireComma = false; + } + + private void endsValue() { + this.requireComma = true; + } + + private void write(char ch) { + try { + this.writer.write(ch); + } catch (IOException ignored) { + } + } + + private void writeStringLiteral(String str) { + try { + this.writer.write('"'); + + for (int i = 0; i < str.length(); ++i) { + char c = str.charAt(i); + // Escape any char outside ASCII to their Unicode equivalent + if (c > 127) { + this.writer.write('\\'); + this.writer.write('u'); + String hexCharacter = Integer.toHexString(c).toUpperCase(ROOT); + if (c < 4096) { + this.writer.write('0'); + if (c < 256) { + this.writer.write('0'); + } + } + this.writer.append(hexCharacter); + } else { + switch (c) { + case '"': // Quotation mark + case '\\': // Reverse solidus + case '/': // Solidus + this.writer.write('\\'); + this.writer.write(c); + break; + case '\b': // Backspace + this.writer.write('\\'); + this.writer.write('b'); + break; + case '\f': // Form feed + this.writer.write('\\'); + this.writer.write('f'); + break; + case '\n': // Line feed + this.writer.write('\\'); + this.writer.write('n'); + break; + case '\r': // Carriage return + this.writer.write('\\'); + this.writer.write('r'); + break; + case '\t': // Horizontal tab + this.writer.write('\\'); + this.writer.write('t'); + break; + default: + this.writer.write(c); + break; + } + } + } + + this.writer.write('"'); + } catch (IOException ignored) { + } + } + + private void writeStringRaw(String str) { + try { + this.writer.write(str); + } catch (IOException ignored) { + } + } +} diff --git a/components/json/src/main/java/datadog/json/LenientJsonStructure.java b/components/json/src/main/java/datadog/json/LenientJsonStructure.java new file mode 100644 index 00000000000..ee1cfd7331e --- /dev/null +++ b/components/json/src/main/java/datadog/json/LenientJsonStructure.java @@ -0,0 +1,34 @@ +package datadog.json; + +/** A permissive {@link JsonStructure} that performs no structural checks on the built JSON. */ +class LenientJsonStructure implements JsonStructure { + LenientJsonStructure() {} + + @Override + public void beginObject() {} + + @Override + public boolean objectStarted() { + return true; + } + + @Override + public void endObject() {} + + @Override + public void beginArray() {} + + @Override + public boolean arrayStarted() { + return true; + } + + @Override + public void endArray() {} + + @Override + public void addName() {} + + @Override + public void addValue() {} +} diff --git a/components/json/src/main/java/datadog/json/SafeJsonStructure.java b/components/json/src/main/java/datadog/json/SafeJsonStructure.java new file mode 100644 index 00000000000..d6ac0adabf5 --- /dev/null +++ b/components/json/src/main/java/datadog/json/SafeJsonStructure.java @@ -0,0 +1,89 @@ +package datadog.json; + +import java.util.BitSet; + +/** + * This {@link JsonStructure} performs minimal structure checks to ensure the built JSON is + * coherent. + */ +class SafeJsonStructure implements JsonStructure { + private final BitSet structure; + private int depth; + private boolean complete; + + SafeJsonStructure() { + this.structure = new BitSet(); + this.depth = -1; + this.complete = false; + } + + @Override + public void beginObject() { + if (this.complete) { + throw new IllegalStateException("Object is complete"); + } + this.structure.set(++this.depth); + } + + @Override + public boolean objectStarted() { + return this.depth >= 0 && this.structure.get(this.depth); + } + + @Override + public void endObject() { + if (!objectStarted()) { + throw new IllegalStateException("Object not started"); + } + this.depth--; + if (this.depth < 0) { + this.complete = true; + } + } + + @Override + public void beginArray() { + if (this.complete) { + throw new IllegalStateException("Object is complete"); + } + this.structure.clear(++this.depth); + } + + @Override + public boolean arrayStarted() { + return this.depth >= 0 && !this.structure.get(this.depth); + } + + @Override + public void endArray() { + if (!arrayStarted()) { + throw new IllegalStateException("Array not started"); + } + this.depth--; + if (this.depth < 0) { + this.complete = true; + } + } + + @Override + public void addName() { + if (!objectStarted()) { + throw new IllegalStateException("Object not started"); + } + } + + @Override + public void addValue() { + if (this.complete) { + throw new IllegalStateException("Object is complete"); + } + if (this.depth < 0) { + this.complete = true; + } + } + + @Override + public String toString() { + return (this.complete ? "complete" : "") + this.structure; + } +} diff --git a/components/json/src/test/groovy/datadog/json/JsonMapperTest.groovy b/components/json/src/test/groovy/datadog/json/JsonMapperTest.groovy new file mode 100644 index 00000000000..2c17cd87e53 --- /dev/null +++ b/components/json/src/test/groovy/datadog/json/JsonMapperTest.groovy @@ -0,0 +1,89 @@ +package datadog.json + +import spock.lang.Specification + +import static java.lang.Math.PI + +class JsonMapperTest extends Specification { + + def "test mapping to JSON object: #input"() { + when: + String json = JsonMapper.toJson((Map) input) + + then: + json == expected + + where: + input | expected + null | '{}' + new HashMap<>() | '{}' + ['key1': 'value1'] | '{"key1":"value1"}' + ['key1': 'value1', 'key2': 'value2'] | '{"key1":"value1","key2":"value2"}' + ['key1': 'va"lu"e1', 'ke"y2': 'value2'] | '{"key1":"va\\"lu\\"e1","ke\\"y2":"value2"}' + ['key1': null, 'key2': 'bar', 'key3': 3, 'key4': 3456789123L, 'key5': 3.142f, 'key6': PI, 'key7': true, 'key8': new UnsupportedType()] | '{"key1":null,"key2":"bar","key3":3,"key4":3456789123,"key5":3.142,"key6":3.141592653589793,"key7":true,"key8":"toString"}' + } + + private class UnsupportedType { + @Override + String toString() { + 'toString' + } + } + + def "test mapping iterable to JSON array: #input"() { + when: + String json = JsonMapper.toJson(input as Collection) + + then: + json == expected + + where: + input | expected + null | "[]" + new ArrayList<>() | "[]" + ['value1'] | "[\"value1\"]" + ['value1', 'value2'] | "[\"value1\",\"value2\"]" + ['va"lu"e1', 'value2'] | "[\"va\\\"lu\\\"e1\",\"value2\"]" + } + + def "test mapping array to JSON array: #input"() { + when: + String json = JsonMapper.toJson((String[]) input) + + then: + json == expected + + where: + input | expected + null | "[]" + [] | "[]" + ['value1'] | "[\"value1\"]" + ['value1', 'value2'] | "[\"value1\",\"value2\"]" + ['va"lu"e1', 'value2'] | "[\"va\\\"lu\\\"e1\",\"value2\"]" + } + + def "test mapping to JSON string: input"() { + when: + String escaped = JsonMapper.toJson((String) string) + + then: + escaped == expected + + where: + string | expected + null | "" + "" | "" + ((char) 4096).toString() | '"\\u1000"' + ((char) 256).toString() | '"\\u0100"' + ((char) 128).toString() | '"\\u0080"' + "\b" | '"\\b"' + "\t" | '"\\t"' + "\n" | '"\\n"' + "\f" | '"\\f"' + "\r" | '"\\r"' + '"' | '"\\\""' + '/' | '"\\/"' + '\\' | '"\\\\"' + "a" | '"a"' + } +} diff --git a/components/json/src/test/java/datadog/json/JsonStructureTest.java b/components/json/src/test/java/datadog/json/JsonStructureTest.java new file mode 100644 index 00000000000..e3fb9d16802 --- /dev/null +++ b/components/json/src/test/java/datadog/json/JsonStructureTest.java @@ -0,0 +1,33 @@ +package datadog.json; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class JsonStructureTest { + // toString is only for debug purpose and has no behavior to cover + @Test + void coverToString() { + SafeJsonStructure safeJsonStructure = new SafeJsonStructure(); + assertNotNull(safeJsonStructure.toString()); + safeJsonStructure.beginObject(); + safeJsonStructure.endObject(); + assertNotNull(safeJsonStructure.toString()); + } + + // Lenient is a stub and has no behavior to cover + @Test + void lenientStructure() { + LenientJsonStructure lenientJsonStructure = new LenientJsonStructure(); + lenientJsonStructure.beginObject(); + assertTrue(lenientJsonStructure.objectStarted()); + lenientJsonStructure.endObject(); + lenientJsonStructure.beginArray(); + assertTrue(lenientJsonStructure.arrayStarted()); + lenientJsonStructure.endArray(); + assertDoesNotThrow(lenientJsonStructure::addName); + assertDoesNotThrow(lenientJsonStructure::addValue); + } +} diff --git a/components/json/src/test/java/datadog/json/JsonWriterTest.java b/components/json/src/test/java/datadog/json/JsonWriterTest.java new file mode 100644 index 00000000000..9a4b913e36d --- /dev/null +++ b/components/json/src/test/java/datadog/json/JsonWriterTest.java @@ -0,0 +1,168 @@ +package datadog.json; + +import static java.lang.Math.PI; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class JsonWriterTest { + @Test + void testObject() { + try (JsonWriter writer = new JsonWriter()) { + writer + .beginObject() + .name("string") + .value("bar") + .name("int") + .value(3) + .name("long") + .value(3456789123L) + .name("float") + .value(3.142) + .name("double") + .value(PI) + .name("true") + .value(true) + .name("false") + .value(false) + .name("null") + .nullValue() + .endObject(); + + assertEquals( + "{\"string\":\"bar\",\"int\":3,\"long\":3456789123,\"float\":3.142,\"double\":3.141592653589793,\"true\":true,\"false\":false,\"null\":null}", + writer.toString(), + "Check object writer"); + } + } + + @Test + void testNullName() { + assertThrows( + IllegalArgumentException.class, + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject(); + writer.name(null); + } + }, + "Check null name"); + } + + @Test + void testNullValue() { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject(); + writer.name("string").value(null); + writer.endObject(); + assertEquals("{\"string\":null}", writer.toString(), "Check null string"); + } + } + + @Test + void testNaNValues() { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject(); + writer.name("float").value(Float.NaN); + writer.name("double").value(Double.NaN); + writer.endObject(); + assertEquals("{\"float\":null,\"double\":null}", writer.toString(), "Check NaN values"); + } + } + + @Test + void testArray() { + try (JsonWriter writer = new JsonWriter()) { + writer.beginArray().value("foo").value("baz").value("bar").value("quux").endArray(); + + assertEquals("[\"foo\",\"baz\",\"bar\",\"quux\"]", writer.toString(), "Check array writer"); + } + } + + @Test + void testStringEscaping() { + try (JsonWriter writer = new JsonWriter()) { + writer + .beginArray() + .value("\"") + .value("\\") + .value("/") + .value("\b") + .value("\f") + .value("\n") + .value("\r") + .value("\t") + .endArray(); + + assertEquals( + "[\"\\\"\",\"\\\\\",\"\\/\",\"\\b\",\"\\f\",\"\\n\",\"\\r\",\"\\t\"]", + writer.toString(), + "Check string escaping"); + } + } + + @Test + void testArrayObjectNesting() { + try (JsonWriter writer = new JsonWriter()) { + writer + .beginObject() + .name("array") + .beginArray() + .value("true") + .value("false") + .endArray() + .endObject(); + + assertEquals( + "{\"array\":[\"true\",\"false\"]}", writer.toString(), "Check array / object nesting"); + } + } + + @Test + void testObjectArrayNesting() { + try (JsonWriter writer = new JsonWriter()) { + writer + .beginArray() + .beginObject() + .name("true") + .value(true) + .endObject() + .beginObject() + .name("false") + .value(false) + .endObject() + .endArray(); + + assertEquals( + "[{\"true\":true},{\"false\":false}]", writer.toString(), "Check object / array nesting"); + } + } + + @Test + void testNameOnlyInObject() { + try (JsonWriter writer = new JsonWriter()) { + assertThrows( + IllegalStateException.class, () -> writer.name("key"), "Check name only in object"); + } + } + + @Test + void testCompleteObject() { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject(); + writer.endObject(); + assertThrows(IllegalStateException.class, writer::beginObject, "Check complete object"); + assertThrows(IllegalStateException.class, writer::beginArray, "Check complete object"); + } + } + + @Test + void testCompleteArray() { + try (JsonWriter writer = new JsonWriter()) { + writer.beginArray(); + writer.endArray(); + assertThrows(IllegalStateException.class, writer::beginObject, "Check complete array"); + assertThrows(IllegalStateException.class, writer::beginArray, "Check complete array"); + } + } +} diff --git a/components/json/src/test/java/datadog/json/SafeJsonWriterTest.java b/components/json/src/test/java/datadog/json/SafeJsonWriterTest.java new file mode 100644 index 00000000000..5299f961f05 --- /dev/null +++ b/components/json/src/test/java/datadog/json/SafeJsonWriterTest.java @@ -0,0 +1,132 @@ +package datadog.json; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class SafeJsonWriterTest { + @Test + void testRootElement() { + assertDoesNotThrow( + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject().endObject(); + } + }, + "Check object allowed as root element"); + assertDoesNotThrow( + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject().beginArray(); + } + }, + "Check array allowed as root element"); + assertDoesNotThrow( + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer.value("string"); + } + }, + "Check string allowed as root element"); + assertDoesNotThrow( + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer.value(1); + } + }, + "Check number allowed as root element"); + assertDoesNotThrow( + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer.value(true); + } + }, + "Check boolean allowed as root element"); + assertDoesNotThrow( + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer.nullValue(); + } + }, + "Check null value allowed as root element"); + } + + @Test + void testNestedElements() { + assertDoesNotThrow( + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer + .beginObject() + .beginObject() + .beginObject() + .endObject() + .beginObject() + .endObject() + .endObject() + .endObject(); + } + }, + "Check nested objects"); + assertDoesNotThrow( + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer + .beginArray() + .beginArray() + .beginArray() + .endArray() + .beginArray() + .endArray() + .endArray() + .endArray(); + } + }, + "Check nested arrays"); + + assertThrows( + IllegalStateException.class, + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject().beginObject().endObject().endArray(); + } + }, + "Check invalid array end"); + assertThrows( + IllegalStateException.class, + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer.beginArray().beginArray().endArray().endObject(); + } + }, + "Check invalid object end"); + } + + @Test + void testCompleteJson() { + assertThrows( + IllegalStateException.class, + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject().endObject().value("invalid"); + } + }, + "Check complete object"); + assertThrows( + IllegalStateException.class, + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer.beginArray().endArray().value("invalid"); + } + }, + "Check complete array"); + assertThrows( + IllegalStateException.class, + () -> { + try (JsonWriter writer = new JsonWriter()) { + writer.value("string").value("invalid"); + } + }, + "Check complete value"); + } +} diff --git a/dd-java-agent/agent-bootstrap/build.gradle b/dd-java-agent/agent-bootstrap/build.gradle index 3aa57b554e2..6e5ed6223de 100644 --- a/dd-java-agent/agent-bootstrap/build.gradle +++ b/dd-java-agent/agent-bootstrap/build.gradle @@ -22,6 +22,7 @@ dependencies { api project(':internal-api:internal-api-9') api project(':dd-java-agent:agent-logging') api project(':dd-java-agent:agent-debugger:debugger-bootstrap') + api project(':components:json') api libs.slf4j // ^ Generally a bad idea for libraries, but we're shadowing. diff --git a/dd-java-agent/agent-ci-visibility/build.gradle b/dd-java-agent/agent-ci-visibility/build.gradle index 7f15e8abefb..92329277890 100644 --- a/dd-java-agent/agent-ci-visibility/build.gradle +++ b/dd-java-agent/agent-ci-visibility/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation group: 'org.jacoco', name: 'org.jacoco.report', version: '0.8.12' implementation project(':communication') + implementation project(':components:json') implementation project(':internal-api') implementation project(':internal-api:internal-api-9') diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BuildkiteInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BuildkiteInfo.java index 4aa2a6781b9..a558543e791 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BuildkiteInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BuildkiteInfo.java @@ -1,10 +1,10 @@ package datadog.trace.civisibility.ci; +import static datadog.json.JsonMapper.toJson; import static datadog.trace.api.git.GitUtils.filterSensitiveInfo; import static datadog.trace.api.git.GitUtils.normalizeBranch; import static datadog.trace.api.git.GitUtils.normalizeTag; import static datadog.trace.civisibility.utils.FileUtils.expandTilde; -import static datadog.trace.util.Strings.toJson; import datadog.trace.api.civisibility.telemetry.tag.Provider; import datadog.trace.api.git.CommitInfo; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CITagsProvider.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CITagsProvider.java index 6dbced9cc04..469cc5762de 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CITagsProvider.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CITagsProvider.java @@ -1,6 +1,6 @@ package datadog.trace.civisibility.ci; -import static datadog.trace.util.Strings.toJson; +import static datadog.json.JsonMapper.toJson; import datadog.trace.api.DDTags; import datadog.trace.api.git.GitInfo; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/JenkinsInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/JenkinsInfo.java index bb3d56f81e3..0d0ec299ac6 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/JenkinsInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/JenkinsInfo.java @@ -1,5 +1,6 @@ package datadog.trace.civisibility.ci; +import static datadog.json.JsonMapper.toJson; import static datadog.trace.api.git.GitUtils.filterSensitiveInfo; import static datadog.trace.api.git.GitUtils.isTagReference; import static datadog.trace.api.git.GitUtils.normalizeBranch; @@ -10,11 +11,8 @@ import datadog.trace.api.git.CommitInfo; import datadog.trace.api.git.GitInfo; import datadog.trace.civisibility.ci.env.CiEnvironment; -import datadog.trace.util.Strings; import de.thetaphi.forbiddenapis.SuppressForbidden; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Locale; import java.util.Map; @@ -75,8 +73,7 @@ private String buildCiNodeLabels() { if (labels == null || labels.isEmpty()) { return labels; } - List labelsList = Arrays.asList(labels.split(" ")); - return Strings.toJson(labelsList); + return toJson(labels.split(" ")); } private String buildGitRepositoryUrl() { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java index 9479fe3c0e9..ea3dcff6c2b 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java @@ -1,8 +1,8 @@ package datadog.trace.civisibility.domain; +import static datadog.json.JsonMapper.toJson; import static datadog.trace.api.civisibility.CIConstants.CI_VISIBILITY_INSTRUMENTATION_NAME; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; -import static datadog.trace.util.Strings.toJson; import datadog.trace.api.Config; import datadog.trace.api.DDTraceId; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestSuiteImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestSuiteImpl.java index fb546111375..7caae522969 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestSuiteImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestSuiteImpl.java @@ -1,8 +1,8 @@ package datadog.trace.civisibility.domain; +import static datadog.json.JsonMapper.toJson; import static datadog.trace.api.civisibility.CIConstants.CI_VISIBILITY_INSTRUMENTATION_NAME; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; -import static datadog.trace.util.Strings.toJson; import datadog.trace.api.Config; import datadog.trace.api.civisibility.DDTestSuite; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java index 9d07f0d2e32..a634586f5cd 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java @@ -1,7 +1,6 @@ package datadog.trace.civisibility.events; -import static datadog.trace.util.Strings.toJson; - +import datadog.json.JsonWriter; import datadog.trace.api.DisableTestTrace; import datadog.trace.api.civisibility.DDTest; import datadog.trace.api.civisibility.DDTestSuite; @@ -21,7 +20,6 @@ import datadog.trace.civisibility.domain.TestSuiteImpl; import java.lang.reflect.Method; import java.util.Collection; -import java.util.Collections; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.objectweb.asm.Type; @@ -76,13 +74,23 @@ public void onTestSuiteStart( } } if (categories != null && !categories.isEmpty()) { - testSuite.setTag( - Tags.TEST_TRAITS, toJson(Collections.singletonMap("category", toJson(categories)), true)); + testSuite.setTag(Tags.TEST_TRAITS, getTestTraits(categories)); } inProgressTestSuites.put(descriptor, testSuite); } + private String getTestTraits(Collection categories) { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject().name("category").beginArray(); + for (String category : categories) { + writer.value(category); + } + writer.endArray().endObject(); + return writer.toString(); + } + } + @Override public void onTestSuiteFinish(SuiteKey descriptor) { if (skipTrace(descriptor.getClass())) { @@ -160,8 +168,7 @@ public void onTestStart( test.setTag(Tags.TEST_SOURCE_METHOD, testMethodName + Type.getMethodDescriptor(testMethod)); } if (categories != null && !categories.isEmpty()) { - String json = toJson(Collections.singletonMap("category", toJson(categories)), true); - test.setTag(Tags.TEST_TRAITS, json); + test.setTag(Tags.TEST_TRAITS, getTestTraits(categories)); for (String category : categories) { if (category.endsWith(InstrumentationBridge.ITR_UNSKIPPABLE_TAG)) { diff --git a/dd-java-agent/build.gradle b/dd-java-agent/build.gradle index af6a9b84792..d8ce652266e 100644 --- a/dd-java-agent/build.gradle +++ b/dd-java-agent/build.gradle @@ -201,6 +201,7 @@ tasks.withType(GenerateMavenPom).configureEach { task -> } dependencies { + implementation project(path: ':components:json') modules { module("com.squareup.okio:okio") { replacedBy("com.datadoghq.okio:okio") // embed our patched fork diff --git a/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Utils.java b/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Utils.java index af26acf3b0e..4d4c82675e0 100644 --- a/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Utils.java +++ b/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Utils.java @@ -1,10 +1,11 @@ package datadog.trace.instrumentation.junit4; +import static datadog.json.JsonMapper.toJson; + import datadog.trace.api.civisibility.config.TestIdentifier; import datadog.trace.api.civisibility.events.TestDescriptor; import datadog.trace.api.civisibility.events.TestSuiteDescriptor; import datadog.trace.util.MethodHandles; -import datadog.trace.util.Strings; import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandle; import java.lang.reflect.Method; @@ -201,7 +202,7 @@ public static String getParameters(final Description description) { // No public access to the test parameters map in JUnit4. // In this case, we store the fullTestName in the "metadata.test_name" object. - return "{\"metadata\":{\"test_name\":\"" + Strings.escapeToJson(methodName) + "\"}}"; + return "{\"metadata\":{\"test_name\":" + toJson(methodName) + "}}"; } public static List getCategories(Class testClass, @Nullable Method testMethod) { diff --git a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnitPlatformUtils.java b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnitPlatformUtils.java index 8059de35721..2a10d78066c 100644 --- a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnitPlatformUtils.java +++ b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnitPlatformUtils.java @@ -1,12 +1,13 @@ package datadog.trace.instrumentation.junit5; +import static datadog.json.JsonMapper.toJson; + import datadog.trace.api.civisibility.config.TestIdentifier; import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; import datadog.trace.util.MethodHandles; -import datadog.trace.util.Strings; import java.lang.invoke.MethodHandle; import java.lang.reflect.Method; import java.util.List; @@ -89,7 +90,7 @@ public static String getParameters(MethodSource methodSource, String displayName || methodSource.getMethodParameterTypes().isEmpty()) { return null; } - return "{\"metadata\":{\"test_name\":\"" + Strings.escapeToJson(displayName) + "\"}}"; + return "{\"metadata\":{\"test_name\":" + toJson(displayName) + "}}"; } public static TestIdentifier toTestIdentifier(TestDescriptor testDescriptor) { diff --git a/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateUtils.java b/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateUtils.java index b0b00872a55..74cc1bc4ab3 100644 --- a/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateUtils.java +++ b/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateUtils.java @@ -1,5 +1,7 @@ package datadog.trace.instrumentation.karate; +import static datadog.json.JsonMapper.toJson; + import com.intuit.karate.core.Feature; import com.intuit.karate.core.FeatureRuntime; import com.intuit.karate.core.Result; @@ -76,7 +78,7 @@ public static List getCategories(List tags) { } public static String getParameters(Scenario scenario) { - return scenario.getExampleData() != null ? Strings.toJson(scenario.getExampleData()) : null; + return scenario.getExampleData() != null ? toJson(scenario.getExampleData()) : null; } public static TestIdentifier toTestIdentifier(Scenario scenario) { diff --git a/dd-java-agent/instrumentation/karate/src/test/groovy/KarateTest.groovy b/dd-java-agent/instrumentation/karate/src/test/groovy/KarateTest.groovy index 9646681d7c4..76cc50de5d5 100644 --- a/dd-java-agent/instrumentation/karate/src/test/groovy/KarateTest.groovy +++ b/dd-java-agent/instrumentation/karate/src/test/groovy/KarateTest.groovy @@ -59,7 +59,7 @@ class KarateTest extends CiVisibilityInstrumentationTest { testcaseName | tests | expectedTracesCount | skippedTests "test-itr-skipping" | [TestSucceedKarate] | 3 | [new TestIdentifier("[org/example/test_succeed] test succeed", "first scenario", null)] "test-itr-skipping-parameterized" | [TestParameterizedKarate] | 3 | [ - new TestIdentifier("[org/example/test_parameterized] test parameterized", "first scenario as an outline", '{"param":"\\\'a\\\'","value":"aa"}') + new TestIdentifier("[org/example/test_parameterized] test parameterized", "first scenario as an outline", '{"param":"\'a\'","value":"aa"}') ] "test-itr-unskippable" | [TestUnskippableKarate] | 3 | [new TestIdentifier("[org/example/test_unskippable] test unskippable", "first scenario", null)] } diff --git a/dd-java-agent/instrumentation/karate/src/test/resources/test-efd-faulty-session-threshold/events.ftl b/dd-java-agent/instrumentation/karate/src/test/resources/test-efd-faulty-session-threshold/events.ftl index 005eb1c5ee9..8565d778131 100644 --- a/dd-java-agent/instrumentation/karate/src/test/resources/test-efd-faulty-session-threshold/events.ftl +++ b/dd-java-agent/instrumentation/karate/src/test/resources/test-efd-faulty-session-threshold/events.ftl @@ -567,7 +567,7 @@ "test_session.name" : "session-name", "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aa\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, @@ -611,7 +611,7 @@ "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", "test.is_retry" : "true", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aa\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, @@ -655,7 +655,7 @@ "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", "test.is_retry" : "true", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aa\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, @@ -698,7 +698,7 @@ "test_session.name" : "session-name", "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", - "test.parameters" : "{\"param\":\"\\'b\\'\",\"value\":\"bb\"}", + "test.parameters" : "{\"param\":\"'b'\",\"value\":\"bb\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, @@ -742,7 +742,7 @@ "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", "test.is_retry" : "true", - "test.parameters" : "{\"param\":\"\\'b\\'\",\"value\":\"bb\"}", + "test.parameters" : "{\"param\":\"'b'\",\"value\":\"bb\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, @@ -786,7 +786,7 @@ "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", "test.is_retry" : "true", - "test.parameters" : "{\"param\":\"\\'b\\'\",\"value\":\"bb\"}", + "test.parameters" : "{\"param\":\"'b'\",\"value\":\"bb\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, @@ -829,7 +829,7 @@ "test_session.name" : "session-name", "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", - "test.parameters" : "{\"param\":\"\\'c\\'\",\"value\":\"cc\"}", + "test.parameters" : "{\"param\":\"'c'\",\"value\":\"cc\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, diff --git a/dd-java-agent/instrumentation/karate/src/test/resources/test-efd-known-parameterized-test/events.ftl b/dd-java-agent/instrumentation/karate/src/test/resources/test-efd-known-parameterized-test/events.ftl index 70811f5f9af..afab8ca8e38 100644 --- a/dd-java-agent/instrumentation/karate/src/test/resources/test-efd-known-parameterized-test/events.ftl +++ b/dd-java-agent/instrumentation/karate/src/test/resources/test-efd-known-parameterized-test/events.ftl @@ -201,7 +201,7 @@ "language" : "jvm", "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aa\"}", "library_version" : ${content_meta_library_version}, "component" : "karate", "_dd.profiling.ctx" : "test", @@ -243,7 +243,7 @@ "language" : "jvm", "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", - "test.parameters" : "{\"param\":\"\\'b\\'\",\"value\":\"bb\"}", + "test.parameters" : "{\"param\":\"'b'\",\"value\":\"bb\"}", "library_version" : ${content_meta_library_version}, "component" : "karate", "_dd.profiling.ctx" : "test", diff --git a/dd-java-agent/instrumentation/karate/src/test/resources/test-efd-new-parameterized-test/events.ftl b/dd-java-agent/instrumentation/karate/src/test/resources/test-efd-new-parameterized-test/events.ftl index 5adac82097a..2bc327270fa 100644 --- a/dd-java-agent/instrumentation/karate/src/test/resources/test-efd-new-parameterized-test/events.ftl +++ b/dd-java-agent/instrumentation/karate/src/test/resources/test-efd-new-parameterized-test/events.ftl @@ -495,7 +495,7 @@ "test_session.name" : "session-name", "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aa\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, @@ -539,7 +539,7 @@ "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", "test.is_retry" : "true", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aa\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, @@ -583,7 +583,7 @@ "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", "test.is_retry" : "true", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aa\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, @@ -626,7 +626,7 @@ "test_session.name" : "session-name", "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", - "test.parameters" : "{\"param\":\"\\'b\\'\",\"value\":\"bb\"}", + "test.parameters" : "{\"param\":\"'b'\",\"value\":\"bb\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, @@ -670,7 +670,7 @@ "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", "test.is_retry" : "true", - "test.parameters" : "{\"param\":\"\\'b\\'\",\"value\":\"bb\"}", + "test.parameters" : "{\"param\":\"'b'\",\"value\":\"bb\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, @@ -714,7 +714,7 @@ "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", "test.is_retry" : "true", - "test.parameters" : "{\"param\":\"\\'b\\'\",\"value\":\"bb\"}", + "test.parameters" : "{\"param\":\"'b'\",\"value\":\"bb\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, diff --git a/dd-java-agent/instrumentation/karate/src/test/resources/test-itr-skipping-parameterized/events.ftl b/dd-java-agent/instrumentation/karate/src/test/resources/test-itr-skipping-parameterized/events.ftl index 5664bd058e7..98e2ff99d88 100644 --- a/dd-java-agent/instrumentation/karate/src/test/resources/test-itr-skipping-parameterized/events.ftl +++ b/dd-java-agent/instrumentation/karate/src/test/resources/test-itr-skipping-parameterized/events.ftl @@ -136,7 +136,7 @@ "test_session.name" : "session-name", "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aa\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.skipped_by_itr" : "true", @@ -180,7 +180,7 @@ "test_session.name" : "session-name", "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", - "test.parameters" : "{\"param\":\"\\'b\\'\",\"value\":\"bb\"}", + "test.parameters" : "{\"param\":\"'b'\",\"value\":\"bb\"}", "component" : "karate", "_dd.profiling.ctx" : "test", "test.framework_version" : ${content_meta_test_framework_version}, diff --git a/dd-java-agent/instrumentation/karate/src/test/resources/test-parameterized/events.ftl b/dd-java-agent/instrumentation/karate/src/test/resources/test-parameterized/events.ftl index 11ae6fb4373..671fe8561cf 100644 --- a/dd-java-agent/instrumentation/karate/src/test/resources/test-parameterized/events.ftl +++ b/dd-java-agent/instrumentation/karate/src/test/resources/test-parameterized/events.ftl @@ -201,7 +201,7 @@ "language" : "jvm", "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aa\"}", "library_version" : ${content_meta_library_version}, "component" : "karate", "_dd.profiling.ctx" : "test", @@ -243,7 +243,7 @@ "language" : "jvm", "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", - "test.parameters" : "{\"param\":\"\\'b\\'\",\"value\":\"bb\"}", + "test.parameters" : "{\"param\":\"'b'\",\"value\":\"bb\"}", "library_version" : ${content_meta_library_version}, "component" : "karate", "_dd.profiling.ctx" : "test", diff --git a/dd-java-agent/instrumentation/karate/src/test/resources/test-retry-parameterized/events.ftl b/dd-java-agent/instrumentation/karate/src/test/resources/test-retry-parameterized/events.ftl index f134e05256c..f3c8ac0740d 100644 --- a/dd-java-agent/instrumentation/karate/src/test/resources/test-retry-parameterized/events.ftl +++ b/dd-java-agent/instrumentation/karate/src/test/resources/test-retry-parameterized/events.ftl @@ -497,7 +497,7 @@ "test_session.name" : "session-name", "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aaa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aaa\"}", "component" : "karate", "error.type" : "com.intuit.karate.KarateException", "_dd.profiling.ctx" : "test", @@ -543,7 +543,7 @@ "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", "test.is_retry" : "true", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aaa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aaa\"}", "component" : "karate", "error.type" : "com.intuit.karate.KarateException", "_dd.profiling.ctx" : "test", @@ -589,7 +589,7 @@ "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", "test.is_retry" : "true", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aaa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aaa\"}", "component" : "karate", "error.type" : "com.intuit.karate.KarateException", "_dd.profiling.ctx" : "test", @@ -635,7 +635,7 @@ "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", "test.is_retry" : "true", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aaa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aaa\"}", "component" : "karate", "error.type" : "com.intuit.karate.KarateException", "_dd.profiling.ctx" : "test", @@ -681,7 +681,7 @@ "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", "test.is_retry" : "true", - "test.parameters" : "{\"param\":\"\\'a\\'\",\"value\":\"aaa\"}", + "test.parameters" : "{\"param\":\"'a'\",\"value\":\"aaa\"}", "component" : "karate", "error.type" : "com.intuit.karate.KarateException", "_dd.profiling.ctx" : "test", @@ -721,7 +721,7 @@ "language" : "jvm", "env" : "none", "dummy_ci_tag" : "dummy_ci_tag_value", - "test.parameters" : "{\"param\":\"\\'b\\'\",\"value\":\"bb\"}", + "test.parameters" : "{\"param\":\"'b'\",\"value\":\"bb\"}", "library_version" : ${content_meta_library_version}, "component" : "karate", "_dd.profiling.ctx" : "test", diff --git a/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/TestNGUtils.java b/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/TestNGUtils.java index 4ec7cb4d464..af62c182d10 100644 --- a/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/TestNGUtils.java +++ b/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/TestNGUtils.java @@ -1,8 +1,8 @@ package datadog.trace.instrumentation.testng; +import datadog.json.JsonWriter; import datadog.trace.api.civisibility.config.TestIdentifier; import datadog.trace.api.civisibility.events.TestSuiteDescriptor; -import datadog.trace.util.Strings; import java.io.InputStream; import java.lang.invoke.MethodHandle; import java.lang.reflect.Method; @@ -75,19 +75,14 @@ public static String getParameters(Object[] parameters) { // We build manually the JSON for test.parameters tag. // Example: {"arguments":{"0":"param1","1":"param2"}} - final StringBuilder sb = new StringBuilder("{\"arguments\":{"); - for (int i = 0; i < parameters.length; i++) { - sb.append('\"') - .append(i) - .append("\":\"") - .append(Strings.escapeToJson(String.valueOf(parameters[i]))) - .append('\"'); - if (i != parameters.length - 1) { - sb.append(','); + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject().name("arguments").beginObject(); + for (int i = 0; i < parameters.length; i++) { + writer.name(Integer.toString(i)).value(String.valueOf(parameters[i])); } + writer.endObject().endObject(); + return writer.toString(); } - sb.append("}}"); - return sb.toString(); } public static List getGroups(ITestResult result) { diff --git a/dd-java-agent/src/main/java/datadog/trace/bootstrap/BootstrapInitializationTelemetry.java b/dd-java-agent/src/main/java/datadog/trace/bootstrap/BootstrapInitializationTelemetry.java index c423e0175d0..c896983af22 100644 --- a/dd-java-agent/src/main/java/datadog/trace/bootstrap/BootstrapInitializationTelemetry.java +++ b/dd-java-agent/src/main/java/datadog/trace/bootstrap/BootstrapInitializationTelemetry.java @@ -1,13 +1,16 @@ package datadog.trace.bootstrap; +import datadog.json.JsonWriter; import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; /** Thread safe telemetry class used to relay information about tracer activation. */ public abstract class BootstrapInitializationTelemetry { /** Returns a singleton no op instance of initialization telemetry */ - public static final BootstrapInitializationTelemetry noOpInstance() { + public static BootstrapInitializationTelemetry noOpInstance() { return NoOp.INSTANCE; } @@ -17,8 +20,7 @@ public static final BootstrapInitializationTelemetry noOpInstance() { * * @param forwarderPath - a String - path to forwarding executable */ - public static final BootstrapInitializationTelemetry createFromForwarderPath( - String forwarderPath) { + public static BootstrapInitializationTelemetry createFromForwarderPath(String forwarderPath) { return new JsonBased(new ForwarderJsonSender(forwarderPath)); } @@ -85,27 +87,29 @@ public void finish() {} public static final class JsonBased extends BootstrapInitializationTelemetry { private final JsonSender sender; - private JsonBuffer metaBuffer = new JsonBuffer(); - private JsonBuffer pointsBuffer = new JsonBuffer(); + private final List meta; + private final List points; // one way false to true private volatile boolean incomplete = false; JsonBased(JsonSender sender) { this.sender = sender; + this.meta = new ArrayList<>(); + this.points = new ArrayList<>(); } @Override public void initMetaInfo(String attr, String value) { - synchronized (metaBuffer) { - metaBuffer.name(attr).value(value); + synchronized (this.meta) { + this.meta.add(attr); + this.meta.add(value); } } @Override public void onAbort(String reasonCode) { onPoint("library_entrypoint.abort", "reason:" + reasonCode); - markIncomplete(); } @@ -117,7 +121,6 @@ public void onError(Throwable t) { @Override public void onFatalError(Throwable t) { onError(t); - markIncomplete(); } @@ -126,62 +129,48 @@ public void onError(String reasonCode) { onPoint("library_entrypoint.error", "error_type:" + reasonCode); } - @Override - public void markIncomplete() { - incomplete = true; - } - - void onPoint(String pointName) { - synchronized (pointsBuffer) { - pointsBuffer.beginObject(); - pointsBuffer.name("name").value(pointName); - pointsBuffer.endObject(); + private void onPoint(String name, String tag) { + synchronized (this.points) { + this.points.add(name); + this.points.add(tag); } } - void onPoint(String pointName, String tag) { - synchronized (pointsBuffer) { - pointsBuffer.beginObject(); - pointsBuffer.name("name").value(pointName); - pointsBuffer.name("tags").array(tag); - pointsBuffer.endObject(); - } - } - - void onPoint(String pointName, String[] tags) { - synchronized (pointsBuffer) { - pointsBuffer.beginObject(); - pointsBuffer.name("name").value(pointName); - pointsBuffer.name("tags").array(tags); - pointsBuffer.endObject(); - } + @Override + public void markIncomplete() { + this.incomplete = true; } @Override public void finish() { - if (!incomplete) { - onPoint("library_entrypoint.complete"); - } - - JsonBuffer buffer = new JsonBuffer(); - buffer.beginObject(); - - buffer.name("metadata"); - synchronized (metaBuffer) { - buffer.object(metaBuffer); - } - - buffer.name("points"); - synchronized (pointsBuffer) { - buffer.array(pointsBuffer); - - pointsBuffer.reset(); - } - - buffer.endObject(); - - try { - sender.send(buffer); + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject(); + writer.name("metadata").beginObject(); + synchronized (this.meta) { + for (int i = 0; i + 1 < this.meta.size(); i = i + 2) { + writer.name(this.meta.get(i)); + writer.value(this.meta.get(i + 1)); + } + } + writer.endObject(); + + writer.name("points").beginArray(); + synchronized (this.points) { + for (int i = 0; i + 1 < this.points.size(); i = i + 2) { + writer.beginObject(); + writer.name("name").value(this.points.get(i)); + writer.name("tags").beginArray().value(this.points.get(i + 1)).endArray(); + writer.endObject(); + } + this.points.clear(); + } + if (!this.incomplete) { + writer.beginObject().name("name").value("library_entrypoint.complete").endObject(); + } + writer.endArray(); + writer.endObject(); + + this.sender.send(writer.toByteArray()); } catch (Throwable t) { // Since this is the reporting mechanism, there's little recourse here // Decided to simply ignore - arguably might want to write to stderr @@ -189,8 +178,8 @@ public void finish() { } } - public static interface JsonSender { - public abstract void send(JsonBuffer buffer) throws IOException; + public interface JsonSender { + void send(byte[] payload) throws IOException; } public static final class ForwarderJsonSender implements JsonSender { @@ -201,12 +190,12 @@ public static final class ForwarderJsonSender implements JsonSender { } @Override - public void send(JsonBuffer buffer) throws IOException { + public void send(byte[] payload) throws IOException { ProcessBuilder builder = new ProcessBuilder(forwarderPath, "library_entrypoint"); Process process = builder.start(); try (OutputStream out = process.getOutputStream()) { - out.write(buffer.toByteArray()); + out.write(payload); } try { diff --git a/dd-java-agent/src/main/java/datadog/trace/bootstrap/JsonBuffer.java b/dd-java-agent/src/main/java/datadog/trace/bootstrap/JsonBuffer.java deleted file mode 100644 index 003007ab0b8..00000000000 --- a/dd-java-agent/src/main/java/datadog/trace/bootstrap/JsonBuffer.java +++ /dev/null @@ -1,264 +0,0 @@ -package datadog.trace.bootstrap; - -import java.io.ByteArrayOutputStream; -import java.io.Flushable; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; - -/** - * Light weight JSON writer with no dependencies other than JDK. Loosely modeled after GSON - * JsonWriter - */ -public final class JsonBuffer implements Flushable { - private ByteArrayOutputStream bytesOut; - private OutputStreamWriter writer; - - private byte[] cachedBytes = null; - private boolean requireComma = false; - - public JsonBuffer() { - this.reset(); - } - - public void reset() { - bytesOut = new ByteArrayOutputStream(); - writer = new OutputStreamWriter(bytesOut, Charset.forName("utf-8")); - - cachedBytes = null; - requireComma = false; - } - - public JsonBuffer beginObject() { - injectCommaIfNeeded(); - - return write('{'); - } - - public JsonBuffer endObject() { - endsValue(); - - return write('}'); - } - - public JsonBuffer object(JsonBuffer objectContents) { - beginObject(); - writeBytesRaw(objectContents.toByteArray()); - endObject(); - - return this; - } - - public JsonBuffer name(String name) { - injectCommaIfNeeded(); - - return writeStringLiteral(name).write(':'); - } - - public JsonBuffer nullValue() { - injectCommaIfNeeded(); - endsValue(); - - return writeStringRaw("null"); - } - - public JsonBuffer value(JsonBuffer buffer) { - injectCommaIfNeeded(); - endsValue(); - - return writeBytesRaw(buffer.toByteArray()); - } - - public JsonBuffer value(boolean value) { - injectCommaIfNeeded(); - endsValue(); - - return writeStringRaw(value ? "true" : "false"); - } - - public JsonBuffer value(String value) { - injectCommaIfNeeded(); - endsValue(); - - return writeStringLiteral(value); - } - - public JsonBuffer value(int value) { - injectCommaIfNeeded(); - endsValue(); - - return writeStringRaw(Integer.toString(value)); - } - - public JsonBuffer beginArray() { - injectCommaIfNeeded(); - - return write('['); - } - - public JsonBuffer endArray() { - endsValue(); - - return write(']'); - } - - public JsonBuffer array(String element) { - beginArray(); - value(element); - endArray(); - - return this; - } - - public JsonBuffer array(String[] elements) { - beginArray(); - for (String e : elements) { - value(e); - } - endArray(); - - return this; - } - - public JsonBuffer array(JsonBuffer arrayContents) { - beginArray(); - writeBytesRaw(arrayContents.toByteArray()); - endArray(); - - return this; - } - - public void flush() { - try { - writer.flush(); - } catch (IOException e) { - // ignore - } - } - - public byte[] toByteArray() { - byte[] cachedBytes = this.cachedBytes; - if (cachedBytes != null) { - return cachedBytes; - } - - flush(); - - cachedBytes = bytesOut.toByteArray(); - this.cachedBytes = cachedBytes; - return cachedBytes; - } - - void injectCommaIfNeeded() { - if (requireComma) { - write(','); - } - requireComma = false; - } - - void endsValue() { - requireComma = true; - } - - void clearBytesCache() { - cachedBytes = null; - } - - private JsonBuffer write(char ch) { - clearBytesCache(); - - try { - writer.write(ch); - } catch (IOException e) { - // ignore - } - return this; - } - - private JsonBuffer writeStringLiteral(String str) { - clearBytesCache(); - - try { - writer.write('"'); - - for (int i = 0; i < str.length(); ++i) { - char ch = str.charAt(i); - - // Based on https://keploy.io/blog/community/json-escape-and-unescape - switch (ch) { - case '"': - writer.write("\\\""); - break; - - case '\\': - writer.write("\\\\"); - break; - - case '/': - writer.write("\\/"); - break; - - case '\b': - writer.write("\\b"); - break; - - case '\f': - writer.write("\\f"); - break; - - case '\n': - writer.write("\\n"); - break; - - case '\r': - writer.write("\\r"); - break; - - case '\t': - writer.write("\\t"); - break; - - default: - writer.write(ch); - break; - } - } - - writer.write('"'); - } catch (IOException e) { - // ignore - } - - return this; - } - - private JsonBuffer writeStringRaw(String str) { - clearBytesCache(); - - try { - writer.write(str); - } catch (IOException e) { - // ignore - } - return this; - } - - private JsonBuffer writeBytesRaw(byte[] bytes) { - clearBytesCache(); - - try { - writer.flush(); - - bytesOut.write(bytes); - } catch (IOException e) { - // ignore - } - return this; - } - - @Override - public String toString() { - return new String(this.toByteArray(), StandardCharsets.UTF_8); - } -} diff --git a/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/BootstrapInitializationTelemetryTest.groovy b/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/BootstrapInitializationTelemetryTest.groovy index 1116a068298..da0dafa6d4a 100644 --- a/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/BootstrapInitializationTelemetryTest.groovy +++ b/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/BootstrapInitializationTelemetryTest.groovy @@ -1,9 +1,9 @@ -package datadog.trace.agent +package datadog.trace.bootstrap -import datadog.trace.bootstrap.BootstrapInitializationTelemetry -import datadog.trace.bootstrap.JsonBuffer import spock.lang.Specification +import static java.nio.charset.StandardCharsets.UTF_8 + class BootstrapInitializationTelemetryTest extends Specification { def initTelemetry, capture @@ -77,14 +77,14 @@ class BootstrapInitializationTelemetryTest extends Specification { } static class Capture implements BootstrapInitializationTelemetry.JsonSender { - JsonBuffer buffer + String json - void send(JsonBuffer buffer) { - this.buffer = buffer + void send(byte[] payload) { + this.json = new String(payload, UTF_8) } String json() { - return this.buffer.toString() + return this.json } } } diff --git a/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/JsonBufferTest.groovy b/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/JsonBufferTest.groovy deleted file mode 100644 index 8ff26b88c21..00000000000 --- a/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/JsonBufferTest.groovy +++ /dev/null @@ -1,126 +0,0 @@ -package datadog.trace.agent - -import datadog.trace.bootstrap.JsonBuffer -import spock.lang.Specification - -class JsonBufferTest extends Specification { - def "object"() { - when: - def jsonBuffer = new JsonBuffer() - jsonBuffer.beginObject() - jsonBuffer.name("foo").value("bar") - jsonBuffer.name("pi").value(3_142) - jsonBuffer.name("true").value(true) - jsonBuffer.name("false").value(false) - jsonBuffer.endObject() - - then: - jsonBuffer.toString() == '{"foo":"bar","pi":3142,"true":true,"false":false}' - } - - def "array"() { - when: - def jsonBuffer = new JsonBuffer() - jsonBuffer.beginArray() - jsonBuffer.value("foo") - jsonBuffer.value("baz") - jsonBuffer.value("bar") - jsonBuffer.value("quux") - jsonBuffer.endArray() - - then: - jsonBuffer.toString() == '["foo","baz","bar","quux"]' - } - - def "escaping"() { - when: - def jsonBuffer = new JsonBuffer() - jsonBuffer.beginArray() - jsonBuffer.value('"') - jsonBuffer.value("\\") - jsonBuffer.value("/") - jsonBuffer.value("\b") - jsonBuffer.value("\f") - jsonBuffer.value("\n") - jsonBuffer.value("\r") - jsonBuffer.value("\t") - jsonBuffer.endArray() - - then: - jsonBuffer.toString() == '["\\"","\\\\","\\/","\\b","\\f","\\n","\\r","\\t"]' - } - - def "nesting array in object"() { - when: - def jsonBuffer = new JsonBuffer() - jsonBuffer.beginObject() - jsonBuffer.name("array") - jsonBuffer.beginArray() - jsonBuffer.value("true") - jsonBuffer.value("false") - jsonBuffer.endArray() - jsonBuffer.endObject() - - then: - jsonBuffer.toString() == '{"array":["true","false"]}' - } - - def "nesting object in array"() { - when: - def jsonBuffer = new JsonBuffer() - jsonBuffer.beginArray() - jsonBuffer.beginObject() - jsonBuffer.name("true").value(true) - jsonBuffer.endObject() - jsonBuffer.beginObject() - jsonBuffer.name("false").value(false) - jsonBuffer.endObject() - jsonBuffer.endArray() - - then: - jsonBuffer.toString() == '[{"true":true},{"false":false}]' - } - - def "partial object buffer"() { - when: - def partialJsonBuffer = new JsonBuffer() - partialJsonBuffer.name("foo").value("bar") - partialJsonBuffer.name("quux").value("baz") - - def jsonBuffer = new JsonBuffer() - jsonBuffer.beginObject() - jsonBuffer.name("partial").object(partialJsonBuffer) - jsonBuffer.endObject() - - then: - jsonBuffer.toString() == '{"partial":{"foo":"bar","quux":"baz"}}' - } - - def "partial array buffer"() { - when: - def partialJsonBuffer = new JsonBuffer() - partialJsonBuffer.value("foo") - partialJsonBuffer.value("bar") - - def jsonBuffer = new JsonBuffer() - jsonBuffer.beginObject() - jsonBuffer.name("partial").array(partialJsonBuffer) - jsonBuffer.endObject() - - then: - jsonBuffer.toString() == '{"partial":["foo","bar"]}' - } - - def "reset"() { - when: - def jsonBuffer = new JsonBuffer() - jsonBuffer.name("foo").value("quux") - - jsonBuffer.reset() - - jsonBuffer.array("bar", "baz") - - then: - jsonBuffer.toString() == '["bar","baz"]' - } -} diff --git a/dd-trace-core/build.gradle b/dd-trace-core/build.gradle index 4abfb557d16..f19c0f3935d 100644 --- a/dd-trace-core/build.gradle +++ b/dd-trace-core/build.gradle @@ -61,6 +61,7 @@ dependencies { api project(':dd-trace-api') api project(':communication') api project(':internal-api') + implementation project(':components:json') implementation project(':utils:container-utils') implementation project(':utils:socket-utils') // for span exception debugging diff --git a/dd-trace-core/src/main/java/datadog/trace/civisibility/writer/ddintake/CiTestCycleMapperV1.java b/dd-trace-core/src/main/java/datadog/trace/civisibility/writer/ddintake/CiTestCycleMapperV1.java index 7d1ec19b786..b2ed0bffebf 100644 --- a/dd-trace-core/src/main/java/datadog/trace/civisibility/writer/ddintake/CiTestCycleMapperV1.java +++ b/dd-trace-core/src/main/java/datadog/trace/civisibility/writer/ddintake/CiTestCycleMapperV1.java @@ -2,6 +2,7 @@ import static datadog.communication.http.OkHttpUtils.gzippedMsgpackRequestBodyOf; import static datadog.communication.http.OkHttpUtils.msgpackRequestBodyOf; +import static datadog.json.JsonMapper.toJson; import datadog.communication.serialization.GrowableBuffer; import datadog.communication.serialization.Writable; @@ -20,7 +21,6 @@ import datadog.trace.core.CoreSpan; import datadog.trace.core.Metadata; import datadog.trace.core.MetadataConsumer; -import datadog.trace.util.Strings; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.WritableByteChannel; @@ -357,7 +357,7 @@ public void accept(Metadata metadata) { if (!(value instanceof Iterable)) { writable.writeObjectString(value, null); } else { - String serializedValue = Strings.toJson((Iterable) value); + String serializedValue = toJson((Collection) value); writable.writeString(serializedValue, null); } } diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 54de0d30daa..42c12af3cea 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -15,6 +15,7 @@ final class CachedData { exclude(project(':internal-api')) exclude(project(':internal-api:internal-api-9')) exclude(project(':communication')) + exclude(project(':components:json')) exclude(project(':remote-config:remote-config-api')) exclude(project(':remote-config:remote-config-core')) exclude(project(':telemetry')) diff --git a/internal-api/src/main/java/datadog/trace/util/Strings.java b/internal-api/src/main/java/datadog/trace/util/Strings.java index 6715604260f..2fea0e107e5 100644 --- a/internal-api/src/main/java/datadog/trace/util/Strings.java +++ b/internal-api/src/main/java/datadog/trace/util/Strings.java @@ -5,10 +5,6 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Iterator; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; import java.util.concurrent.ThreadLocalRandom; import javax.annotation.Nonnull; @@ -18,70 +14,6 @@ public final class Strings { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; - public static String escapeToJson(String string) { - if (string == null || string.isEmpty()) { - return ""; - } - - final StringBuilder sb = new StringBuilder(); - int sz = string.length(); - for (int i = 0; i < sz; ++i) { - char ch = string.charAt(i); - if (ch > 4095) { - sb.append("\\u").append(hex(ch)); - } else if (ch > 255) { - sb.append("\\u0").append(hex(ch)); - } else if (ch > 127) { - sb.append("\\u00").append(hex(ch)); - } else if (ch < ' ') { - switch (ch) { - case '\b': - sb.append((char) 92).append((char) 98); - break; - case '\t': - sb.append((char) 92).append((char) 116); - break; - case '\n': - sb.append((char) 92).append((char) 110); - break; - case '\u000b': - default: - if (ch > 15) { - sb.append("\\u00").append(hex(ch)); - } else { - sb.append("\\u000").append(hex(ch)); - } - break; - case '\f': - sb.append((char) 92).append((char) 102); - break; - case '\r': - sb.append((char) 92).append((char) 114); - break; - } - } else { - switch (ch) { - case '"': - sb.append((char) 92).append((char) 34); - break; - case '\'': - sb.append((char) 92).append((char) 39); - break; - case '/': - sb.append((char) 92).append((char) 47); - break; - case '\\': - sb.append((char) 92).append((char) 92); - break; - default: - sb.append(ch); - } - } - } - - return sb.toString(); - } - public static String toEnvVar(String string) { return string.replace('.', '_').replace('-', '_').toUpperCase(); } @@ -209,16 +141,12 @@ public static String trim(final String string) { return null == string ? "" : string.trim(); } - private static String hex(char ch) { - return Integer.toHexString(ch).toUpperCase(Locale.ENGLISH); - } - public static String sha256(String input) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder hexString = new StringBuilder(2 * hash.length); - for (int i = 0; i < hash.length; i++) { - String hex = Integer.toHexString(0xFF & hash[i]); + for (byte b : hash) { + String hex = Integer.toHexString(0xFF & b); if (hex.length() == 1) { hexString.append('0'); } @@ -238,52 +166,6 @@ public static CharSequence truncate(CharSequence input, int limit) { return input.subSequence(0, limit); } - public static String toJson(final Map map) { - return toJson(map, false); - } - - public static String toJson(final Map map, boolean valuesAreJson) { - if (map == null || map.isEmpty()) { - return "{}"; - } - final StringBuilder sb = new StringBuilder("{"); - final Iterator> entriesIter = map.entrySet().iterator(); - while (entriesIter.hasNext()) { - final Entry entry = entriesIter.next(); - - sb.append('\"').append(escapeToJson(entry.getKey())).append("\":"); - - if (valuesAreJson) { - sb.append(entry.getValue()); - } else { - sb.append('\"').append(escapeToJson(String.valueOf(entry.getValue()))).append('\"'); - } - - if (entriesIter.hasNext()) { - sb.append(','); - } - } - sb.append('}'); - return sb.toString(); - } - - public static String toJson(final Iterable items) { - if (items == null) { - return "[]"; - } - StringBuilder json = new StringBuilder("["); - Iterator it = items.iterator(); - while (it.hasNext()) { - String item = it.next(); - json.append('"').append(escapeToJson(item)).append('"'); - if (it.hasNext()) { - json.append(','); - } - } - json.append(']'); - return json.toString(); - } - /** * Checks that a string is not blank, i.e. contains at least one character that is not a * whitespace diff --git a/internal-api/src/test/groovy/datadog/trace/util/StringsTest.groovy b/internal-api/src/test/groovy/datadog/trace/util/StringsTest.groovy index 503922c724f..b7958191c8d 100644 --- a/internal-api/src/test/groovy/datadog/trace/util/StringsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/util/StringsTest.groovy @@ -88,33 +88,6 @@ class StringsTest extends DDSpecification { // spotless:on } - def "test escape javascript"() { - when: - String escaped = Strings.escapeToJson(string) - - then: - escaped == expected - - where: - string | expected - null | "" - "" | "" - ((char) 4096).toString() | '\\u1000' - ((char) 256).toString() | '\\u0100' - ((char) 128).toString() | '\\u0080' - "\b" | "\\b" - "\t" | "\\t" - "\n" | "\\n" - "\f" | "\\f" - "\r" | "\\r" - '"' | '\\"' - '\'' | '\\\'' - '/' | '\\/' - '\\' | '\\\\' - "\u000b" | "\\u000B" - "a" | "a" - } - def "test sha256"() { when: String sha256 = Strings.sha256(input) @@ -144,38 +117,6 @@ class StringsTest extends DDSpecification { "hélló wórld" | 5 | "hélló" } - def "test map toJson: #input"() { - when: - String json = Strings.toJson((Map) input) - - then: - json == expected - - where: - input | expected - null | "{}" - new HashMap<>() | "{}" - ['key1': 'value1'] | "{\"key1\":\"value1\"}" - ['key1': 'value1', 'key2': 'value2'] | "{\"key1\":\"value1\",\"key2\":\"value2\"}" - ['key1': 'va"lu"e1', 'ke"y2': 'value2'] | "{\"key1\":\"va\\\"lu\\\"e1\",\"ke\\\"y2\":\"value2\"}" - } - - def "test iterable toJson: #input"() { - when: - String json = Strings.toJson((Iterable) input) - - then: - json == expected - - where: - input | expected - null | "[]" - new ArrayList<>() | "[]" - ['value1'] | "[\"value1\"]" - ['value1', 'value2'] | "[\"value1\",\"value2\"]" - ['va"lu"e1', 'value2'] | "[\"va\\\"lu\\\"e1\",\"value2\"]" - } - def "test isNotBlank: #input"() { when: def notBlank = Strings.isNotBlank(input) diff --git a/settings.gradle b/settings.gradle index fb2bb69f846..0422e36fce6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -66,6 +66,7 @@ include ':dd-java-agent:agent-otel:otel-shim' include ':dd-java-agent:agent-otel:otel-tooling' include ':communication' +include ':components:json' include ':telemetry' include ':remote-config:remote-config-api' include ':remote-config:remote-config-core'