From 1776ab7e4cc4fce3fa9a12bd6b8cbcfa8abdfdb9 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Thu, 21 Nov 2024 17:15:09 +0100 Subject: [PATCH] feat(bootstrap): Migrate to JSON component --- dd-java-agent/build.gradle | 1 + .../BootstrapInitializationTelemetry.java | 113 ++++---- .../datadog/trace/bootstrap/JsonBuffer.java | 264 ------------------ ...ootstrapInitializationTelemetryTest.groovy | 14 +- .../trace/bootstrap/JsonBufferTest.groovy | 126 --------- 5 files changed, 59 insertions(+), 459 deletions(-) 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/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/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"]' - } -}