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 55dd6b4ef23..391436c549d 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,14 +1,20 @@ package datadog.trace.bootstrap; 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.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** Thread safe telemetry class used to relay information about tracer activation. */ public abstract class BootstrapInitializationTelemetry { + private static final int DEFAULT_MAX_TAGS = 5; + /** Returns a singleton no op instance of initialization telemetry */ public static BootstrapInitializationTelemetry noOpInstance() { return NoOp.INSTANCE; @@ -88,7 +94,7 @@ public static final class JsonBased extends BootstrapInitializationTelemetry { private final JsonSender sender; private final List meta; - private final List points; + private final Map> points; // one way false to true private volatile boolean incomplete = false; @@ -97,7 +103,7 @@ public static final class JsonBased extends BootstrapInitializationTelemetry { JsonBased(JsonSender sender) { this.sender = sender; this.meta = new ArrayList<>(); - this.points = new ArrayList<>(); + this.points = new LinkedHashMap<>(); } @Override @@ -118,8 +124,39 @@ public void onAbort(String reasonCode) { @Override public void onError(Throwable t) { error = true; - onPoint("library_entrypoint.error", "error_type:" + t.getClass().getName()); setMetaInfo("error", "internal_error", t.getMessage()); + + List causes = new ArrayList<>(); + + Throwable cause = t.getCause(); + while (cause != null) { + causes.add("error_type:" + cause.getClass().getName()); + cause = cause.getCause(); + } + causes.add("error_type:" + t.getClass().getName()); + + // Limit the number of tags to avoid overpopulating the JSON payload. + int maxTags = maxTags(); + int numCauses = causes.size(); + if (numCauses > maxTags) { + causes = causes.subList(numCauses - maxTags, numCauses); + } + + onPoint("library_entrypoint.error", causes); + } + + private int maxTags() { + String maxTags = EnvironmentVariables.get("DD_TELEMETRY_FORWARDER_MAX_TAGS"); + + if (maxTags != null) { + try { + return Integer.parseInt(maxTags); + } catch (Throwable ignore) { + // Ignore and return default value. + } + } + + return DEFAULT_MAX_TAGS; } @Override @@ -159,9 +196,12 @@ private String mapResultClass(String reasonCode) { } private void onPoint(String name, String tag) { + onPoint(name, Collections.singletonList(tag)); + } + + private void onPoint(String name, List tags) { synchronized (this.points) { - this.points.add(name); - this.points.add(tag); + this.points.put(name, tags); } } @@ -189,10 +229,14 @@ public void finish() { writer.name("points").beginArray(); synchronized (this.points) { - for (int i = 0; i + 1 < this.points.size(); i = i + 2) { + for (Map.Entry> entry : points.entrySet()) { writer.beginObject(); - writer.name("name").value(this.points.get(i)); - writer.name("tags").beginArray().value(this.points.get(i + 1)).endArray(); + 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(); 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 c5506a343d0..ac62d4c7ff1 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,5 +1,6 @@ package datadog.trace.bootstrap +import groovy.json.JsonBuilder import spock.lang.Specification import static java.nio.charset.StandardCharsets.UTF_8 @@ -106,8 +107,20 @@ class BootstrapInitializationTelemetryTest extends Specification { !capture.json().contains("library_entrypoint.complete") } - private 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 groovy.json.JsonBuilder(points)}}""" + def "unwind root cause"() { + when: + initTelemetry.onError(new Exception("top cause", new FileNotFoundException("root cause"))) + initTelemetry.finish() + + 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"] + ]) + } + + 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)}}""" } static class Capture implements BootstrapInitializationTelemetry.JsonSender {