From 64d3c2bc23ad2b3069d71aac8811bed8c5ad79e4 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Thu, 17 Jul 2025 15:49:42 -0400 Subject: [PATCH 1/4] Move JSON generation to sender thread to improve startup time. --- .../BootstrapInitializationTelemetry.java | 135 +++++++++--------- ...ootstrapInitializationTelemetryTest.groovy | 112 +++++++-------- 2 files changed, 114 insertions(+), 133 deletions(-) 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 391436c549d..5c375c83609 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 @@ -6,7 +6,7 @@ import java.io.Closeable; import java.io.OutputStream; import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -51,14 +51,14 @@ public static BootstrapInitializationTelemetry createFromForwarderPath(String fo */ public abstract void onError(Throwable t); + public abstract void onError(String reasonCode); + /** * Indicates an exception that occurred during the bootstrapping process that left initialization * incomplete. Equivalent to calling {@link #onError(Throwable)} and {@link #markIncomplete()} */ public abstract void onFatalError(Throwable t); - public abstract void onError(String reasonCode); - public abstract void markIncomplete(); public abstract void finish(); @@ -78,10 +78,10 @@ public void onAbort(String reasonCode) {} public void onError(String reasonCode) {} @Override - public void onFatalError(Throwable t) {} + public void onError(Throwable t) {} @Override - public void onError(Throwable t) {} + public void onFatalError(Throwable t) {} @Override public void markIncomplete() {} @@ -93,24 +93,24 @@ public void finish() {} public static final class JsonBased extends BootstrapInitializationTelemetry { private final JsonSender sender; - private final List meta; + private final Map meta; private final Map> points; // one way false to true private volatile boolean incomplete = false; - private volatile boolean error = false; JsonBased(JsonSender sender) { this.sender = sender; - this.meta = new ArrayList<>(); + this.meta = new LinkedHashMap<>(); this.points = new LinkedHashMap<>(); + + setMetaInfo("success", "success", "Successfully configured ddtrace package"); } @Override public void initMetaInfo(String attr, String value) { synchronized (this.meta) { - this.meta.add(attr); - this.meta.add(value); + this.meta.put(attr, value); } } @@ -123,7 +123,6 @@ public void onAbort(String reasonCode) { @Override public void onError(Throwable t) { - error = true; setMetaInfo("error", "internal_error", t.getMessage()); List causes = new ArrayList<>(); @@ -142,7 +141,13 @@ public void onError(Throwable t) { causes = causes.subList(numCauses - maxTags, numCauses); } - onPoint("library_entrypoint.error", causes); + onPoint("library_entrypoint.error", causes.toArray(new String[0])); + } + + @Override + public void onError(String reasonCode) { + onPoint("library_entrypoint.error", "error_type:" + reasonCode); + setMetaInfo("error", mapResultClass(reasonCode), reasonCode); } private int maxTags() { @@ -165,13 +170,6 @@ public void onFatalError(Throwable t) { markIncomplete(); } - @Override - public void onError(String reasonCode) { - error = true; - onPoint("library_entrypoint.error", "error_type:" + reasonCode); - setMetaInfo("error", mapResultClass(reasonCode), reasonCode); - } - private void setMetaInfo(String result, String resultClass, String resultReason) { initMetaInfo("result", result); initMetaInfo("result_class", resultClass); @@ -195,13 +193,9 @@ private String mapResultClass(String reasonCode) { } } - private void onPoint(String name, String tag) { - onPoint(name, Collections.singletonList(tag)); - } - - private void onPoint(String name, List tags) { + private void onPoint(String name, String... tags) { synchronized (this.points) { - this.points.put(name, tags); + this.points.put(name, Arrays.asList(tags)); } } @@ -212,51 +206,16 @@ public void markIncomplete() { @Override public void finish() { - if (!this.incomplete && !this.error) { - setMetaInfo("success", "success", "Successfully configured ddtrace package"); + if (!this.incomplete) { + onPoint("library_entrypoint.complete"); } - 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 (Map.Entry> entry : points.entrySet()) { - writer.beginObject(); - writer.name("name").value(entry.getKey()); - writer.name("tags").beginArray(); - for (String tag : entry.getValue()) { - writer.value(tag); - } - writer.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 - } + this.sender.send(meta, points); } } public interface JsonSender { - void send(byte[] payload); + void send(Map meta, Map> points); } public static final class ForwarderJsonSender implements JsonSender { @@ -267,8 +226,8 @@ public static final class ForwarderJsonSender implements JsonSender { } @Override - public void send(byte[] payload) { - ForwarderJsonSenderThread t = new ForwarderJsonSenderThread(forwarderPath, payload); + public void send(Map meta, Map> points) { + ForwarderJsonSenderThread t = new ForwarderJsonSenderThread(forwarderPath, meta, points); t.setDaemon(true); t.start(); } @@ -276,12 +235,15 @@ public void send(byte[] payload) { public static final class ForwarderJsonSenderThread extends Thread { private final String forwarderPath; - private final byte[] payload; + private final Map meta; + private final Map> points; - public ForwarderJsonSenderThread(String forwarderPath, byte[] payload) { + public ForwarderJsonSenderThread( + String forwarderPath, Map meta, Map> points) { super("dd-forwarder-json-sender"); this.forwarderPath = forwarderPath; - this.payload = payload; + this.meta = meta; + this.points = points; } @SuppressForbidden @@ -291,6 +253,8 @@ public void run() { // Run forwarder and mute tracing for subprocesses executed in by dd-java-agent. try (final Closeable ignored = muteTracing()) { + byte[] payload = json(); + Process process = builder.start(); try (OutputStream out = process.getOutputStream()) { out.write(payload); @@ -301,6 +265,39 @@ public void run() { } } + private byte[] json() { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject(); + writer.name("metadata").beginObject(); + synchronized (this.meta) { + for (Map.Entry entry : meta.entrySet()) { + writer.name(entry.getKey()); + writer.value(entry.getValue()); + } + } + writer.endObject(); + + writer.name("points").beginArray(); + synchronized (this.points) { + for (Map.Entry> entry : points.entrySet()) { + writer.beginObject(); + writer.name("name").value(entry.getKey()); + writer.name("tags").beginArray(); + for (String tag : entry.getValue()) { + writer.value(tag); + } + writer.endArray(); + writer.endObject(); + } + this.points.clear(); + } + writer.endArray(); + writer.endObject(); + + return writer.toByteArray(); + } + } + @SuppressForbidden private Closeable muteTracing() { try { 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 ac62d4c7ff1..cd586af3cd4 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,56 +1,56 @@ package datadog.trace.bootstrap -import groovy.json.JsonBuilder import spock.lang.Specification -import static java.nio.charset.StandardCharsets.UTF_8 - class BootstrapInitializationTelemetryTest extends Specification { - def initTelemetry, capture + Capture capture + def initTelemetry def setup() { - def capture = new Capture() - def initTelemetry = new BootstrapInitializationTelemetry.JsonBased(capture) + capture = new Capture() // There's an annoying interaction between our bootstrap injection // and the GroovyClassLoader class resolution. Groovy resolves the import // against the application ClassLoader, but when a method invocation // happens it resolves the invocation against the bootstrap classloader. - + // // To side step this problem, put a Groovy Proxy around the object under test - // codeNarc was incorrectly flagging "import groovy.util.Proxy" as unnecessary, // since groovy.util is imported implicitly. However, java.util is also // implicitly imported and also contains a Proxy class, so need to use the // full name inline to disambiguate and pass codeNarc. def initTelemetryProxy = new groovy.util.Proxy() - initTelemetryProxy.setAdaptee(initTelemetry) - + initTelemetryProxy.setAdaptee(new BootstrapInitializationTelemetry.JsonBased(capture)) this.initTelemetry = initTelemetryProxy - this.initTelemetry.initMetaInfo("runtime_name", "java") - this.initTelemetry.initMetaInfo("runtime_version", "1.8.0_382") - this.capture = capture } - def "test success"() { + def "test happy path"() { when: initTelemetry.finish() then: - capture.json() == json("success", "success", "Successfully configured ddtrace package", - [[name: "library_entrypoint.complete"]]) + assertMeta("success", "success", "Successfully configured ddtrace package") + assertPoints(true, "library_entrypoint.complete", []) } - def "real example"() { + def "test non fatal error as text"() { when: - initTelemetry.onError(new Exception("foo")) + initTelemetry.onError("some reason") initTelemetry.finish() then: - capture.json() == json("error", "internal_error", "foo", [ - [name: "library_entrypoint.error", tags: ["error_type:java.lang.Exception"]], - [name: "library_entrypoint.complete"] - ]) + assertMeta("error", "unknown", "some reason") + assertPoints(true, "library_entrypoint.error", ["error_type:some reason"]) + } + + def "test non fatal error as exception"() { + when: + initTelemetry.onError(new Exception("non fatal error")) + initTelemetry.finish() + + then: + assertMeta("error", "internal_error", "non fatal error") + assertPoints(true, "library_entrypoint.error", ["error_type:java.lang.Exception"]) } def "test abort"() { @@ -59,8 +59,8 @@ class BootstrapInitializationTelemetryTest extends Specification { initTelemetry.finish() then: - capture.json() == json("abort", resultClass, reasonCode, - [[name: "library_entrypoint.abort", tags: ["reason:${reasonCode}"]]]) + assertMeta("abort", resultClass, reasonCode) + assertPoints(false, "library_entrypoint.abort", ["reason:${reasonCode}"]) where: reasonCode | resultClass @@ -70,68 +70,52 @@ class BootstrapInitializationTelemetryTest extends Specification { "foo" | "unknown" } - def "trivial completion check"() { + def "test fatal error"() { when: + initTelemetry.onFatalError(new Exception("fatal error")) initTelemetry.finish() then: - capture.json().contains("library_entrypoint.complete") + assertMeta("error", "internal_error", "fatal error") + assertPoints(false, "library_entrypoint.error", ["error_type:java.lang.Exception"]) } - def "trivial incomplete check"() { + def "test unwind root cause"() { when: - initTelemetry.markIncomplete() + initTelemetry.onError(new Exception("top cause", new FileNotFoundException("root cause"))) initTelemetry.finish() then: - !capture.json().contains("library_entrypoint.complete") + assertMeta("error", "internal_error", "top cause") + assertPoints(true, "library_entrypoint.error", ["error_type:java.io.FileNotFoundException", "error_type:java.lang.Exception"]) } - def "incomplete on fatal error"() { - when: - initTelemetry.onFatalError(new Exception("foo")) - initTelemetry.finish() - - then: - !capture.json().contains("library_entrypoint.complete") - capture.json() == json("error", "internal_error", "foo", - [[name: "library_entrypoint.error", tags: ["error_type:java.lang.Exception"]]]) - } + def assertMeta(String result, String resultClass, String resultReason) { + def meta = capture.meta - def "incomplete on abort"() { - when: - initTelemetry.onAbort("reason") - initTelemetry.finish() + assert meta.get("result") == result + assert meta.get("result_class") == resultClass + assert meta.get("result_reason") == resultReason - then: - !capture.json().contains("library_entrypoint.complete") + return true } - def "unwind root cause"() { - when: - initTelemetry.onError(new Exception("top cause", new FileNotFoundException("root cause"))) - initTelemetry.finish() + def assertPoints(boolean complete, String point, List tags) { + def points = capture.points - then: - capture.json() == json("error", "internal_error", "top cause", [ - [name: "library_entrypoint.error", tags: ["error_type:java.io.FileNotFoundException", "error_type:java.lang.Exception"]], - [name: "library_entrypoint.complete"] - ]) - } + assert points.containsKey("library_entrypoint.complete") == complete + assert points.get(point) == tags - private static String json(String result, String resultClass, String resultReason, List points) { - return """{"metadata":{"runtime_name":"java","runtime_version":"1.8.0_382","result":"${result}","result_class":"${resultClass}","result_reason":"${resultReason}"},"points":${new JsonBuilder(points)}}""" + return true } static class Capture implements BootstrapInitializationTelemetry.JsonSender { - String json - - void send(byte[] payload) { - this.json = new String(payload, UTF_8) - } + Map meta + Map> points - String json() { - return this.json + void send(Map meta, Map> points) { + this.meta = meta + this.points = points } } } From e8fa456689822d247586b2ea875d7be547f88830 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Mon, 21 Jul 2025 16:39:35 -0400 Subject: [PATCH 2/4] Refactored telemetry into separate class. Fixed review comments. --- .../BootstrapInitializationTelemetry.java | 160 ++++++++++-------- ...ootstrapInitializationTelemetryTest.groovy | 61 ++++--- 2 files changed, 124 insertions(+), 97 deletions(-) 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 5c375c83609..b0dc8e0dc54 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,12 +1,14 @@ package datadog.trace.bootstrap; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + import datadog.json.JsonWriter; import datadog.trace.bootstrap.environment.EnvironmentVariables; import de.thetaphi.forbiddenapis.SuppressForbidden; import java.io.Closeable; import java.io.OutputStream; import java.util.ArrayList; -import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -93,37 +95,31 @@ public void finish() {} public static final class JsonBased extends BootstrapInitializationTelemetry { private final JsonSender sender; - private final Map meta; - private final Map> points; + private final Telemetry telemetry; // one way false to true private volatile boolean incomplete = false; JsonBased(JsonSender sender) { this.sender = sender; - this.meta = new LinkedHashMap<>(); - this.points = new LinkedHashMap<>(); - - setMetaInfo("success", "success", "Successfully configured ddtrace package"); + this.telemetry = new Telemetry(); } @Override public void initMetaInfo(String attr, String value) { - synchronized (this.meta) { - this.meta.put(attr, value); - } + telemetry.setMetadata(attr, value); } @Override public void onAbort(String reasonCode) { - onPoint("library_entrypoint.abort", "reason:" + reasonCode); + onPoint("library_entrypoint.abort", singletonList("reason:" + reasonCode)); markIncomplete(); - setMetaInfo("abort", mapResultClass(reasonCode), reasonCode); + setResultMeta("abort", mapResultClass(reasonCode), reasonCode); } @Override public void onError(Throwable t) { - setMetaInfo("error", "internal_error", t.getMessage()); + setResultMeta("error", "internal_error", t.getMessage()); List causes = new ArrayList<>(); @@ -141,13 +137,13 @@ public void onError(Throwable t) { causes = causes.subList(numCauses - maxTags, numCauses); } - onPoint("library_entrypoint.error", causes.toArray(new String[0])); + onPoint("library_entrypoint.error", causes); } @Override public void onError(String reasonCode) { - onPoint("library_entrypoint.error", "error_type:" + reasonCode); - setMetaInfo("error", mapResultClass(reasonCode), reasonCode); + onPoint("library_entrypoint.error", singletonList("error_type:" + reasonCode)); + setResultMeta("error", mapResultClass(reasonCode), reasonCode); } private int maxTags() { @@ -170,7 +166,7 @@ public void onFatalError(Throwable t) { markIncomplete(); } - private void setMetaInfo(String result, String resultClass, String resultReason) { + private void setResultMeta(String result, String resultClass, String resultReason) { initMetaInfo("result", result); initMetaInfo("result_class", resultClass); initMetaInfo("result_reason", resultReason); @@ -193,10 +189,8 @@ private String mapResultClass(String reasonCode) { } } - private void onPoint(String name, String... tags) { - synchronized (this.points) { - this.points.put(name, Arrays.asList(tags)); - } + private void onPoint(String name, List tags) { + telemetry.addPoint(name, tags); } @Override @@ -207,15 +201,85 @@ public void markIncomplete() { @Override public void finish() { if (!this.incomplete) { - onPoint("library_entrypoint.complete"); + onPoint("library_entrypoint.complete", emptyList()); } - this.sender.send(meta, points); + this.sender.send(telemetry); + } + } + + public static class Telemetry { + private final Map metadata; + private final Map> points; + + public Telemetry() { + metadata = new LinkedHashMap<>(); + points = new LinkedHashMap<>(); + + setResults("success", "success", "Successfully configured ddtrace package"); + } + + public void setMetadata(String name, String value) { + synchronized (metadata) { + metadata.put(name, value); + } + } + + public void setResults(String result, String resultClass, String resultReason) { + synchronized (metadata) { + metadata.put("result", result); + metadata.put("result_class", resultClass); + metadata.put("result_reason", resultReason); + } + } + + public void addPoint(String name, List tags) { + synchronized (points) { + points.put(name, tags); + } + } + + public byte[] json() { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject(); + writer.name("metadata").beginObject(); + synchronized (metadata) { + for (Map.Entry entry : metadata.entrySet()) { + writer.name(entry.getKey()); + writer.value(entry.getValue()); + } + + metadata.clear(); + } + writer.endObject(); + + writer.name("points").beginArray(); + synchronized (points) { + for (Map.Entry> entry : points.entrySet()) { + writer.beginObject(); + writer.name("name").value(entry.getKey()); + if (!entry.getValue().isEmpty()) { + writer.name("tags").beginArray(); + for (String tag : entry.getValue()) { + writer.value(tag); + } + writer.endArray(); + } + writer.endObject(); + } + + points.clear(); + } + writer.endArray(); + writer.endObject(); + + return writer.toByteArray(); + } } } public interface JsonSender { - void send(Map meta, Map> points); + void send(Telemetry telemetry); } public static final class ForwarderJsonSender implements JsonSender { @@ -226,8 +290,8 @@ public static final class ForwarderJsonSender implements JsonSender { } @Override - public void send(Map meta, Map> points) { - ForwarderJsonSenderThread t = new ForwarderJsonSenderThread(forwarderPath, meta, points); + public void send(Telemetry telemetry) { + ForwarderJsonSenderThread t = new ForwarderJsonSenderThread(forwarderPath, telemetry); t.setDaemon(true); t.start(); } @@ -235,15 +299,12 @@ public void send(Map meta, Map> points) { public static final class ForwarderJsonSenderThread extends Thread { private final String forwarderPath; - private final Map meta; - private final Map> points; + private final Telemetry telemetry; - public ForwarderJsonSenderThread( - String forwarderPath, Map meta, Map> points) { + public ForwarderJsonSenderThread(String forwarderPath, Telemetry telemetry) { super("dd-forwarder-json-sender"); this.forwarderPath = forwarderPath; - this.meta = meta; - this.points = points; + this.telemetry = telemetry; } @SuppressForbidden @@ -253,7 +314,7 @@ public void run() { // Run forwarder and mute tracing for subprocesses executed in by dd-java-agent. try (final Closeable ignored = muteTracing()) { - byte[] payload = json(); + byte[] payload = telemetry.json(); Process process = builder.start(); try (OutputStream out = process.getOutputStream()) { @@ -265,39 +326,6 @@ public void run() { } } - private byte[] json() { - try (JsonWriter writer = new JsonWriter()) { - writer.beginObject(); - writer.name("metadata").beginObject(); - synchronized (this.meta) { - for (Map.Entry entry : meta.entrySet()) { - writer.name(entry.getKey()); - writer.value(entry.getValue()); - } - } - writer.endObject(); - - writer.name("points").beginArray(); - synchronized (this.points) { - for (Map.Entry> entry : points.entrySet()) { - writer.beginObject(); - writer.name("name").value(entry.getKey()); - writer.name("tags").beginArray(); - for (String tag : entry.getValue()) { - writer.value(tag); - } - writer.endArray(); - writer.endObject(); - } - this.points.clear(); - } - writer.endArray(); - writer.endObject(); - - return writer.toByteArray(); - } - } - @SuppressForbidden private Closeable muteTracing() { try { 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 cd586af3cd4..89a0fd297a1 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,7 +1,11 @@ package datadog.trace.bootstrap +import datadog.trace.bootstrap.BootstrapInitializationTelemetry.Telemetry +import groovy.json.JsonBuilder import spock.lang.Specification +import static java.nio.charset.StandardCharsets.UTF_8 + class BootstrapInitializationTelemetryTest extends Specification { Capture capture def initTelemetry @@ -29,8 +33,7 @@ class BootstrapInitializationTelemetryTest extends Specification { initTelemetry.finish() then: - assertMeta("success", "success", "Successfully configured ddtrace package") - assertPoints(true, "library_entrypoint.complete", []) + assertJson("success", "success", "Successfully configured ddtrace package", [[name: "library_entrypoint.complete"]]) } def "test non fatal error as text"() { @@ -39,8 +42,10 @@ class BootstrapInitializationTelemetryTest extends Specification { initTelemetry.finish() then: - assertMeta("error", "unknown", "some reason") - assertPoints(true, "library_entrypoint.error", ["error_type:some reason"]) + assertJson("error", "unknown", "some reason", [ + [name: "library_entrypoint.error", tags: ["error_type:some reason"]], + [name: "library_entrypoint.complete"] + ]) } def "test non fatal error as exception"() { @@ -49,8 +54,10 @@ class BootstrapInitializationTelemetryTest extends Specification { initTelemetry.finish() then: - assertMeta("error", "internal_error", "non fatal error") - assertPoints(true, "library_entrypoint.error", ["error_type:java.lang.Exception"]) + assertJson("error", "internal_error", "non fatal error", [ + [name: "library_entrypoint.error", tags: ["error_type:java.lang.Exception"]], + [name: "library_entrypoint.complete"] + ]) } def "test abort"() { @@ -59,8 +66,7 @@ class BootstrapInitializationTelemetryTest extends Specification { initTelemetry.finish() then: - assertMeta("abort", resultClass, reasonCode) - assertPoints(false, "library_entrypoint.abort", ["reason:${reasonCode}"]) + assertJson("abort", resultClass, reasonCode, [[name: "library_entrypoint.abort", tags: ["reason:${reasonCode}"]]]) where: reasonCode | resultClass @@ -76,8 +82,7 @@ class BootstrapInitializationTelemetryTest extends Specification { initTelemetry.finish() then: - assertMeta("error", "internal_error", "fatal error") - assertPoints(false, "library_entrypoint.error", ["error_type:java.lang.Exception"]) + assertJson("error", "internal_error", "fatal error", [[name: "library_entrypoint.error", tags: ["error_type:java.lang.Exception"]]]) } def "test unwind root cause"() { @@ -86,36 +91,30 @@ class BootstrapInitializationTelemetryTest extends Specification { initTelemetry.finish() then: - assertMeta("error", "internal_error", "top cause") - assertPoints(true, "library_entrypoint.error", ["error_type:java.io.FileNotFoundException", "error_type:java.lang.Exception"]) - } - - def assertMeta(String result, String resultClass, String resultReason) { - def meta = capture.meta - - assert meta.get("result") == result - assert meta.get("result_class") == resultClass - assert meta.get("result_reason") == resultReason - - return true + assertJson("error", "internal_error", "top cause", [ + [name: "library_entrypoint.error", tags: ["error_type:java.io.FileNotFoundException", "error_type:java.lang.Exception"]], + [name: "library_entrypoint.complete"] + ]) } - def assertPoints(boolean complete, String point, List tags) { - def points = capture.points + private boolean assertJson(String result, String resultClass, String resultReason, List points) { + def expectedJson = """{"metadata":{"result":"${result}","result_class":"${resultClass}","result_reason":"${resultReason}"},"points":${new JsonBuilder(points)}}""" + def actualJson = capture.json() - assert points.containsKey("library_entrypoint.complete") == complete - assert points.get(point) == tags + assert expectedJson == actualJson return true } static class Capture implements BootstrapInitializationTelemetry.JsonSender { - Map meta - Map> points + Telemetry telemetry + + void send(Telemetry telemetry) { + this.telemetry = telemetry + } - void send(Map meta, Map> points) { - this.meta = meta - this.points = points + String json() { + return new String(telemetry.json(), UTF_8) } } } From 26c4c2303301a3c980d44a1a66a184d8c0c2fc32 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Wed, 23 Jul 2025 16:20:55 -0400 Subject: [PATCH 3/4] Fixed issue with double class loading. --- .../BootstrapInitializationTelemetry.java | 15 ++++++++------- .../BootstrapInitializationTelemetryTest.groovy | 9 +++------ 2 files changed, 11 insertions(+), 13 deletions(-) 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 b0dc8e0dc54..a060169395a 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 @@ -239,7 +239,8 @@ public void addPoint(String name, List tags) { } } - public byte[] json() { + @Override + public String toString() { try (JsonWriter writer = new JsonWriter()) { writer.beginObject(); writer.name("metadata").beginObject(); @@ -273,13 +274,13 @@ public byte[] json() { writer.endArray(); writer.endObject(); - return writer.toByteArray(); + return writer.toString(); } } } public interface JsonSender { - void send(Telemetry telemetry); + void send(Object telemetry); } public static final class ForwarderJsonSender implements JsonSender { @@ -290,7 +291,7 @@ public static final class ForwarderJsonSender implements JsonSender { } @Override - public void send(Telemetry telemetry) { + public void send(Object telemetry) { ForwarderJsonSenderThread t = new ForwarderJsonSenderThread(forwarderPath, telemetry); t.setDaemon(true); t.start(); @@ -299,9 +300,9 @@ public void send(Telemetry telemetry) { public static final class ForwarderJsonSenderThread extends Thread { private final String forwarderPath; - private final Telemetry telemetry; + private final Object telemetry; - public ForwarderJsonSenderThread(String forwarderPath, Telemetry telemetry) { + public ForwarderJsonSenderThread(String forwarderPath, Object telemetry) { super("dd-forwarder-json-sender"); this.forwarderPath = forwarderPath; this.telemetry = telemetry; @@ -314,7 +315,7 @@ public void run() { // Run forwarder and mute tracing for subprocesses executed in by dd-java-agent. try (final Closeable ignored = muteTracing()) { - byte[] payload = telemetry.json(); + byte[] payload = telemetry.toString().getBytes(); Process process = builder.start(); try (OutputStream out = process.getOutputStream()) { 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 89a0fd297a1..8efa3c014c5 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,11 +1,8 @@ package datadog.trace.bootstrap -import datadog.trace.bootstrap.BootstrapInitializationTelemetry.Telemetry import groovy.json.JsonBuilder import spock.lang.Specification -import static java.nio.charset.StandardCharsets.UTF_8 - class BootstrapInitializationTelemetryTest extends Specification { Capture capture def initTelemetry @@ -107,14 +104,14 @@ class BootstrapInitializationTelemetryTest extends Specification { } static class Capture implements BootstrapInitializationTelemetry.JsonSender { - Telemetry telemetry + Object telemetry - void send(Telemetry telemetry) { + void send(Object telemetry) { this.telemetry = telemetry } String json() { - return new String(telemetry.json(), UTF_8) + return telemetry.toString() } } } From 6916cac31c410b557f66d39efc091d8575112526 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Wed, 23 Jul 2025 18:28:39 -0400 Subject: [PATCH 4/4] Docs. --- .../trace/bootstrap/BootstrapInitializationTelemetry.java | 4 ++++ 1 file changed, 4 insertions(+) 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 a060169395a..007e926d089 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 @@ -279,6 +279,10 @@ public String toString() { } } + /** + * Declare telemetry as {@code Object} to avoid issue with double class loading from different + * classloaders. + */ public interface JsonSender { void send(Object telemetry); }