From c5a69c121e8b12b93397bfe846ee313bdf0960f7 Mon Sep 17 00:00:00 2001 From: "shun.yamamoto" Date: Wed, 26 Oct 2022 01:38:35 +0900 Subject: [PATCH 01/29] Add `dd.trace.db.client.split-by-host` Config --- .../decorator/DatabaseClientDecorator.java | 4 ++ .../DatabaseClientDecoratorTest.groovy | 38 ++++++++++++------- .../datadog/trace/api/ConfigDefaults.java | 1 + .../config/TraceInstrumentationConfig.java | 1 + .../main/java/datadog/trace/api/Config.java | 13 +++++++ .../datadog/trace/api/ConfigTest.groovy | 9 +++++ 6 files changed, 53 insertions(+), 13 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecorator.java index 258bca1229a4..237682cb69d7 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecorator.java @@ -50,6 +50,10 @@ public AgentSpan onConnection(final AgentSpan span, final CONNECTION connection) CharSequence hostName = dbHostname(connection); if (hostName != null) { span.setTag(Tags.PEER_HOSTNAME, hostName); + + if (Config.get().isDbClientSplitByHost()) { + span.setServiceName(hostName.toString()); + } } } return span; diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy index de3d8f0291da..0269f51d5a80 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy @@ -4,6 +4,7 @@ import datadog.trace.api.DDTags import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.bootstrap.instrumentation.api.Tags +import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_HOST import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_INSTANCE import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_INSTANCE_TYPE_SUFFIX @@ -35,8 +36,9 @@ class DatabaseClientDecoratorTest extends ClientDecoratorTest { def "test onConnection"() { setup: - injectSysConfig(DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "$renameService") - injectSysConfig(DB_CLIENT_HOST_SPLIT_BY_INSTANCE_TYPE_SUFFIX, "$typeSuffix") + injectSysConfig(DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "$renameByInstance") + injectSysConfig(DB_CLIENT_HOST_SPLIT_BY_INSTANCE_TYPE_SUFFIX, "$instanceTypeSuffix") + injectSysConfig(DB_CLIENT_HOST_SPLIT_BY_HOST, "$renameByHost") def decorator = newDecorator() when: @@ -49,24 +51,34 @@ class DatabaseClientDecoratorTest extends ClientDecoratorTest { if (session.hostname != null) { 1 * span.setTag(Tags.PEER_HOSTNAME, session.hostname) } - if (typeSuffix && renameService && session.instance) { + if (instanceTypeSuffix && renameByInstance && session.instance) { 1 * span.setServiceName(session.instance + "-" + decorator.dbType()) - } else if (renameService && session.instance) { + } else if (renameByInstance && session.instance) { 1 * span.setServiceName(session.instance) + } else if (renameByHost) { + 1 * span.setServiceName(session.hostname) } } 0 * _ where: - renameService | typeSuffix | session - false | false | null - true | false | [user: "test-user", hostname: "test-hostname"] - false | false | [instance: "test-instance", hostname: "test-hostname"] - true | false | [user: "test-user", instance: "test-instance"] - false | true | null - true | true | [user: "test-user", hostname: "test-hostname"] - false | true | [instance: "test-instance", hostname: "test-hostname"] - true | true | [user: "test-user", instance: "test-instance"] + renameByInstance | instanceTypeSuffix | renameByHost | session + false | false | false | null + true | false | false | [user: "test-user", hostname: "test-hostname"] + false | false | false | [instance: "test-instance", hostname: "test-hostname"] + true | false | false | [user: "test-user", instance: "test-instance"] + false | true | false | null + true | true | false | [user: "test-user", hostname: "test-hostname"] + false | true | false | [instance: "test-instance", hostname: "test-hostname"] + true | true | false | [user: "test-user", instance: "test-instance"] + false | false | true | null + true | false | true | [user: "test-user", hostname: "test-hostname"] + false | false | true | [instance: "test-instance", hostname: "test-hostname"] + true | false | true | [user: "test-user", instance: "test-instance"] + false | true | true | null + true | true | true | [user: "test-user", hostname: "test-hostname"] + false | true | true | [instance: "test-instance", hostname: "test-hostname"] + true | true | true | [user: "test-user", instance: "test-instance"] } def "test onStatement"() { diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index 28217fc0086c..4f325ab12769 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -51,6 +51,7 @@ public final class ConfigDefaults { static final boolean DEFAULT_HTTP_CLIENT_SPLIT_BY_DOMAIN = false; static final boolean DEFAULT_DB_CLIENT_HOST_SPLIT_BY_INSTANCE = false; static final boolean DEFAULT_DB_CLIENT_HOST_SPLIT_BY_INSTANCE_TYPE_SUFFIX = false; + static final boolean DEFAULT_DB_CLIENT_HOST_SPLIT_BY_HOST = false; static final int DEFAULT_SCOPE_DEPTH_LIMIT = 100; static final int DEFAULT_SCOPE_ITERATION_KEEP_ALIVE = 10; // in seconds static final int DEFAULT_PARTIAL_FLUSH_MIN_SPANS = 1000; diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java index c38c96f02b9a..65888633d647 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java @@ -36,6 +36,7 @@ public final class TraceInstrumentationConfig { public static final String DB_CLIENT_HOST_SPLIT_BY_INSTANCE = "trace.db.client.split-by-instance"; public static final String DB_CLIENT_HOST_SPLIT_BY_INSTANCE_TYPE_SUFFIX = "trace.db.client.split-by-instance.type.suffix"; + public static final String DB_CLIENT_HOST_SPLIT_BY_HOST = "trace.db.client.split-by-host"; public static final String JDBC_PREPARED_STATEMENT_CLASS_NAME = "trace.jdbc.prepared.statement.class.name"; diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 69d2c7aab52a..82061876be24 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -14,6 +14,7 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_CWS_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_CWS_TLS_REFRESH; import static datadog.trace.api.ConfigDefaults.DEFAULT_DATA_STREAMS_ENABLED; +import static datadog.trace.api.ConfigDefaults.DEFAULT_DB_CLIENT_HOST_SPLIT_BY_HOST; import static datadog.trace.api.ConfigDefaults.DEFAULT_DB_CLIENT_HOST_SPLIT_BY_INSTANCE; import static datadog.trace.api.ConfigDefaults.DEFAULT_DB_CLIENT_HOST_SPLIT_BY_INSTANCE_TYPE_SUFFIX; import static datadog.trace.api.ConfigDefaults.DEFAULT_DEBUGGER_CLASSFILE_DUMP_ENABLED; @@ -222,6 +223,7 @@ import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIG_TARGETS_KEY; import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIG_TARGETS_KEY_ID; import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIG_URL; +import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_HOST; import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_INSTANCE; import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_INSTANCE_TYPE_SUFFIX; import static datadog.trace.api.config.TraceInstrumentationConfig.GRPC_CLIENT_ERROR_STATUSES; @@ -436,6 +438,7 @@ public class Config { private final boolean httpClientSplitByDomain; private final boolean dbClientSplitByInstance; private final boolean dbClientSplitByInstanceTypeSuffix; + private final boolean dbClientSplitByHost; private final Set splitByTags; private final int scopeDepthLimit; private final boolean scopeStrictMode; @@ -868,6 +871,10 @@ private Config(final ConfigProvider configProvider) { DB_CLIENT_HOST_SPLIT_BY_INSTANCE_TYPE_SUFFIX, DEFAULT_DB_CLIENT_HOST_SPLIT_BY_INSTANCE_TYPE_SUFFIX); + dbClientSplitByHost = + configProvider.getBoolean( + DB_CLIENT_HOST_SPLIT_BY_HOST, DEFAULT_DB_CLIENT_HOST_SPLIT_BY_HOST); + splitByTags = tryMakeImmutableSet(configProvider.getList(SPLIT_BY_TAGS)); scopeDepthLimit = configProvider.getInteger(SCOPE_DEPTH_LIMIT, DEFAULT_SCOPE_DEPTH_LIMIT); @@ -1512,6 +1519,10 @@ public boolean isDbClientSplitByInstanceTypeSuffix() { return dbClientSplitByInstanceTypeSuffix; } + public boolean isDbClientSplitByHost() { + return dbClientSplitByHost; + } + public Set getSplitByTags() { return splitByTags; } @@ -2849,6 +2860,8 @@ public String toString() { + dbClientSplitByInstance + ", dbClientSplitByInstanceTypeSuffix=" + dbClientSplitByInstanceTypeSuffix + + ", dbClientSplitByHost=" + + dbClientSplitByHost + ", splitByTags=" + splitByTags + ", scopeDepthLimit=" diff --git a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy index 45e05803ec6a..7dbe39e59b78 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy @@ -76,6 +76,7 @@ import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIG_ENABLED import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIG_INITIAL_POLL_INTERVAL import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIG_MAX_PAYLOAD_SIZE import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIG_URL +import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_HOST import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_INSTANCE import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_INSTANCE_TYPE_SUFFIX import static datadog.trace.api.config.TraceInstrumentationConfig.HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN @@ -168,6 +169,7 @@ class ConfigTest extends DDSpecification { prop.setProperty(HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, "true") prop.setProperty(DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "true") prop.setProperty(DB_CLIENT_HOST_SPLIT_BY_INSTANCE_TYPE_SUFFIX, "true") + prop.setProperty(DB_CLIENT_HOST_SPLIT_BY_HOST, "true") prop.setProperty(SPLIT_BY_TAGS, "some.tag1,some.tag2,some.tag1") prop.setProperty(PARTIAL_FLUSH_MIN_SPANS, "15") prop.setProperty(TRACE_REPORT_HOSTNAME, "true") @@ -251,6 +253,7 @@ class ConfigTest extends DDSpecification { config.httpClientSplitByDomain == true config.dbClientSplitByInstance == true config.dbClientSplitByInstanceTypeSuffix == true + config.dbClientSplitByHost == true config.splitByTags == ["some.tag1", "some.tag2"].toSet() config.partialFlushMinSpans == 15 config.reportHostName == true @@ -336,6 +339,7 @@ class ConfigTest extends DDSpecification { System.setProperty(PREFIX + HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, "true") System.setProperty(PREFIX + DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "true") System.setProperty(PREFIX + DB_CLIENT_HOST_SPLIT_BY_INSTANCE_TYPE_SUFFIX, "true") + System.setProperty(PREFIX + DB_CLIENT_HOST_SPLIT_BY_HOST, "true") System.setProperty(PREFIX + SPLIT_BY_TAGS, "some.tag3, some.tag2, some.tag1") System.setProperty(PREFIX + PARTIAL_FLUSH_MIN_SPANS, "25") System.setProperty(PREFIX + TRACE_REPORT_HOSTNAME, "true") @@ -419,6 +423,7 @@ class ConfigTest extends DDSpecification { config.httpClientSplitByDomain == true config.dbClientSplitByInstance == true config.dbClientSplitByInstanceTypeSuffix == true + config.dbClientSplitByHost == true config.splitByTags == ["some.tag3", "some.tag2", "some.tag1"].toSet() config.partialFlushMinSpans == 25 config.reportHostName == true @@ -549,6 +554,7 @@ class ConfigTest extends DDSpecification { System.setProperty(PREFIX + HTTP_CLIENT_ERROR_STATUSES, "1:1") System.setProperty(PREFIX + HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, "invalid") System.setProperty(PREFIX + DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "invalid") + System.setProperty(PREFIX + DB_CLIENT_HOST_SPLIT_BY_HOST, "invalid") System.setProperty(PREFIX + PROPAGATION_STYLE_EXTRACT, "some garbage") System.setProperty(PREFIX + PROPAGATION_STYLE_INJECT, " ") @@ -572,6 +578,7 @@ class ConfigTest extends DDSpecification { config.httpClientSplitByDomain == false config.dbClientSplitByInstance == false config.dbClientSplitByInstanceTypeSuffix == false + config.dbClientSplitByHost == false config.splitByTags == [].toSet() config.propagationStylesToExtract.toList() == [PropagationStyle.DATADOG] config.propagationStylesToInject.toList() == [PropagationStyle.DATADOG] @@ -644,6 +651,7 @@ class ConfigTest extends DDSpecification { properties.setProperty(HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, "true") properties.setProperty(DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "true") properties.setProperty(DB_CLIENT_HOST_SPLIT_BY_INSTANCE_TYPE_SUFFIX, "true") + properties.setProperty(DB_CLIENT_HOST_SPLIT_BY_HOST, "true") properties.setProperty(PARTIAL_FLUSH_MIN_SPANS, "15") properties.setProperty(PROPAGATION_STYLE_EXTRACT, "B3 Datadog") properties.setProperty(PROPAGATION_STYLE_INJECT, "Datadog B3") @@ -675,6 +683,7 @@ class ConfigTest extends DDSpecification { config.httpClientSplitByDomain == true config.dbClientSplitByInstance == true config.dbClientSplitByInstanceTypeSuffix == true + config.dbClientSplitByHost == true config.splitByTags == [].toSet() config.partialFlushMinSpans == 15 config.propagationStylesToExtract.toList() == [PropagationStyle.B3, PropagationStyle.DATADOG] From 4eff6fe5afd45e90b0cba4ce48488fc1556e6027 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Fri, 22 Sep 2023 17:41:48 +0200 Subject: [PATCH 02/29] Disable sampling for span decoration probe As span decoration probes are sharing the same instrumented code than Log probes, we also evaluate the sampling during the execution which does not bring any value in the case of Span decoration probes. We set the rate limiter for span decoration probe to a constant one. --- .../bootstrap/debugger/ProbeRateLimiter.java | 18 ++++++---- .../debugger/agent/ConfigurationUpdater.java | 9 ++--- .../ServerDebuggerTestApplication.java | 34 +++++++++++++++---- .../smoketest/BaseIntegrationTest.java | 4 ++- .../ServerAppDebuggerIntegrationTest.java | 7 +++- .../SpanDecorationProbesIntegrationTests.java | 29 ++++++++++++++++ 6 files changed, 81 insertions(+), 20 deletions(-) diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/ProbeRateLimiter.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/ProbeRateLimiter.java index df3aeafee259..211a1ee34f11 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/ProbeRateLimiter.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/ProbeRateLimiter.java @@ -1,6 +1,8 @@ package datadog.trace.bootstrap.debugger; import datadog.trace.api.sampling.AdaptiveSampler; +import datadog.trace.api.sampling.ConstantSampler; +import datadog.trace.api.sampling.Sampler; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.concurrent.ConcurrentHashMap; @@ -19,14 +21,13 @@ public class ProbeRateLimiter { private static final double DEFAULT_GLOBAL_LOG_RATE = 5000.0; private static final ConcurrentMap PROBE_SAMPLERS = new ConcurrentHashMap<>(); - private static AdaptiveSampler GLOBAL_SNAPSHOT_SAMPLER = - createSampler(DEFAULT_GLOBAL_SNAPSHOT_RATE); - private static AdaptiveSampler GLOBAL_LOG_SAMPLER = createSampler(DEFAULT_GLOBAL_LOG_RATE); + private static Sampler GLOBAL_SNAPSHOT_SAMPLER = createSampler(DEFAULT_GLOBAL_SNAPSHOT_RATE); + private static Sampler GLOBAL_LOG_SAMPLER = createSampler(DEFAULT_GLOBAL_LOG_RATE); public static boolean tryProbe(String probeId) { RateLimitInfo rateLimitInfo = PROBE_SAMPLERS.computeIfAbsent(probeId, ProbeRateLimiter::getDefaultRateLimitInfo); - AdaptiveSampler globalSampler = + Sampler globalSampler = rateLimitInfo.isCaptureSnapshot ? GLOBAL_SNAPSHOT_SAMPLER : GLOBAL_LOG_SAMPLER; if (globalSampler.sample()) { return rateLimitInfo.sampler.sample(); @@ -64,7 +65,10 @@ public static void resetAll() { resetGlobalRate(); } - private static AdaptiveSampler createSampler(double rate) { + private static Sampler createSampler(double rate) { + if (rate < 0) { + return new ConstantSampler(true); + } if (rate < 1) { int intRate = (int) Math.round(rate * 10); return new AdaptiveSampler(TEN_SECONDS_WINDOW, intRate, 180, 16); @@ -73,10 +77,10 @@ private static AdaptiveSampler createSampler(double rate) { } private static class RateLimitInfo { - final AdaptiveSampler sampler; + final Sampler sampler; final boolean isCaptureSnapshot; - public RateLimitInfo(AdaptiveSampler sampler, boolean isCaptureSnapshot) { + public RateLimitInfo(Sampler sampler, boolean isCaptureSnapshot) { this.sampler = sampler; this.isCaptureSnapshot = isCaptureSnapshot; } diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java index 565bee528aed..f45c17584b47 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java @@ -246,10 +246,6 @@ public ProbeImplementation resolve(String id, Class callingClass) { } private void applyRateLimiter(ConfigurationComparer changes) { - Collection probes = currentConfiguration.getLogProbes(); - if (probes == null) { - return; - } // ensure rate is up-to-date for all new probes for (ProbeDefinition addedDefinitions : changes.getAddedDefinitions()) { if (addedDefinitions instanceof LogProbe) { @@ -262,6 +258,11 @@ private void applyRateLimiter(ConfigurationComparer changes) { : getDefaultRateLimitPerProbe(probe), probe.isCaptureSnapshot()); } + if (addedDefinitions instanceof SpanDecorationProbe) { + // Span decoration probes use the same instrumentation as log probes, but we don't want + // to sample here. + ProbeRateLimiter.setRate(addedDefinitions.getId(), -1, false); + } } // remove rate for all removed probes for (ProbeDefinition removedDefinition : changes.getRemovedDefinitions()) { diff --git a/dd-smoke-tests/debugger-integration-tests/src/main/java/datadog/smoketest/debugger/ServerDebuggerTestApplication.java b/dd-smoke-tests/debugger-integration-tests/src/main/java/datadog/smoketest/debugger/ServerDebuggerTestApplication.java index b9cc24345e7c..9e4120c810cd 100644 --- a/dd-smoke-tests/debugger-integration-tests/src/main/java/datadog/smoketest/debugger/ServerDebuggerTestApplication.java +++ b/dd-smoke-tests/debugger-integration-tests/src/main/java/datadog/smoketest/debugger/ServerDebuggerTestApplication.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import okhttp3.HttpUrl; import okhttp3.MediaType; import okhttp3.OkHttpClient; @@ -18,7 +19,7 @@ public class ServerDebuggerTestApplication { private static final MockResponse EMPTY_HTTP_200 = new MockResponse().setResponseCode(200); private static final String LOG_FILENAME = System.getenv().get("DD_LOG_FILE"); - private static final Map methodsByName = new HashMap<>(); + private static final Map> methodsByName = new HashMap<>(); private final MockWebServer webServer = new MockWebServer(); private final String controlServerUrl; private final OkHttpClient httpClient = new OkHttpClient(); @@ -39,6 +40,8 @@ public static void main(String[] args) throws Exception { public static void registerMethods() { methodsByName.put("fullMethod", ServerDebuggerTestApplication::runFullMethod); methodsByName.put("tracedMethod", ServerDebuggerTestApplication::runTracedMethod); + methodsByName.put("loopingFullMethod", ServerDebuggerTestApplication::runLoopingFullMethod); + methodsByName.put("loopingTracedMethod", ServerDebuggerTestApplication::runLoopingTracedMethod); } public ServerDebuggerTestApplication(String controlServerUrl) { @@ -93,17 +96,17 @@ protected void waitForReTransformation(String className) { } } - protected void execute(String methodName) { - Runnable method = methodsByName.get(methodName); + protected void execute(String methodName, String arg) { + Consumer method = methodsByName.get(methodName); if (method == null) { throw new RuntimeException("cannot find method: " + methodName); } System.out.println("Executing method: " + methodName); - method.run(); + method.accept(arg); System.out.println("Executed"); } - private static void runFullMethod() { + private static void runFullMethod(String arg) { Map map = new HashMap<>(); map.put("key1", "val1"); map.put("key2", "val2"); @@ -111,8 +114,18 @@ private static void runFullMethod() { fullMethod(42, "foobar", 3.42, map, "var1", "var2", "var3"); } + private static void runLoopingFullMethod(String arg) { + Map map = new HashMap<>(); + map.put("key1", "val1"); + map.put("key2", "val2"); + map.put("key3", "val3"); + for (int i = 0; i < Integer.parseInt(arg); i++) { + fullMethod(42, "foobar", 3.42, map, "var1", "var2", "var3"); + } + } + @Trace - private static void runTracedMethod() { + private static void runTracedMethod(String arg) { Map map = new HashMap<>(); map.put("key1", "val1"); map.put("key2", "val2"); @@ -120,6 +133,12 @@ private static void runTracedMethod() { tracedMethod(42, "foobar", 3.42, map, "var1", "var2", "var3"); } + private static void runLoopingTracedMethod(String arg) { + for (int i = 0; i < Integer.parseInt(arg); i++) { + runTracedMethod(arg); + } + } + private static String fullMethod( int argInt, String argStr, double argDouble, Map argMap, String... argVar) { try { @@ -182,7 +201,8 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio case "/app/execute": { String methodName = request.getRequestUrl().queryParameter("methodname"); - app.execute(methodName); + String arg = request.getRequestUrl().queryParameter("arg"); + app.execute(methodName, arg); break; } case "/app/stop": diff --git a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/BaseIntegrationTest.java b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/BaseIntegrationTest.java index f81ea711c82e..3574a2854934 100644 --- a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/BaseIntegrationTest.java +++ b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/BaseIntegrationTest.java @@ -310,6 +310,7 @@ protected void clearProbeStatusListener() { } protected void processRequests() throws InterruptedException { + long start = System.currentTimeMillis(); RecordedRequest request; do { request = datadogAgentServer.takeRequest(REQUEST_WAIT_TIMEOUT, TimeUnit.SECONDS); @@ -322,7 +323,8 @@ protected void processRequests() throws InterruptedException { return; } } - } while (request != null); + } while (request != null && (System.currentTimeMillis() - start < 30_000)); + throw new RuntimeException("timeout!"); } protected void processRemainingRequests() throws InterruptedException { diff --git a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/ServerAppDebuggerIntegrationTest.java b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/ServerAppDebuggerIntegrationTest.java index 29f4371b2859..34ee1de31fa5 100644 --- a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/ServerAppDebuggerIntegrationTest.java +++ b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/ServerAppDebuggerIntegrationTest.java @@ -93,8 +93,13 @@ protected List waitForSnapshots() throws Exception { } protected void execute(String appUrl, String methodName) throws IOException { + execute(appUrl, methodName, null); + } + + protected void execute(String appUrl, String methodName, String arg) throws IOException { datadogAgentServer.enqueue(EMPTY_200_RESPONSE); // expect 1 snapshot - String url = String.format(appUrl + "/execute?methodname=%s", methodName); + String executeFormat = arg != null ? "/execute?methodname=%s&arg=%s" : "/execute?methodname=%s"; + String url = String.format(appUrl + executeFormat, methodName, arg); sendRequest(url); LOG.info("Execution done"); } diff --git a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/SpanDecorationProbesIntegrationTests.java b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/SpanDecorationProbesIntegrationTests.java index c25dbae62e89..634ca3e41297 100644 --- a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/SpanDecorationProbesIntegrationTests.java +++ b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/SpanDecorationProbesIntegrationTests.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -239,6 +240,34 @@ void testMethodMultiTagValueError() throws Exception { processRequests(); } + @Test + @DisplayName("testSamplingSpanDecoration") + void testSamplingSpanDecoration() throws Exception { + SpanDecorationProbe spanDecorationProbe = + SpanDecorationProbe.builder() + .probeId(PROBE_ID) + .where(TEST_APP_CLASS_NAME, TRACED_METHOD_NAME) + .decorate(createDecoration("tag1", "staticText")) + .targetSpan(SpanDecorationProbe.TargetSpan.ACTIVE) + .build(); + addProbe(spanDecorationProbe); + waitForInstrumentation(appUrl); + execute(appUrl, "loopingTracedMethod", "100"); + AtomicInteger count = new AtomicInteger(); + registerTraceListener( + decodedTrace -> { + for (DecodedSpan span : decodedTrace.getSpans()) { + if (isTracedFullMethodSpan(span)) { + if (span.getMeta().containsKey("tag1")) { + return count.incrementAndGet() >= 100; + } + } + } + return false; + }); + processRequests(); + } + private boolean isTracedFullMethodSpan(DecodedSpan span) { return span.getName().equals("trace.annotation") && span.getResource().equals("ServerDebuggerTestApplication.runTracedMethod"); From e1d96fcb851871d6b55afd7c022624c2cf599c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjo=CC=88rn=20Antonsson?= Date: Wed, 11 Oct 2023 10:17:16 +0200 Subject: [PATCH 03/29] Lock down CircleCI build images and remove workaround --- .circleci/config.continue.yml.j2 | 6 +++--- .circleci/start_docker_autoforward.sh | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.circleci/config.continue.yml.j2 b/.circleci/config.continue.yml.j2 index d9d719a7c511..1e17f728aa75 100644 --- a/.circleci/config.continue.yml.j2 +++ b/.circleci/config.continue.yml.j2 @@ -635,7 +635,7 @@ jobs: <<: *tests docker: - - image: << pipeline.parameters.docker_image >>:<< parameters.testJvm >> + - image: << pipeline.parameters.docker_image >>:{{ docker_image_prefix }}<< parameters.testJvm >> environment: - CI_USE_TEST_AGENT=true - image: ghcr.io/datadog/dd-apm-test-agent/ddapm-test-agent:v1.11.0 @@ -676,7 +676,7 @@ jobs: resource_class: medium docker: - - image: << pipeline.parameters.docker_image >>:7 + - image: << pipeline.parameters.docker_image >>:{{ docker_image_prefix }}8 - image: datadog/agent:7.34.0 environment: - DD_APM_ENABLED=true @@ -687,7 +687,7 @@ jobs: <<: *defaults resource_class: medium docker: - - image: << pipeline.parameters.docker_image >>:7 + - image: << pipeline.parameters.docker_image >>:{{ docker_image_prefix }}7 steps: - setup_code diff --git a/.circleci/start_docker_autoforward.sh b/.circleci/start_docker_autoforward.sh index a07696ec603f..c4bf29cc7734 100755 --- a/.circleci/start_docker_autoforward.sh +++ b/.circleci/start_docker_autoforward.sh @@ -1,9 +1,6 @@ #!/usr/bin/env bash set -eux -# workaround for https://github.com/docker/docker-py/issues/3113 -pip3 install --force-reinstall "urllib3<2" - if [[ -n "${DOCKER_CERT_PATH:-}" ]]; then TLS_ARGS=" --secure From f45924d6a6a736db3ef21209d60899f49d75156c Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Wed, 11 Oct 2023 09:54:25 +0100 Subject: [PATCH 04/29] Remove empty protobuf module (#6021) --- .../instrumentation/protobuf/build.gradle | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 dd-java-agent/instrumentation/protobuf/build.gradle diff --git a/dd-java-agent/instrumentation/protobuf/build.gradle b/dd-java-agent/instrumentation/protobuf/build.gradle deleted file mode 100644 index 468b58294024..000000000000 --- a/dd-java-agent/instrumentation/protobuf/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -apply from: "$rootDir/gradle/java.gradle" - -addTestSuiteForDir('latestDepTest', 'test') - -muzzle { - - pass { - group = 'com.google.protobuf' - module = 'protobuf-java' - versions = '[2.0.1,]' - assertInverse = true - } -} - -dependencies { - compileOnly group: 'com.google.protobuf', name: 'protobuf-java', version: '2.0.1' - latestDepTestImplementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.+' -} From e781a6af49a4a6ea144779edf989406a2106ea48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjo=CC=88rn=20Antonsson?= Date: Wed, 11 Oct 2023 12:09:15 +0200 Subject: [PATCH 05/29] Change code owner of appsec directories --- .github/CODEOWNERS | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bfe5381bac3a..658580cf0163 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -33,11 +33,10 @@ dd-java-agent/agent-debugger/ @DataDog/debugger-java dd-smoke-tests/debugger-integration-tests/ @DataDog/debugger-java # @DataDog/asm-java (AppSec/IAST) -dd-java-agent/appsec/ @DataDog/asm-java dd-java-agent/agent-iast/ @DataDog/asm-java dd-java-agent/instrumentation/*iast* @DataDog/asm-java dd-java-agent/instrumentation/*appsec* @DataDog/asm-java -dd-smoke-tests/appsec/ @DataDog/asm-java +**/appsec/ @DataDog/asm-java **/iast/ @DataDog/asm-java **/Iast*.java @DataDog/asm-java **/Iast*.groovy @DataDog/asm-java From a3aa8c578e8db8a4ecb0932f54e847fb34d5ef60 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Wed, 11 Oct 2023 11:37:42 +0100 Subject: [PATCH 06/29] Avoid calling inbox.clear() when shutting down as it might block (#6011) --- .../trace/core/datastreams/DefaultDataStreamsMonitoring.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/datastreams/DefaultDataStreamsMonitoring.java b/dd-trace-core/src/main/java/datadog/trace/core/datastreams/DefaultDataStreamsMonitoring.java index 64bc13f1418a..c1e6b4656064 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/datastreams/DefaultDataStreamsMonitoring.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/datastreams/DefaultDataStreamsMonitoring.java @@ -269,7 +269,6 @@ public void close() { thread.join(THREAD_JOIN_TIMOUT_MS); } catch (InterruptedException ignored) { } - inbox.clear(); } private class InboxProcessor implements Runnable { From 5daa24afc3422a33892bb169947d250c04fa3603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Wed, 11 Oct 2023 17:44:16 +0200 Subject: [PATCH 07/29] Remove recommendation to install deprecated "action on save" intelliJ plugin (#6030) --- CONTRIBUTING.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d558c6b8535..7c903bdffabf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -200,8 +200,6 @@ Suggested plugins and settings: * With java use the following import layout (groovy should still use the default) to ensure consistency with google-java-format: ![import layout](https://user-images.githubusercontent.com/734411/43430811-28442636-94ae-11e8-86f1-f270ddcba023.png) * [Google Java Format](https://plugins.jetbrains.com/plugin/8527-google-java-format) -* [Save Actions](https://plugins.jetbrains.com/plugin/7642-save-actions) - ![Recommended Settings](https://user-images.githubusercontent.com/35850765/124003079-838f4280-d9a4-11eb-9250-5c517631e362.png) ## Troubleshooting From 2e20023706b25046a858e3c4f6641ee7c7276a54 Mon Sep 17 00:00:00 2001 From: Lucas Rogerio Caetano Ferreira Date: Wed, 11 Oct 2023 11:05:15 -0500 Subject: [PATCH 08/29] Add Graal Native image support for jctools (#6020) --- ...nSubstitutionProcessorInstrumentation.java | 32 +++++++++++++++++-- ...ers_FixedSizeStripedLongCounterFields.java | 12 +++++++ ...org_jctools_util_UnsafeRefArrayAccess.java | 12 +++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/Target_org_jctools_counters_FixedSizeStripedLongCounterFields.java create mode 100644 dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/Target_org_jctools_util_UnsafeRefArrayAccess.java diff --git a/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/AnnotationSubstitutionProcessorInstrumentation.java b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/AnnotationSubstitutionProcessorInstrumentation.java index e7870be9ff82..8a75e238c08f 100644 --- a/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/AnnotationSubstitutionProcessorInstrumentation.java +++ b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/AnnotationSubstitutionProcessorInstrumentation.java @@ -6,6 +6,8 @@ import com.google.auto.service.AutoService; import datadog.trace.agent.tooling.Instrumenter; +import java.util.List; +import net.bytebuddy.asm.Advice; @AutoService(Instrumenter.class) public final class AnnotationSubstitutionProcessorInstrumentation @@ -23,11 +25,37 @@ public void adviceTransformations(AdviceTransformation transformation) { .and(named("lookup")) .and(takesArgument(0, named("jdk.vm.ci.meta.ResolvedJavaField"))), packageName + ".DeleteFieldAdvice"); + transformation.applyAdvice( + isMethod().and(named("findTargetClasses")), + AnnotationSubstitutionProcessorInstrumentation.class.getName() + + "$FindTargetClassesAdvice"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".Target_org_jctools_counters_FixedSizeStripedLongCounterFields", + packageName + ".Target_org_jctools_util_UnsafeRefArrayAccess" + }; } @Override public String[] muzzleIgnoredClassNames() { - // ignore JVMCI classes which are part of GraalVM but aren't available in public repositories - return new String[] {"jdk.vm.ci.meta.ResolvedJavaType", "jdk.vm.ci.meta.ResolvedJavaField"}; + // JVMCI classes which are part of GraalVM but aren't available in public repositories + return new String[] { + "jdk.vm.ci.meta.ResolvedJavaType", + "jdk.vm.ci.meta.ResolvedJavaField", + // ignore helper class names as usual + packageName + ".Target_org_jctools_counters_FixedSizeStripedLongCounterFields", + packageName + ".Target_org_jctools_util_UnsafeRefArrayAccess" + }; + } + + public static class FindTargetClassesAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Return(readOnly = false) List> result) { + result.add(Target_org_jctools_counters_FixedSizeStripedLongCounterFields.class); + result.add(Target_org_jctools_util_UnsafeRefArrayAccess.class); + } } } diff --git a/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/Target_org_jctools_counters_FixedSizeStripedLongCounterFields.java b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/Target_org_jctools_counters_FixedSizeStripedLongCounterFields.java new file mode 100644 index 000000000000..6eedbae2acdc --- /dev/null +++ b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/Target_org_jctools_counters_FixedSizeStripedLongCounterFields.java @@ -0,0 +1,12 @@ +package datadog.trace.instrumentation.graal.nativeimage; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "org.jctools.counters.FixedSizeStripedLongCounterFields") +public final class Target_org_jctools_counters_FixedSizeStripedLongCounterFields { + @Alias + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.ArrayBaseOffset, declClass = long[].class) + public static long COUNTER_ARRAY_BASE; +} diff --git a/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/Target_org_jctools_util_UnsafeRefArrayAccess.java b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/Target_org_jctools_util_UnsafeRefArrayAccess.java new file mode 100644 index 000000000000..9ac8984b5c76 --- /dev/null +++ b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/Target_org_jctools_util_UnsafeRefArrayAccess.java @@ -0,0 +1,12 @@ +package datadog.trace.instrumentation.graal.nativeimage; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "org.jctools.util.UnsafeRefArrayAccess") +public final class Target_org_jctools_util_UnsafeRefArrayAccess { + @Alias + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.ArrayIndexShift, declClass = Object[].class) + public static int REF_ELEMENT_SHIFT; +} From b43a556210d166f557efacf2ec8102cd2cb37e38 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Thu, 12 Oct 2023 13:06:11 +0200 Subject: [PATCH 09/29] Update system-tests to edfea31b7a9ceaed03b705de34a4e525853444c0 (#6038) --- .circleci/config.continue.yml.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.continue.yml.j2 b/.circleci/config.continue.yml.j2 index 1e17f728aa75..4a3d002d2b86 100644 --- a/.circleci/config.continue.yml.j2 +++ b/.circleci/config.continue.yml.j2 @@ -36,7 +36,7 @@ instrumentation_modules: &instrumentation_modules "dd-java-agent/instrumentation debugger_modules: &debugger_modules "dd-java-agent/agent-debugger|dd-java-agent/agent-bootstrap|dd-java-agent/agent-builder|internal-api|communication|dd-trace-core" profiling_modules: &profiling_modules "dd-java-agent/agent-profiling" -default_system_tests_commit: &default_system_tests_commit 74de81e97fd6786854c2fe254764cfb75610c676 +default_system_tests_commit: &default_system_tests_commit edfea31b7a9ceaed03b705de34a4e525853444c0 parameters: nightly: From 0e37bd87c9598799d41a2bd6594f9aba46c5ea52 Mon Sep 17 00:00:00 2001 From: Richard Startin Date: Thu, 12 Oct 2023 13:06:24 +0100 Subject: [PATCH 10/29] Upgrade to ddprof 0.81.0 --- gradle/dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index c801b0cdd305..21b6ec0c0234 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -33,7 +33,7 @@ final class CachedData { testcontainers: '1.17.3', jmc : "8.1.0", autoservice : "1.0-rc7", - ddprof : "0.79.0", + ddprof : "0.81.0", asm : "9.6", cafe_crypto : "0.1.0" ] From 841014a035092b95feb47aba883c55f78d39ffb2 Mon Sep 17 00:00:00 2001 From: Richard Startin Date: Thu, 12 Oct 2023 13:47:15 +0100 Subject: [PATCH 11/29] expose omit linenumbers mode for profiler --- .../com/datadog/profiling/ddprof/DatadogProfiler.java | 4 ++++ .../datadog/profiling/ddprof/DatadogProfilerConfig.java | 9 +++++++++ .../java/datadog/trace/api/config/ProfilingConfig.java | 5 +++++ 3 files changed, 18 insertions(+) diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfiler.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfiler.java index eb7a0a1bdc28..0178fb76d3b1 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfiler.java +++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfiler.java @@ -18,6 +18,7 @@ import static com.datadog.profiling.ddprof.DatadogProfilerConfig.isMemoryLeakProfilingEnabled; import static com.datadog.profiling.ddprof.DatadogProfilerConfig.isSpanNameContextAttributeEnabled; import static com.datadog.profiling.ddprof.DatadogProfilerConfig.isWallClockProfilerEnabled; +import static com.datadog.profiling.ddprof.DatadogProfilerConfig.omitLineNumbers; import static com.datadog.profiling.utils.ProfilingMode.ALLOCATION; import static com.datadog.profiling.utils.ProfilingMode.CPU; import static com.datadog.profiling.utils.ProfilingMode.MEMLEAK; @@ -305,6 +306,9 @@ String cmdStartProfiling(Path file) throws IllegalStateException { cmd.append(",cstack=").append(getCStack(configProvider)); cmd.append(",safemode=").append(getSafeMode(configProvider)); cmd.append(",attributes=").append(String.join(";", orderedContextAttributes)); + if (omitLineNumbers(configProvider)) { + cmd.append(",linenumbers=f"); + } if (profilingModes.contains(CPU)) { // cpu profiling is enabled. String schedulingEvent = getSchedulingEvent(configProvider); diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java index 94fc1983ff84..d6eeac40899f 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java +++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java @@ -15,6 +15,8 @@ import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_CSTACK; import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_CSTACK_DEFAULT; import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_LIBPATH; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_LINE_NUMBERS; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_LINE_NUMBERS_DEFAULT; import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_LIVEHEAP_CAPACITY; import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_LIVEHEAP_CAPACITY_DEFAULT; import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_LIVEHEAP_ENABLED; @@ -244,6 +246,13 @@ public static String getCStack() { return getCStack(ConfigProvider.getInstance()); } + public static boolean omitLineNumbers(ConfigProvider configProvider) { + return !getBoolean( + configProvider, + PROFILING_DATADOG_PROFILER_LINE_NUMBERS, + PROFILING_DATADOG_PROFILER_LINE_NUMBERS_DEFAULT); + } + private static int clamp(int min, int max, int value) { return Math.max(min, Math.min(max, value)); } diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java index 486489178a82..2959d69bf2e1 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java @@ -117,6 +117,11 @@ public final class ProfilingConfig { public static final int PROFILING_DATADOG_PROFILER_SAFEMODE_DEFAULT = 12; // POP_METHOD|UNWIND_NATIVE + public static final String PROFILING_DATADOG_PROFILER_LINE_NUMBERS = + "profiling.ddprof.linenumbers"; + + public static final boolean PROFILING_DATADOG_PROFILER_LINE_NUMBERS_DEFAULT = true; + @Deprecated public static final String PROFILING_DATADOG_PROFILER_MEMLEAK_ENABLED = "profiling.ddprof.memleak.enabled"; From 6d817ffedb626784ecfe42811405b14581242b53 Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko <121111529+nikita-tkachenko-datadog@users.noreply.github.com> Date: Thu, 12 Oct 2023 19:26:58 +0200 Subject: [PATCH 12/29] Add latest dependency tests to Maven instrumentation (#5899) --- .../civisibility/CiVisibilityTest.groovy | 22 +++- .../instrumentation/maven-3.2.1/build.gradle | 14 ++- .../src/test/groovy/MavenTest.groovy | 105 +++++++++--------- .../pom.xml | 1 - 4 files changed, 79 insertions(+), 63 deletions(-) diff --git a/dd-java-agent/agent-ci-visibility/src/testFixtures/groovy/datadog/trace/civisibility/CiVisibilityTest.groovy b/dd-java-agent/agent-ci-visibility/src/testFixtures/groovy/datadog/trace/civisibility/CiVisibilityTest.groovy index 120e1c135a1b..421731b198bd 100644 --- a/dd-java-agent/agent-ci-visibility/src/testFixtures/groovy/datadog/trace/civisibility/CiVisibilityTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/testFixtures/groovy/datadog/trace/civisibility/CiVisibilityTest.groovy @@ -30,6 +30,7 @@ import spock.lang.Unroll import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths +import java.util.regex.Pattern @Unroll abstract class CiVisibilityTest extends AgentTestRunner { @@ -43,6 +44,7 @@ abstract class CiVisibilityTest extends AgentTestRunner { static final int DUMMY_TEST_METHOD_START = 12 static final int DUMMY_TEST_METHOD_END = 18 static final Collection DUMMY_CODE_OWNERS = ["owner1", "owner2"] + static final Pattern ANY_MESSAGE = Pattern.compile(".*") private static Path agentKeyFile @@ -66,7 +68,7 @@ abstract class CiVisibilityTest extends AgentTestRunner { def moduleExecutionSettingsFactory = Stub(ModuleExecutionSettingsFactory) moduleExecutionSettingsFactory.create(_, _) >> { Map properties = [ - (CiVisibilityConfig.CIVISIBILITY_ITR_ENABLED) : String.valueOf(itrEnabled) + (CiVisibilityConfig.CIVISIBILITY_ITR_ENABLED): String.valueOf(itrEnabled) ] return new ModuleExecutionSettings(false, itrEnabled, properties, Collections.singletonMap(dummyModule, skippableTests), Collections.emptyList()) } @@ -160,7 +162,8 @@ abstract class CiVisibilityTest extends AgentTestRunner { final String resource = null, final String testCommand = null, final String testToolchain = null, - final Throwable exception = null) { + final Throwable exception = null, + final boolean verifyExceptionMessage = true) { def testFramework = expectedTestFramework() def testFrameworkVersion = expectedTestFrameworkVersion() @@ -197,7 +200,11 @@ abstract class CiVisibilityTest extends AgentTestRunner { } if (exception) { - errorTags(exception.class, exception.message) + if (verifyExceptionMessage) { + errorTags(exception.class, exception.message) + } else { + errorTags(exception.class, ANY_MESSAGE) + } } "$DUMMY_CI_TAG" DUMMY_CI_TAG_VALUE @@ -225,7 +232,8 @@ abstract class CiVisibilityTest extends AgentTestRunner { final String testStatus, final Map testTags = null, final Throwable exception = null, - final String resource = null) { + final String resource = null, + final boolean verifyExceptionMessage = true) { def testFramework = expectedTestFramework() def testFrameworkVersion = expectedTestFrameworkVersion() @@ -254,7 +262,11 @@ abstract class CiVisibilityTest extends AgentTestRunner { } if (exception) { - errorTags(exception.class, exception.message) + if (verifyExceptionMessage) { + errorTags(exception.class, exception.message) + } else { + errorTags(exception.class, ANY_MESSAGE) + } } "$DUMMY_CI_TAG" DUMMY_CI_TAG_VALUE diff --git a/dd-java-agent/instrumentation/maven-3.2.1/build.gradle b/dd-java-agent/instrumentation/maven-3.2.1/build.gradle index 3500f43374fc..488c0c64e499 100644 --- a/dd-java-agent/instrumentation/maven-3.2.1/build.gradle +++ b/dd-java-agent/instrumentation/maven-3.2.1/build.gradle @@ -1,3 +1,5 @@ +apply from: "$rootDir/gradle/java.gradle" + muzzle { pass { group = 'org.apache.maven' @@ -7,7 +9,7 @@ muzzle { } } -apply from: "$rootDir/gradle/java.gradle" +addTestSuiteForDir('latestDepTest', 'test') dependencies { compileOnly 'org.apache.maven:maven-embedder:3.2.1' @@ -17,7 +19,11 @@ dependencies { // this is not the earliest version of Maven that we support, // but using the earliest one is not possible here because of dependency conflicts testImplementation 'org.apache.maven:maven-embedder:3.2.5' - testImplementation 'org.eclipse.aether:aether-connector-basic:1.0.0.v20140518' - testImplementation 'org.eclipse.aether:aether-transport-wagon:1.0.0.v20140518' - testImplementation 'org.apache.maven.wagon:wagon-http:2.8' + testImplementation group: 'org.apache.maven.resolver', name: 'maven-resolver-connector-basic', version: '1.0.3' + testImplementation group: 'org.apache.maven.resolver', name: 'maven-resolver-transport-http', version: '1.0.3' + + latestDepTestImplementation group: 'org.apache.maven', name: 'maven-embedder', version: '+' + latestDepTestImplementation group: 'org.apache.maven.resolver', name: 'maven-resolver-connector-basic', version: '+' + latestDepTestImplementation group: 'org.apache.maven.resolver', name: 'maven-resolver-transport-http', version: '+' + latestDepTestImplementation group: 'org.fusesource.jansi', name: 'jansi', version: '+' } diff --git a/dd-java-agent/instrumentation/maven-3.2.1/src/test/groovy/MavenTest.groovy b/dd-java-agent/instrumentation/maven-3.2.1/src/test/groovy/MavenTest.groovy index e53b1f82e587..95ef82ef2803 100644 --- a/dd-java-agent/instrumentation/maven-3.2.1/src/test/groovy/MavenTest.groovy +++ b/dd-java-agent/instrumentation/maven-3.2.1/src/test/groovy/MavenTest.groovy @@ -20,6 +20,7 @@ class MavenTest extends CiVisibilityTest { @Override void setup() { + System.setProperty("maven.multiModuleProjectDirectory", projectFolder.toAbsolutePath().toString()) givenMavenProjectFiles(specificationContext.currentIteration.name) givenMavenDependenciesAreLoaded() TEST_WRITER.clear() // loading dependencies will generate a test-session span @@ -33,7 +34,7 @@ class MavenTest extends CiVisibilityTest { def "test_maven_build_with_no_tests_generates_spans"() { given: - String[] args = ["verify"] + String[] args = ["-B", "verify"] String workingDirectory = projectFolder.toString() when: @@ -46,8 +47,8 @@ class MavenTest extends CiVisibilityTest { trace(1, true) { testSessionSpan(it, 0, CIConstants.TEST_SKIP, [:], "Maven Integration Tests Project", - "mvn verify", - "maven:3.2.5" + "mvn -B verify", + "maven:${expectedToolchainVersion()}" ) } } @@ -55,7 +56,7 @@ class MavenTest extends CiVisibilityTest { def "test_maven_build_with_incorrect_command_generates_spans"() { given: - String[] args = ["unknownPhase"] + String[] args = ["-B", "unknownPhase"] String workingDirectory = projectFolder.toString() when: @@ -68,19 +69,18 @@ class MavenTest extends CiVisibilityTest { trace(1, true) { testSessionSpan(it, 0, CIConstants.TEST_FAIL, null, "Maven Integration Tests Project", - "mvn unknownPhase", - "maven:3.2.5" - , - new LifecyclePhaseNotFoundException( - "Unknown lifecycle phase \"unknownPhase\". You must specify a valid lifecycle phase or a goal in the format : or :[:]:. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy.", - "unknownPhase")) + "mvn -B unknownPhase", + "maven:${expectedToolchainVersion()}", + new LifecyclePhaseNotFoundException("", ""), + false + ) } } } def "test_maven_build_with_tests_generates_spans"() { given: - String[] args = ["clean", "test"] + String[] args = ["-B", "clean", "test"] String workingDirectory = projectFolder.toString() when: @@ -93,13 +93,13 @@ class MavenTest extends CiVisibilityTest { trace(2, true) { def testSessionId = testSessionSpan(it, 1, CIConstants.TEST_PASS, [:], "Maven Integration Tests Project", - "mvn clean test", - "maven:3.2.5" + "mvn -B clean test", + "maven:${expectedToolchainVersion()}" ) testModuleSpan(it, 0, testSessionId, CIConstants.TEST_PASS, [ - (Tags.TEST_COMMAND) : "mvn clean test", + (Tags.TEST_COMMAND) : "mvn -B clean test", (Tags.TEST_EXECUTION): "maven-surefire-plugin:test:default-test", ], null, "Maven Integration Tests Project maven-surefire-plugin default-test") @@ -109,7 +109,7 @@ class MavenTest extends CiVisibilityTest { def "test_maven_build_with_failed_tests_generates_spans"() { given: - String[] args = ["clean", "test"] + String[] args = ["-B", "clean", "test"] String workingDirectory = projectFolder.toString() when: @@ -120,31 +120,28 @@ class MavenTest extends CiVisibilityTest { assertTraces(1) { trace(2, true) { - def testsFailedException = new LifecycleExecutionException("Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test (default-test) on project maven-integration-test: There are test failures.\n" + - "\n" + - "Please refer to ${workingDirectory}/target/surefire-reports for the individual test results.") - def testSessionId = testSessionSpan(it, 1, CIConstants.TEST_FAIL, null, "Maven Integration Tests Project", - "mvn clean test", - "maven:3.2.5" - , - testsFailedException) + "mvn -B clean test", + "maven:${expectedToolchainVersion()}", + new LifecycleExecutionException(""), + false) testModuleSpan(it, 0, testSessionId, CIConstants.TEST_FAIL, [ - (Tags.TEST_COMMAND) : "mvn clean test", + (Tags.TEST_COMMAND) : "mvn -B clean test", (Tags.TEST_EXECUTION): "maven-surefire-plugin:test:default-test", ], - testsFailedException, - "Maven Integration Tests Project maven-surefire-plugin default-test") + new LifecycleExecutionException(""), + "Maven Integration Tests Project maven-surefire-plugin default-test", + false) } } } def "test_maven_build_with_tests_in_multiple_modules_generates_spans"() { given: - String[] args = ["clean", "test"] + String[] args = ["-B", "clean", "test"] String workingDirectory = projectFolder.toString() when: @@ -155,37 +152,35 @@ class MavenTest extends CiVisibilityTest { assertTraces(1) { trace(3, true) { - def testsFailedException = new LifecycleExecutionException("Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test (default-test) on project maven-integration-test-module-b: There are test failures.\n" + - "\n" + - "Please refer to ${workingDirectory}/module-b/target/surefire-reports for the individual test results.") def testSessionId = testSessionSpan(it, 2, CIConstants.TEST_FAIL, null, "Maven Integration Tests Project", - "mvn clean test", - "maven:3.2.5" - , - testsFailedException) + "mvn -B clean test", + "maven:${expectedToolchainVersion()}", + new LifecycleExecutionException(""), + false) testModuleSpan(it, 0, testSessionId, CIConstants.TEST_PASS, [ - (Tags.TEST_COMMAND) : "mvn clean test", + (Tags.TEST_COMMAND) : "mvn -B clean test", (Tags.TEST_EXECUTION): "maven-surefire-plugin:test:default-test", ], null, "module-a maven-surefire-plugin default-test") testModuleSpan(it, 1, testSessionId, CIConstants.TEST_FAIL, [ - (Tags.TEST_COMMAND) : "mvn clean test", + (Tags.TEST_COMMAND) : "mvn -B clean test", (Tags.TEST_EXECUTION): "maven-surefire-plugin:test:default-test", ], - testsFailedException, - "module-b maven-surefire-plugin default-test") + new LifecycleExecutionException(""), + "module-b maven-surefire-plugin default-test", + false) } } } def "test_maven_build_with_tests_in_multiple_modules_run_in_parallel_generates_spans"() { given: - String[] args = ["-T4", "clean", "test"] + String[] args = ["-B", "-T4", "clean", "test"] String workingDirectory = projectFolder.toString() when: @@ -198,20 +193,20 @@ class MavenTest extends CiVisibilityTest { trace(3, true) { def testSessionId = testSessionSpan(it, 2, CIConstants.TEST_PASS, [:], "Maven Integration Tests Project", - "mvn -T4 clean test", - "maven:3.2.5", + "mvn -B -T4 clean test", + "maven:${expectedToolchainVersion()}" ) testModuleSpan(it, 0, testSessionId, CIConstants.TEST_PASS, [ - (Tags.TEST_COMMAND) : "mvn -T4 clean test", + (Tags.TEST_COMMAND) : "mvn -B -T4 clean test", (Tags.TEST_EXECUTION): "maven-surefire-plugin:test:default-test", ], null, "module-a maven-surefire-plugin default-test") testModuleSpan(it, 1, testSessionId, CIConstants.TEST_PASS, [ - (Tags.TEST_COMMAND) : "mvn -T4 clean test", + (Tags.TEST_COMMAND) : "mvn -B -T4 clean test", (Tags.TEST_EXECUTION): "maven-surefire-plugin:test:default-test", ], null, "module-b maven-surefire-plugin default-test") @@ -221,7 +216,7 @@ class MavenTest extends CiVisibilityTest { def "test_maven_build_with_unit_and_integration_tests_generates_spans"() { given: - String[] args = ["verify"] + String[] args = ["-B", "verify"] String workingDirectory = projectFolder.toString() when: @@ -234,20 +229,20 @@ class MavenTest extends CiVisibilityTest { trace(3, true) { def testSessionId = testSessionSpan(it, 2, CIConstants.TEST_PASS, [:], "Maven Integration Tests Project", - "mvn verify", - "maven:3.2.5" + "mvn -B verify", + "maven:${expectedToolchainVersion()}" ) testModuleSpan(it, 1, testSessionId, CIConstants.TEST_PASS, [ - (Tags.TEST_COMMAND) : "mvn verify", + (Tags.TEST_COMMAND) : "mvn -B verify", (Tags.TEST_EXECUTION): "maven-surefire-plugin:test:default-test", ], null, "Maven Integration Tests Project maven-surefire-plugin default-test") testModuleSpan(it, 0, testSessionId, CIConstants.TEST_PASS, [ - (Tags.TEST_COMMAND) : "mvn verify", + (Tags.TEST_COMMAND) : "mvn -B verify", (Tags.TEST_EXECUTION): "maven-failsafe-plugin:integration-test:default", ], null, "Maven Integration Tests Project maven-failsafe-plugin default") @@ -257,7 +252,7 @@ class MavenTest extends CiVisibilityTest { def "test_maven_build_with_no_fork_generates_spans"() { given: - String[] args = ["clean", "test"] + String[] args = ["-B", "clean", "test"] String workingDirectory = projectFolder.toString() when: @@ -271,13 +266,13 @@ class MavenTest extends CiVisibilityTest { trace(2, true) { def testSessionId = testSessionSpan(it, 1, CIConstants.TEST_PASS, [:], "Maven Integration Tests Project", - "mvn clean test", - "maven:3.2.5" + "mvn -B clean test", + "maven:${expectedToolchainVersion()}" ) testModuleSpan(it, 0, testSessionId, CIConstants.TEST_PASS, [ - (Tags.TEST_COMMAND) : "mvn clean test", + (Tags.TEST_COMMAND) : "mvn -B clean test", (Tags.TEST_EXECUTION): "maven-surefire-plugin:test:default-test", ], null, "Maven Integration Tests Project maven-surefire-plugin default-test") @@ -297,7 +292,7 @@ class MavenTest extends CiVisibilityTest { * before proceeding with running the build */ void givenMavenDependenciesAreLoaded() { - String[] args = ["dependency:go-offline"] + String[] args = ["org.apache.maven.plugins:maven-dependency-plugin:go-offline"] String workingDirectory = projectFolder.toString() for (int attempt = 0; attempt < DEPENDENCIES_DOWNLOAD_RETRIES; attempt++) { def exitCode = new MavenCli().doMain(args, workingDirectory, null, null) @@ -308,6 +303,10 @@ class MavenTest extends CiVisibilityTest { throw new AssertionError((Object) "Tried to download dependencies $DEPENDENCIES_DOWNLOAD_RETRIES times and failed") } + String expectedToolchainVersion() { + return MavenCli.getPackage().getImplementationVersion() + } + @Override String expectedOperationPrefix() { return "maven" diff --git a/dd-java-agent/instrumentation/maven-3.2.1/src/test/resources/test_maven_build_with_unit_and_integration_tests_generates_spans/pom.xml b/dd-java-agent/instrumentation/maven-3.2.1/src/test/resources/test_maven_build_with_unit_and_integration_tests_generates_spans/pom.xml index 783a1d84cab8..512573fc3248 100644 --- a/dd-java-agent/instrumentation/maven-3.2.1/src/test/resources/test_maven_build_with_unit_and_integration_tests_generates_spans/pom.xml +++ b/dd-java-agent/instrumentation/maven-3.2.1/src/test/resources/test_maven_build_with_unit_and_integration_tests_generates_spans/pom.xml @@ -50,7 +50,6 @@ maven-failsafe-plugin - 2.22.0 From 94625c559d5ce2d089640d0b878639e78cfb8d38 Mon Sep 17 00:00:00 2001 From: Brian Devins-Suresh Date: Thu, 12 Oct 2023 13:35:45 -0400 Subject: [PATCH 13/29] Add DSM implementation for kinesis in SDKv2 (#5966) * Add DSM implementation for kinesis * simplify testing with http2 --------- Co-authored-by: Andrea Marziali --- .../aws-java-sdk-2.2/build.gradle | 19 +- .../groovy/Aws2KinesisDataStreamsTest.groovy | 369 ++++++++++++++++++ .../aws/v2/AwsSdkClientDecorator.java | 198 ++++++++-- .../aws/v2/TracingExecutionInterceptor.java | 10 +- .../src/test/groovy/Aws2ClientTest.groovy | 2 +- .../RecordingDatastreamsPayloadWriter.groovy | 4 +- .../test/server/http/TestHttpServer.groovy | 7 + .../test/groovy/server/HttpServerTest.groovy | 2 +- 8 files changed, 562 insertions(+), 49 deletions(-) create mode 100644 dd-java-agent/instrumentation/aws-java-sdk-2.2/src/kinesisDsmTest/groovy/Aws2KinesisDataStreamsTest.groovy diff --git a/dd-java-agent/instrumentation/aws-java-sdk-2.2/build.gradle b/dd-java-agent/instrumentation/aws-java-sdk-2.2/build.gradle index 1335d28286de..c5fe90b23122 100644 --- a/dd-java-agent/instrumentation/aws-java-sdk-2.2/build.gradle +++ b/dd-java-agent/instrumentation/aws-java-sdk-2.2/build.gradle @@ -1,4 +1,3 @@ - muzzle { pass { group = "software.amazon.awssdk" @@ -11,7 +10,14 @@ muzzle { apply from: "$rootDir/gradle/java.gradle" -addTestSuite('latestDepTest') +addTestSuiteForDir('latestDepTest', 'test') +// Broken: at some point S3 moved the bucket name to the hostname resulting in host not found somebucket.localhost on all S3 tests +// addTestSuiteExtendingForDir('latestDepForkedTest', 'latestDepTest', 'test') + +addTestSuite('kinesisDsmTest') +addTestSuiteExtendingForDir('kinesisDsmForkedTest', 'kinesisDsmTest', 'kinesisDsmTest') +addTestSuiteForDir('latestKinesisDsmTest', 'kinesisDsmTest') +addTestSuiteExtendingForDir('latestKinesisDsmForkedTest', 'latestKinesisDsmTest', 'kinesisDsmTest') def fixedSdkVersion = '2.20.33' // 2.20.34 is missing and breaks IDEA import @@ -31,6 +37,15 @@ dependencies { testImplementation group: 'software.amazon.awssdk', name: 'dynamodb', version: '2.2.0' testImplementation group: 'software.amazon.awssdk', name: 'kinesis', version: '2.2.0' + testImplementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '9.3.0.v20150612' + testImplementation group: 'org.eclipse.jetty.http2', name: 'http2-server', version: '9.3.0.v20150612' + + // First version where dsm traced operations have required StreamARN parameter + kinesisDsmTestImplementation group: 'software.amazon.awssdk', name: 'apache-client', version: '2.18.40' + kinesisDsmTestImplementation group: 'software.amazon.awssdk', name: 'kinesis', version: '2.18.40' + latestKinesisDsmTestImplementation group: 'software.amazon.awssdk', name: 'apache-client', version: '+' + latestKinesisDsmTestImplementation group: 'software.amazon.awssdk', name: 'kinesis', version: '+' + latestDepTestImplementation project(':dd-java-agent:instrumentation:apache-httpclient-4') latestDepTestImplementation project(':dd-java-agent:instrumentation:netty-4.1') diff --git a/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/kinesisDsmTest/groovy/Aws2KinesisDataStreamsTest.groovy b/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/kinesisDsmTest/groovy/Aws2KinesisDataStreamsTest.groovy new file mode 100644 index 000000000000..2c5f808d857d --- /dev/null +++ b/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/kinesisDsmTest/groovy/Aws2KinesisDataStreamsTest.groovy @@ -0,0 +1,369 @@ +import datadog.trace.agent.test.naming.VersionedNamingTestBase +import datadog.trace.api.Config +import datadog.trace.api.DDSpanTypes +import datadog.trace.api.DDTags +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.core.datastreams.StatsGroup +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory +import org.eclipse.jetty.server.HttpConfiguration +import org.eclipse.jetty.server.HttpConnectionFactory +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.ServerConnector +import org.eclipse.jetty.server.SslConnectionFactory +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.core.ResponseInputStream +import software.amazon.awssdk.core.SdkBytes +import software.amazon.awssdk.core.interceptor.Context +import software.amazon.awssdk.core.interceptor.ExecutionAttributes +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.kinesis.KinesisAsyncClient +import software.amazon.awssdk.services.kinesis.KinesisClient +import software.amazon.awssdk.services.kinesis.model.GetRecordsRequest +import software.amazon.awssdk.services.kinesis.model.PutRecordRequest +import software.amazon.awssdk.services.kinesis.model.PutRecordsRequest +import software.amazon.awssdk.services.kinesis.model.PutRecordsRequestEntry +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Unroll + +import java.time.Instant +import java.util.concurrent.Future +import java.util.concurrent.atomic.AtomicReference + +import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer + +abstract class Aws2KinesisDataStreamsTest extends VersionedNamingTestBase { + + private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = StaticCredentialsProvider + .create(AwsBasicCredentials.create("my-access-key", "my-secret-key")) + + @Shared + def responseBody = new AtomicReference() + @Shared + def servedRequestId = new AtomicReference() + + @Shared + def timestamp = Instant.now().minusSeconds(60) + + @Shared + def timestamp2 = timestamp.plusSeconds(1) + + @AutoCleanup + @Shared + def server = httpServer { + customizer { { + Server server -> { + ServerConnector httpConnector = server.getConnectors().find { + !it.connectionFactories.any { + it instanceof SslConnectionFactory + } + } + HttpConfiguration config = (httpConnector.connectionFactories.find { + it instanceof HttpConnectionFactory + } + as HttpConnectionFactory).getHttpConfiguration() + httpConnector.addConnectionFactory(new HTTP2CServerConnectionFactory(config)) + } + } + } + handlers { + all { + response + .status(200) + .addHeader("x-amzn-RequestId", servedRequestId.get()) + .sendWithType("application/x-amz-json-1.1", responseBody.get()) + } + } + } + + @Override + protected void configurePreAgent() { + super.configurePreAgent() + // the actual service returns cbor encoded json + System.setProperty("aws.cborEnabled", "false") + } + + @Override + String operation() { + null + } + + @Override + String service() { + null + } + + @Override + protected boolean isDataStreamsEnabled() { + true + } + + abstract String expectedOperation(String awsService, String awsOperation) + + abstract String expectedService(String awsService, String awsOperation) + + def watch(builder, callback) { + builder.addExecutionInterceptor(new ExecutionInterceptor() { + @Override + void afterExecution(Context.AfterExecution context, ExecutionAttributes executionAttributes) { + callback.call() + } + }) + } + + @Unroll + def "send #operation request with builder #builder.class.getSimpleName() mocked response"() { + setup: + boolean executed = false + def client = builder + // tests that our instrumentation doesn't disturb any overridden configuration + .overrideConfiguration({ watch(it, { executed = true }) }) + .endpointOverride(server.address) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build() + responseBody.set(body) + servedRequestId.set(requestId) + when: + def response = call.call(client) + + if (response instanceof Future) { + response = response.get() + } + TEST_WRITER.waitForTraces(1) + TEST_DATA_STREAMS_WRITER.waitForGroups(1) + + then: + executed + response != null + response.class.simpleName.startsWith(operation) || response instanceof ResponseInputStream + + assertTraces(1) { + trace(1) { + span { + serviceName expectedService(service, operation) + operationName expectedOperation(service, operation) + resourceName "$service.$operation" + spanType DDSpanTypes.HTTP_CLIENT + errored false + measured true + parent() + tags { + def checkPeerService = false + "$Tags.COMPONENT" "java-aws-sdk" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.PEER_HOSTNAME" "localhost" + "$Tags.PEER_PORT" server.address.port + "$Tags.HTTP_URL" "${server.address}${path}" + "$Tags.HTTP_METHOD" "$method" + "$Tags.HTTP_STATUS" 200 + "aws.service" "$service" + "aws_service" "$service" + "aws.operation" "${operation}" + "aws.agent" "java-aws-sdk" + "aws.requestId" "$requestId" + "aws.stream.name" "somestream" + "streamname" "somestream" + "$DDTags.PATHWAY_HASH" { String } + peerServiceFrom("aws.stream.name") + checkPeerService = true + defaultTags(false, checkPeerService) + } + } + } + } + + and: + StatsGroup first = TEST_DATA_STREAMS_WRITER.groups.find { it.parentHash == 0 } + verifyAll(first) { + edgeTags.containsAll(["direction:" + dsmDirection, "topic:arnprefix:stream/somestream", "type:kinesis"]) + edgeTags.size() == 3 + pathwayLatency.count == dsmStatCount + edgeLatency.count == dsmStatCount + } + + cleanup: + servedRequestId.set(null) + + where: + service | operation | dsmDirection | dsmStatCount | method | path | requestId | builder | call | body + "Kinesis" | "GetRecords" | "in" | 1 | "POST" | "/" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | KinesisClient.builder() | { KinesisClient c -> c.getRecords(GetRecordsRequest.builder().streamARN("arnprefix:stream/somestream").build()) } | """{ + "MillisBehindLatest": 2100, + "NextShardIterator": "AAA", + "Records": [ + { + "Data": "XzxkYXRhPl8w", + "PartitionKey": "partitionKey", + "ApproximateArrivalTimestamp": ${timestamp.toEpochMilli()}, + "SequenceNumber": "21269319989652663814458848515492872193" + } + ] +}""" + "Kinesis" | "GetRecords" | "in" | 2 | "POST" | "/" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | KinesisClient.builder() | { KinesisClient c -> c.getRecords(GetRecordsRequest.builder().streamARN("arnprefix:stream/somestream").build()) } | """{ + "MillisBehindLatest": 2100, + "NextShardIterator": "AAA", + "Records": [ + { + "Data": "XzxkYXRhPl8w", + "PartitionKey": "partitionKey", + "ApproximateArrivalTimestamp": ${timestamp.toEpochMilli()}, + "SequenceNumber": "21269319989652663814458848515492872193" + }, + { + "Data": "XzxkYXRhPl8w", + "PartitionKey": "partitionKey", + "ApproximateArrivalTimestamp": ${timestamp2.toEpochMilli()}, + "SequenceNumber": "21269319989652663814458848515492872193" + } + ] +}""" + "Kinesis" | "PutRecord" | "out" | 1 | "POST" | "/" | "UNKNOWN" | KinesisClient.builder() | { KinesisClient c -> c.putRecord(PutRecordRequest.builder().streamARN("arnprefix:stream/somestream").data(SdkBytes.fromUtf8String("message")).build()) } | "" + "Kinesis" | "PutRecords" | "out" | 1 | "POST" | "/" | "UNKNOWN" | KinesisClient.builder() | { KinesisClient c -> c.putRecords(PutRecordsRequest.builder().streamARN("arnprefix:stream/somestream").records(PutRecordsRequestEntry.builder().data(SdkBytes.fromUtf8String("message")).build()).build()) } | "" + "Kinesis" | "PutRecords" | "out" | 2 | "POST" | "/" | "UNKNOWN" | KinesisClient.builder() | { KinesisClient c -> c.putRecords(PutRecordsRequest.builder().streamARN("arnprefix:stream/somestream").records(PutRecordsRequestEntry.builder().data(SdkBytes.fromUtf8String("message")).build(), PutRecordsRequestEntry.builder().data(SdkBytes.fromUtf8String("message")).build()).build()) } | "" + } + + def "send #operation async request with builder #builder.class.getSimpleName() mocked response"() { + setup: + boolean executed = false + def client = builder + // tests that our instrumentation doesn't disturb any overridden configuration + .overrideConfiguration({ watch(it, { executed = true }) }) + .endpointOverride(server.address) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build() + responseBody.set(body) + servedRequestId.set(requestId) + when: + def response = call.call(client) + + if (response instanceof Future) { + response = response.get() + } + TEST_WRITER.waitForTraces(1) + TEST_DATA_STREAMS_WRITER.waitForGroups(1) + + then: + executed + response != null + + assertTraces(1) { + trace(1) { + span { + serviceName expectedService(service, operation) + operationName expectedOperation(service, operation) + resourceName "$service.$operation" + spanType DDSpanTypes.HTTP_CLIENT + errored false + measured true + parent() + tags { + "$Tags.COMPONENT" "java-aws-sdk" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.PEER_HOSTNAME" "localhost" + "$Tags.PEER_PORT" server.address.port + "$Tags.HTTP_URL" "${server.address}${path}" + "$Tags.HTTP_METHOD" "$method" + "$Tags.HTTP_STATUS" 200 + "aws.service" "$service" + "aws_service" "$service" + "aws.operation" "${operation}" + "aws.agent" "java-aws-sdk" + "aws.requestId" "$requestId" + "aws.stream.name" "somestream" + "streamname" "somestream" + "$DDTags.PATHWAY_HASH" { String } + peerServiceFrom("aws.stream.name") + defaultTags(false, true) + } + } + } + } + + and: + StatsGroup first = TEST_DATA_STREAMS_WRITER.groups.find { it.parentHash == 0 } + verifyAll(first) { + edgeTags.containsAll(["direction:" + dsmDirection, "topic:arnprefix:stream/somestream", "type:kinesis"]) + edgeTags.size() == 3 + pathwayLatency.count == dsmStatCount + edgeLatency.count == dsmStatCount + } + + cleanup: + servedRequestId.set(null) + + where: + service | operation | dsmDirection | dsmStatCount | method | path | requestId | builder | call | body + "Kinesis" | "GetRecords" | "in" | 1 | "POST" | "/" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | KinesisAsyncClient.builder() | { KinesisAsyncClient c -> c.getRecords(GetRecordsRequest.builder().streamARN("arnprefix:stream/somestream").build()) } | """{ + "MillisBehindLatest": 2100, + "NextShardIterator": "AAA", + "Records": [ + { + "Data": "XzxkYXRhPl8w", + "PartitionKey": "partitionKey", + "ApproximateArrivalTimestamp": ${timestamp.toEpochMilli()}, + "SequenceNumber": "21269319989652663814458848515492872193" + } + ] +}""" + "Kinesis" | "GetRecords" | "in" | 2 | "POST" | "/" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | KinesisAsyncClient.builder() | { KinesisAsyncClient c -> c.getRecords(GetRecordsRequest.builder().streamARN("arnprefix:stream/somestream").build()) } | """{ + "MillisBehindLatest": 2100, + "NextShardIterator": "AAA", + "Records": [ + { + "Data": "XzxkYXRhPl8w", + "PartitionKey": "partitionKey", + "ApproximateArrivalTimestamp": ${timestamp.toEpochMilli()}, + "SequenceNumber": "21269319989652663814458848515492872193" + }, + { + "Data": "XzxkYXRhPl8w", + "PartitionKey": "partitionKey", + "ApproximateArrivalTimestamp": ${timestamp2.toEpochMilli()}, + "SequenceNumber": "21269319989652663814458848515492872193" + } + ] +}""" + "Kinesis" | "PutRecord" | "out" | 1 | "POST" | "/" | "UNKNOWN" | KinesisAsyncClient.builder() | { KinesisAsyncClient c -> c.putRecord(PutRecordRequest.builder().streamARN("arnprefix:stream/somestream").data(SdkBytes.fromUtf8String("message")).build()) } | "" + "Kinesis" | "PutRecords" | "out" | 1 | "POST" | "/" | "UNKNOWN" | KinesisAsyncClient.builder() | { KinesisAsyncClient c -> c.putRecords(PutRecordsRequest.builder().streamARN("arnprefix:stream/somestream").records(PutRecordsRequestEntry.builder().data(SdkBytes.fromUtf8String("message")).build()).build()) } | "" + "Kinesis" | "PutRecords" | "out" | 2 | "POST" | "/" | "UNKNOWN" | KinesisAsyncClient.builder() | { KinesisAsyncClient c -> c.putRecords(PutRecordsRequest.builder().streamARN("arnprefix:stream/somestream").records(PutRecordsRequestEntry.builder().data(SdkBytes.fromUtf8String("message")).build(), PutRecordsRequestEntry.builder().data(SdkBytes.fromUtf8String("message")).build()).build()) } | "" + } +} + +class Aws2KinesisDataStreamsV0Test extends Aws2KinesisDataStreamsTest { + + @Override + String expectedOperation(String awsService, String awsOperation) { + "aws.http" + } + + @Override + String expectedService(String awsService, String awsOperation) { + return "java-aws-sdk" + } + + @Override + int version() { + 0 + } +} + +class Aws2KinesisDataStreamsV1ForkedTest extends Aws2KinesisDataStreamsTest { + + @Override + String expectedOperation(String awsService, String awsOperation) { + return "aws.${awsService.toLowerCase()}.request" + } + + @Override + String expectedService(String awsService, String awsOperation) { + Config.get().getServiceName() + } + + @Override + int version() { + 1 + } +} diff --git a/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java b/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java index 2f4fb8d826e5..bc50763d0ff2 100644 --- a/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java +++ b/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java @@ -1,22 +1,42 @@ package datadog.trace.instrumentation.aws.v2; +import static datadog.trace.core.datastreams.TagsProcessor.DIRECTION_IN; +import static datadog.trace.core.datastreams.TagsProcessor.DIRECTION_TAG; +import static datadog.trace.core.datastreams.TagsProcessor.TOPIC_TAG; +import static datadog.trace.core.datastreams.TagsProcessor.TYPE_TAG; + import datadog.trace.api.Config; import datadog.trace.api.DDTags; import datadog.trace.api.cache.DDCache; import datadog.trace.api.cache.DDCaches; +import datadog.trace.api.experimental.DataStreamsContextCarrier.NoOp; import datadog.trace.api.naming.SpanNaming; +import datadog.trace.bootstrap.InstanceStore; +import datadog.trace.bootstrap.instrumentation.api.AgentDataStreamsMonitoring; import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; +import datadog.trace.bootstrap.instrumentation.api.PathwayContext; import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.bootstrap.instrumentation.decorator.HttpClientDecorator; import java.net.URI; +import java.time.Instant; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Optional; +import java.util.Set; import javax.annotation.Nonnull; import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.core.SdkField; +import software.amazon.awssdk.core.SdkPojo; import software.amazon.awssdk.core.SdkRequest; import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.core.interceptor.ExecutionAttribute; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; import software.amazon.awssdk.http.SdkHttpRequest; @@ -46,6 +66,22 @@ public class AwsSdkClientDecorator extends HttpClientDecorator KINESIS_PUT_RECORD_OPERATION_NAMES; + + static { + KINESIS_PUT_RECORD_OPERATION_NAMES = new HashSet<>(); + KINESIS_PUT_RECORD_OPERATION_NAMES.add("PutRecord"); + KINESIS_PUT_RECORD_OPERATION_NAMES.add("PutRecords"); + } + + public static final ExecutionAttribute KINESIS_STREAM_ARN_ATTRIBUTE = + InstanceStore.of(ExecutionAttribute.class) + .putIfAbsent("KinesisStreamArn", () -> new ExecutionAttribute<>("KinesisStreamArn")); + + // not static because this object would be ClassLoader specific if multiple SDK instances were + // loaded by different loaders + private SdkField kinesisApproximateArrivalTimestampField = null; + public CharSequence spanName(final ExecutionAttributes attributes) { final String awsServiceName = attributes.getAttribute(SdkExecutionAttribute.SERVICE_NAME); final String awsOperationName = attributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME); @@ -62,7 +98,12 @@ public CharSequence spanName(final ExecutionAttributes attributes) { "aws", attributes.getAttribute(SdkExecutionAttribute.SERVICE_NAME), s)); } - public AgentSpan onSdkRequest(final AgentSpan span, final SdkRequest request) { + public AgentSpan onSdkRequest( + final AgentSpan span, final SdkRequest request, final ExecutionAttributes attributes) { + final String awsServiceName = attributes.getAttribute(SdkExecutionAttribute.SERVICE_NAME); + final String awsOperationName = attributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME); + onOperation(span, awsServiceName, awsOperationName); + // S3 request.getValueForField("Bucket", String.class).ifPresent(name -> setBucketName(span, name)); request @@ -89,10 +130,74 @@ public AgentSpan onSdkRequest(final AgentSpan span, final SdkRequest request) { request .getValueForField("StreamName", String.class) .ifPresent(name -> setStreamName(span, name)); + Optional kinesisStreamArn = request.getValueForField("StreamARN", String.class); + kinesisStreamArn.ifPresent( + streamArn -> { + if (span.traceConfig().isDataStreamsEnabled()) { + attributes.putAttribute(KINESIS_STREAM_ARN_ATTRIBUTE, streamArn); + } + int streamNameStart = streamArn.indexOf(":stream/"); + if (streamNameStart >= 0) { + setStreamName(span, streamArn.substring(streamNameStart + 8)); + } + }); // DynamoDB request.getValueForField("TableName", String.class).ifPresent(name -> setTableName(span, name)); + // DSM + if (span.traceConfig().isDataStreamsEnabled() + && kinesisStreamArn.isPresent() + && "kinesis".equalsIgnoreCase(awsServiceName) + && KINESIS_PUT_RECORD_OPERATION_NAMES.contains(awsOperationName)) { + // https://github.com/DataDog/dd-trace-py/blob/864abb6c99e1cb0449904260bac93e8232261f2a/ddtrace/contrib/botocore/patch.py#L368 + List records = + request + .getValueForField("Records", List.class) + .orElse(Collections.singletonList(request)); // For PutRecord use request + + for (Object ignored : records) { + AgentTracer.get() + .getDataStreamsMonitoring() + .setProduceCheckpoint("kinesis", kinesisStreamArn.get(), NoOp.INSTANCE); + } + } + + return span; + } + + private static AgentSpan onOperation( + final AgentSpan span, final String awsServiceName, final String awsOperationName) { + String awsRequestName = awsServiceName + "." + awsOperationName; + span.setResourceName(awsRequestName, RESOURCE_NAME_PRIORITY); + + switch (awsRequestName) { + case "Sqs.SendMessage": + case "Sqs.SendMessageBatch": + case "Sqs.ReceiveMessage": + case "Sqs.DeleteMessage": + case "Sqs.DeleteMessageBatch": + if (SQS_SERVICE_NAME != null) { + span.setServiceName(SQS_SERVICE_NAME); + } + break; + case "Sns.PublishBatch": + case "Sns.Publish": + if (SNS_SERVICE_NAME != null) { + span.setServiceName(SNS_SERVICE_NAME); + } + break; + default: + if (GENERIC_SERVICE_NAME != null) { + span.setServiceName(GENERIC_SERVICE_NAME); + } + break; + } + span.setTag(InstrumentationTags.AWS_AGENT, COMPONENT_NAME); + span.setTag(InstrumentationTags.AWS_SERVICE, awsServiceName); + span.setTag(InstrumentationTags.TOP_LEVEL_AWS_SERVICE, awsServiceName); + span.setTag(InstrumentationTags.AWS_OPERATION, awsOperationName); + return span; } @@ -134,49 +239,62 @@ private static void setTableName(AgentSpan span, String name) { setPeerService(span, InstrumentationTags.AWS_TABLE_NAME, name); } - public AgentSpan onAttributes(final AgentSpan span, final ExecutionAttributes attributes) { - final String awsServiceName = attributes.getAttribute(SdkExecutionAttribute.SERVICE_NAME); - final String awsOperationName = attributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME); - - String awsRequestName = awsServiceName + "." + awsOperationName; - span.setResourceName(awsRequestName, RESOURCE_NAME_PRIORITY); - - switch (awsRequestName) { - case "Sqs.SendMessage": - case "Sqs.SendMessageBatch": - case "Sqs.ReceiveMessage": - case "Sqs.DeleteMessage": - case "Sqs.DeleteMessageBatch": - if (SQS_SERVICE_NAME != null) { - span.setServiceName(SQS_SERVICE_NAME); - } - break; - case "Sns.PublishBatch": - case "Sns.Publish": - if (SNS_SERVICE_NAME != null) { - span.setServiceName(SNS_SERVICE_NAME); - } - break; - default: - if (GENERIC_SERVICE_NAME != null) { - span.setServiceName(GENERIC_SERVICE_NAME); - } - break; - } - span.setTag(InstrumentationTags.AWS_AGENT, COMPONENT_NAME); - span.setTag(InstrumentationTags.AWS_SERVICE, awsServiceName); - span.setTag(InstrumentationTags.TOP_LEVEL_AWS_SERVICE, awsServiceName); - span.setTag(InstrumentationTags.AWS_OPERATION, awsOperationName); - - return span; - } - - // Not overriding the super. Should call both with each type of response. - public AgentSpan onResponse(final AgentSpan span, final SdkResponse response) { + public AgentSpan onSdkResponse( + final AgentSpan span, final SdkResponse response, final ExecutionAttributes attributes) { if (response instanceof AwsResponse) { span.setTag( InstrumentationTags.AWS_REQUEST_ID, ((AwsResponse) response).responseMetadata().requestId()); + + final String awsServiceName = attributes.getAttribute(SdkExecutionAttribute.SERVICE_NAME); + final String awsOperationName = attributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME); + if (span.traceConfig().isDataStreamsEnabled() + && "kinesis".equalsIgnoreCase(awsServiceName) + && "GetRecords".equals(awsOperationName)) { + // https://github.com/DataDog/dd-trace-py/blob/864abb6c99e1cb0449904260bac93e8232261f2a/ddtrace/contrib/botocore/patch.py#L350 + String streamArn = attributes.getAttribute(KINESIS_STREAM_ARN_ATTRIBUTE); + if (null != streamArn) { + response + .getValueForField("Records", List.class) + .ifPresent( + recordsRaw -> { + //noinspection unchecked + List records = (List) recordsRaw; + if (!records.isEmpty()) { + LinkedHashMap sortedTags = new LinkedHashMap<>(); + sortedTags.put(DIRECTION_TAG, DIRECTION_IN); + sortedTags.put(TOPIC_TAG, streamArn); + sortedTags.put(TYPE_TAG, "kinesis"); + if (null == kinesisApproximateArrivalTimestampField) { + Optional> maybeField = + records.get(0).sdkFields().stream() + .filter(f -> f.locationName().equals("ApproximateArrivalTimestamp")) + .findFirst(); + if (maybeField.isPresent()) { + //noinspection unchecked + kinesisApproximateArrivalTimestampField = + (SdkField) maybeField.get(); + } else { + // shouldn't be possible + return; + } + } + for (SdkPojo record : records) { + Instant arrivalTime = + kinesisApproximateArrivalTimestampField.getValueOrDefault(record); + AgentDataStreamsMonitoring dataStreamsMonitoring = + AgentTracer.get().getDataStreamsMonitoring(); + PathwayContext pathwayContext = dataStreamsMonitoring.newPathwayContext(); + pathwayContext.setCheckpoint( + sortedTags, dataStreamsMonitoring::add, arrivalTime.toEpochMilli()); + if (!span.context().getPathwayContext().isStarted()) { + span.context().mergePathwayContext(pathwayContext); + } + } + } + }); + } + } } return span; } diff --git a/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/TracingExecutionInterceptor.java b/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/TracingExecutionInterceptor.java index 2dbc7cca6c26..161bad29efa3 100644 --- a/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/TracingExecutionInterceptor.java +++ b/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/TracingExecutionInterceptor.java @@ -11,6 +11,7 @@ import datadog.trace.api.TracePropagationStyle; import datadog.trace.bootstrap.ContextStore; import datadog.trace.bootstrap.InstanceStore; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,9 +55,10 @@ public void afterMarshalling( final Context.AfterMarshalling context, final ExecutionAttributes executionAttributes) { final AgentSpan span = executionAttributes.getAttribute(SPAN_ATTRIBUTE); if (span != null) { - DECORATE.onRequest(span, context.httpRequest()); - DECORATE.onSdkRequest(span, context.request()); - DECORATE.onAttributes(span, executionAttributes); + try (AgentScope ignored = activateSpan(span)) { + DECORATE.onRequest(span, context.httpRequest()); + DECORATE.onSdkRequest(span, context.request(), executionAttributes); + } } } @@ -103,7 +105,7 @@ public void afterExecution( if (span != null) { executionAttributes.putAttribute(SPAN_ATTRIBUTE, null); // Call onResponse on both types of responses: - DECORATE.onResponse(span, context.response()); + DECORATE.onSdkResponse(span, context.response(), executionAttributes); DECORATE.onResponse(span, context.httpResponse()); DECORATE.beforeFinish(span); span.finish(); diff --git a/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy b/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy index 997827b3f216..b9a6ec826fd5 100644 --- a/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy +++ b/dd-java-agent/instrumentation/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy @@ -408,7 +408,7 @@ abstract class Aws2ClientTest extends VersionedNamingTestBase { } } -class Aws2ClientV0Test extends Aws2ClientTest { +class Aws2ClientV0ForkedTest extends Aws2ClientTest { @Override String expectedOperation(String awsService, String awsOperation) { diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/datastreams/RecordingDatastreamsPayloadWriter.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/datastreams/RecordingDatastreamsPayloadWriter.groovy index b231b1edc113..4ddb26b51afa 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/datastreams/RecordingDatastreamsPayloadWriter.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/datastreams/RecordingDatastreamsPayloadWriter.groovy @@ -3,10 +3,11 @@ package datadog.trace.agent.test.datastreams import datadog.trace.core.datastreams.DatastreamsPayloadWriter import datadog.trace.core.datastreams.StatsBucket import datadog.trace.core.datastreams.StatsGroup - +import groovy.util.logging.Slf4j import java.util.concurrent.TimeUnit +@Slf4j class RecordingDatastreamsPayloadWriter implements DatastreamsPayloadWriter { @SuppressWarnings('UnusedPrivateField') // bug in codenarc private final List payloads = [] @@ -19,6 +20,7 @@ class RecordingDatastreamsPayloadWriter implements DatastreamsPayloadWriter { @Override synchronized void writePayload(Collection data) { + log.info("payload written - {}", data) this.@payloads.addAll(data) data.each { this.@groups.addAll(it.groups) } for (StatsBucket bucket : data) { diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/TestHttpServer.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/TestHttpServer.groovy index be61e024468c..1a9d89f63e4b 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/TestHttpServer.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/TestHttpServer.groovy @@ -55,6 +55,7 @@ class TestHttpServer implements AutoCloseable { private final Server internalServer private HandlersSpec handlers + private Closure customizer = {} public String keystorePath private URI address @@ -121,6 +122,8 @@ class TestHttpServer implements AutoCloseable { https.setPort(0) internalServer.addConnector(https) + customizer.call(internalServer) + internalServer.start() // set after starting, otherwise two callbacks get added. internalServer.stopAtShutdown = true @@ -194,6 +197,10 @@ class TestHttpServer implements AutoCloseable { return last.get() } + void customizer(Closure spec) { + this.customizer = spec.call() + } + void handlers(@DelegatesTo(value = HandlersSpec, strategy = Closure.DELEGATE_FIRST) Closure spec) { assert handlers == null: "handlers already defined" handlers = new HandlersSpec() diff --git a/dd-java-agent/testing/src/test/groovy/server/HttpServerTest.groovy b/dd-java-agent/testing/src/test/groovy/server/HttpServerTest.groovy index a7bff72db730..3748330b054c 100644 --- a/dd-java-agent/testing/src/test/groovy/server/HttpServerTest.groovy +++ b/dd-java-agent/testing/src/test/groovy/server/HttpServerTest.groovy @@ -68,7 +68,7 @@ class HttpServerTest extends AgentTestRunner { then: clientResponse.code() == 404 - clientResponse.body().string().contains("Error 404 ") + clientResponse.body().string().contains("Error 404") cleanup: server.stop() From 534aff13cf81de38c911a1968a442880c2f39b31 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch <stuart.mcculloch@datadoghq.com> Date: Fri, 13 Oct 2023 09:48:47 +0100 Subject: [PATCH 14/29] Support setting 'noParent=true' on @Trace annotation to always start a new trace at that method, even when there's an existing trace. (#6032) --- .../trace_annotation/TraceDecorator.java | 13 +- .../test/groovy/TraceAnnotationsTest.groovy | 122 ++++++++++++++++++ .../test/trace/annotation/SayTracedHello.java | 12 ++ .../main/java/datadog/trace/api/Trace.java | 3 + 4 files changed, 149 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/instrumentation/trace-annotation/src/main/java/datadog/trace/instrumentation/trace_annotation/TraceDecorator.java b/dd-java-agent/instrumentation/trace-annotation/src/main/java/datadog/trace/instrumentation/trace_annotation/TraceDecorator.java index 7d2a93a06bbc..984ed51daba3 100644 --- a/dd-java-agent/instrumentation/trace-annotation/src/main/java/datadog/trace/instrumentation/trace_annotation/TraceDecorator.java +++ b/dd-java-agent/instrumentation/trace-annotation/src/main/java/datadog/trace/instrumentation/trace_annotation/TraceDecorator.java @@ -46,6 +46,7 @@ public AgentSpan startMethodSpan(Method method) { CharSequence operationName = null; CharSequence resourceName = null; boolean measured = false; + boolean noParent = false; Trace traceAnnotation = method.getAnnotation(Trace.class); if (null != traceAnnotation) { @@ -61,6 +62,12 @@ public AgentSpan startMethodSpan(Method method) { } catch (Throwable ignore) { // dd-trace-api < 1.10.0 on classpath } + + try { + noParent = traceAnnotation.noParent(); + } catch (Throwable ignore) { + // dd-trace-api < 1.22.0 on classpath + } } if (operationName == null || operationName.length() == 0) { @@ -75,7 +82,11 @@ public AgentSpan startMethodSpan(Method method) { resourceName = DECORATE.spanNameForMethod(method); } - AgentSpan span = startSpan(INSTRUMENTATION_NAME, operationName); + AgentSpan span = + noParent + ? startSpan(INSTRUMENTATION_NAME, operationName, null) + : startSpan(INSTRUMENTATION_NAME, operationName); + DECORATE.afterStart(span); span.setResourceName(resourceName); diff --git a/dd-java-agent/instrumentation/trace-annotation/src/test/groovy/TraceAnnotationsTest.groovy b/dd-java-agent/instrumentation/trace-annotation/src/test/groovy/TraceAnnotationsTest.groovy index 7f03155e76e3..e8acea20b832 100644 --- a/dd-java-agent/instrumentation/trace-annotation/src/test/groovy/TraceAnnotationsTest.groovy +++ b/dd-java-agent/instrumentation/trace-annotation/src/test/groovy/TraceAnnotationsTest.groovy @@ -1,4 +1,5 @@ import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.agent.test.utils.TraceUtils import datadog.trace.api.Trace import datadog.trace.bootstrap.instrumentation.api.Tags import dd.test.trace.annotation.SayTracedHello @@ -388,5 +389,126 @@ class TraceAnnotationsTest extends AgentTestRunner { } } } + + def "test measured attribute"() { + setup: + TraceUtils.runUnderTrace("parent", () -> { + SayTracedHello.sayHello() + SayTracedHello.sayHelloMeasured() + SayTracedHello.sayHello() + }) + + expect: + assertTraces(1) { + trace(4) { + span { + hasServiceName() + resourceName "parent" + operationName "parent" + parent() + errored false + tags { + defaultTags() + } + } + span { + serviceName "test" + resourceName "SayTracedHello.sayHello" + operationName "trace.annotation" + childOf(span(0)) + errored false + measured false + tags { + "$Tags.COMPONENT" "trace" + defaultTags() + } + } + span { + serviceName "test" + resourceName "SayTracedHello.sayHelloMeasured" + operationName "trace.annotation" + childOf(span(0)) + errored false + measured true + tags { + "$Tags.COMPONENT" "trace" + defaultTags() + } + } + span { + serviceName "test" + resourceName "SayTracedHello.sayHello" + operationName "trace.annotation" + childOf(span(0)) + errored false + measured false + tags { + "$Tags.COMPONENT" "trace" + defaultTags() + } + } + } + } + } + + def "test noParent attribute"() { + setup: + TraceUtils.runUnderTrace("parent", () -> { + SayTracedHello.sayHello() + SayTracedHello.sayHelloNoParent() + SayTracedHello.sayHello() + }) + + expect: + assertTraces(2) { + trace(3) { + span { + hasServiceName() + resourceName "parent" + operationName "parent" + parent() + errored false + tags { + defaultTags() + } + } + span { + serviceName "test" + resourceName "SayTracedHello.sayHello" + operationName "trace.annotation" + childOf(span(0)) + errored false + tags { + "$Tags.COMPONENT" "trace" + defaultTags() + } + } + span { + serviceName "test" + resourceName "SayTracedHello.sayHello" + operationName "trace.annotation" + childOf(span(0)) + errored false + tags { + "$Tags.COMPONENT" "trace" + defaultTags() + } + } + } + trace(1) { + span { + serviceName "test" + resourceName "SayTracedHello.sayHelloNoParent" + operationName "trace.annotation" + parent() + errored false + tags { + "$Tags.COMPONENT" "trace" + defaultTags() + } + } + } + } + } } diff --git a/dd-java-agent/instrumentation/trace-annotation/src/test/java/dd/test/trace/annotation/SayTracedHello.java b/dd-java-agent/instrumentation/trace-annotation/src/test/java/dd/test/trace/annotation/SayTracedHello.java index 861ab301276b..24c4468ec9e7 100644 --- a/dd-java-agent/instrumentation/trace-annotation/src/test/java/dd/test/trace/annotation/SayTracedHello.java +++ b/dd-java-agent/instrumentation/trace-annotation/src/test/java/dd/test/trace/annotation/SayTracedHello.java @@ -54,6 +54,18 @@ public static String sayHELLOsayHAMixedResourceChildren() { return sayHello() + sayHAWithResource(); } + @Trace(measured = true) + public static String sayHelloMeasured() { + activeSpan().setTag(DDTags.SERVICE_NAME, "test"); + return "hello!"; + } + + @Trace(noParent = true) + public static String sayHelloNoParent() { + activeSpan().setTag(DDTags.SERVICE_NAME, "test"); + return "hello!"; + } + @Trace(operationName = "ERROR") public static String sayERROR() { throw new RuntimeException(); diff --git a/dd-trace-api/src/main/java/datadog/trace/api/Trace.java b/dd-trace-api/src/main/java/datadog/trace/api/Trace.java index b6fb94a67a79..d558e459e906 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/Trace.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/Trace.java @@ -19,4 +19,7 @@ /** Set whether to measure a trace. By default traces are not measured. */ boolean measured() default false; + + /** Set whether to start a new trace. By default it continues the current trace. */ + boolean noParent() default false; } From 0c30753f04f635a476fa143173a225dd111a9a7e Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko <121111529+nikita-tkachenko-datadog@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:22:34 +0200 Subject: [PATCH 15/29] Add support for AWS CodePipeline CI provider (#6027) --- .../civisibility/ci/AwsCodePipelineInfo.java | 29 +++++++++ .../ci/CIProviderInfoFactory.java | 3 + .../ci/AwsCodePipelineInfoTest.groovy | 30 +++++++++ .../test/resources/ci/awscodepipeline.json | 62 +++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AwsCodePipelineInfo.java create mode 100644 dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/AwsCodePipelineInfoTest.groovy create mode 100644 dd-java-agent/agent-ci-visibility/src/test/resources/ci/awscodepipeline.json diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AwsCodePipelineInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AwsCodePipelineInfo.java new file mode 100644 index 000000000000..4a0a9d299e65 --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AwsCodePipelineInfo.java @@ -0,0 +1,29 @@ +package datadog.trace.civisibility.ci; + +import datadog.trace.api.git.GitInfo; + +class AwsCodePipelineInfo implements CIProviderInfo { + + public static final String AWS_CODEPIPELINE = "CODEBUILD_INITIATOR"; + public static final String AWS_CODEPIPELINE_PROVIDER_NAME = "awscodepipeline"; + public static final String AWS_CODEPIPELINE_EXECUTION_ID = "DD_PIPELINE_EXECUTION_ID"; + public static final String AWS_CODEPIPELINE_ACTION_EXECUTION_ID = "DD_ACTION_EXECUTION_ID"; + public static final String AWS_CODEPIPELINE_ARN = "CODEBUILD_BUILD_ARN"; + + @Override + public GitInfo buildCIGitInfo() { + return GitInfo.NOOP; + } + + @Override + public CIInfo buildCIInfo() { + return CIInfo.builder() + .ciProviderName(AWS_CODEPIPELINE_PROVIDER_NAME) + .ciPipelineId(System.getenv(AWS_CODEPIPELINE_EXECUTION_ID)) + .ciEnvVars( + AWS_CODEPIPELINE_EXECUTION_ID, + AWS_CODEPIPELINE_ACTION_EXECUTION_ID, + AWS_CODEPIPELINE_ARN) + .build(); + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CIProviderInfoFactory.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CIProviderInfoFactory.java index f8356d57a024..37a651678b7e 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CIProviderInfoFactory.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CIProviderInfoFactory.java @@ -51,6 +51,9 @@ public CIProviderInfo createCIProviderInfo(Path currentPath) { return new CodefreshInfo(); } else if (System.getenv(TeamcityInfo.TEAMCITY) != null) { return new TeamcityInfo(); + } else if (System.getenv(AwsCodePipelineInfo.AWS_CODEPIPELINE) != null + && System.getenv(AwsCodePipelineInfo.AWS_CODEPIPELINE).startsWith("codepipeline")) { + return new AwsCodePipelineInfo(); } else { return new UnknownCIInfo(targetFolder, currentPath); } diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/AwsCodePipelineInfoTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/AwsCodePipelineInfoTest.groovy new file mode 100644 index 000000000000..bff684f4a000 --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/AwsCodePipelineInfoTest.groovy @@ -0,0 +1,30 @@ +package datadog.trace.civisibility.ci + +import java.nio.file.Path + +class AwsCodePipelineInfoTest extends CITagsProviderTest { + + @Override + String getProviderName() { + return AwsCodePipelineInfo.AWS_CODEPIPELINE_PROVIDER_NAME + } + + @Override + Map<String, String> buildRemoteGitInfoEmpty() { + final Map<String, String> map = new HashMap<>() + map.put(AwsCodePipelineInfo.AWS_CODEPIPELINE, "codepipeline") + return map + } + + @Override + Map<String, String> buildRemoteGitInfoMismatchLocalGit() { + final Map<String, String> map = new HashMap<>() + map.put(AwsCodePipelineInfo.AWS_CODEPIPELINE, "codepipeline") + return map + } + + @Override + Path getWorkspacePath() { + null + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/test/resources/ci/awscodepipeline.json b/dd-java-agent/agent-ci-visibility/src/test/resources/ci/awscodepipeline.json new file mode 100644 index 000000000000..6f3071331fe7 --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/test/resources/ci/awscodepipeline.json @@ -0,0 +1,62 @@ +[ + [ + { + "CODEBUILD_BUILD_ARN": "arn:aws:codebuild:eu-north-1:12345678:build/codebuild-demo-project:b1e6661e-e4f2-4156-9ab9-82a19", + "CODEBUILD_INITIATOR": "codepipeline/test-pipeline", + "DD_ACTION_EXECUTION_ID": "35519dc3-7c45-493c-9ba6-cd78ea11f69d", + "DD_GIT_BRANCH": "user-supplied-branch", + "DD_GIT_COMMIT_AUTHOR_DATE": "usersupplied-authordate", + "DD_GIT_COMMIT_AUTHOR_EMAIL": "usersupplied-authoremail", + "DD_GIT_COMMIT_AUTHOR_NAME": "usersupplied-authorname", + "DD_GIT_COMMIT_COMMITTER_DATE": "usersupplied-comitterdate", + "DD_GIT_COMMIT_COMMITTER_EMAIL": "usersupplied-comitteremail", + "DD_GIT_COMMIT_COMMITTER_NAME": "usersupplied-comittername", + "DD_GIT_COMMIT_MESSAGE": "usersupplied-message", + "DD_GIT_COMMIT_SHA": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", + "DD_GIT_REPOSITORY_URL": "git@github.com:DataDog/userrepo.git", + "DD_PIPELINE_EXECUTION_ID": "bb1f15ed-fde2-494d-8e13-88785bca9cc0" + }, + { + "_dd.ci.env_vars": "{\"CODEBUILD_BUILD_ARN\":\"arn:aws:codebuild:eu-north-1:12345678:build/codebuild-demo-project:b1e6661e-e4f2-4156-9ab9-82a19\",\"DD_PIPELINE_EXECUTION_ID\":\"bb1f15ed-fde2-494d-8e13-88785bca9cc0\",\"DD_ACTION_EXECUTION_ID\":\"35519dc3-7c45-493c-9ba6-cd78ea11f69d\"}", + "ci.pipeline.id": "bb1f15ed-fde2-494d-8e13-88785bca9cc0", + "ci.provider.name": "awscodepipeline", + "git.branch": "user-supplied-branch", + "git.commit.author.date": "usersupplied-authordate", + "git.commit.author.email": "usersupplied-authoremail", + "git.commit.author.name": "usersupplied-authorname", + "git.commit.committer.date": "usersupplied-comitterdate", + "git.commit.committer.email": "usersupplied-comitteremail", + "git.commit.committer.name": "usersupplied-comittername", + "git.commit.message": "usersupplied-message", + "git.commit.sha": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", + "git.repository_url": "git@github.com:DataDog/userrepo.git" + } + ], + [ + { + "CODEBUILD_INITIATOR": "lambdafunction/test-lambda", + "DD_GIT_BRANCH": "user-supplied-branch", + "DD_GIT_COMMIT_AUTHOR_DATE": "usersupplied-authordate", + "DD_GIT_COMMIT_AUTHOR_EMAIL": "usersupplied-authoremail", + "DD_GIT_COMMIT_AUTHOR_NAME": "usersupplied-authorname", + "DD_GIT_COMMIT_COMMITTER_DATE": "usersupplied-comitterdate", + "DD_GIT_COMMIT_COMMITTER_EMAIL": "usersupplied-comitteremail", + "DD_GIT_COMMIT_COMMITTER_NAME": "usersupplied-comittername", + "DD_GIT_COMMIT_MESSAGE": "usersupplied-message", + "DD_GIT_COMMIT_SHA": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", + "DD_GIT_REPOSITORY_URL": "git@github.com:DataDog/userrepo.git" + }, + { + "git.branch": "user-supplied-branch", + "git.commit.author.date": "usersupplied-authordate", + "git.commit.author.email": "usersupplied-authoremail", + "git.commit.author.name": "usersupplied-authorname", + "git.commit.committer.date": "usersupplied-comitterdate", + "git.commit.committer.email": "usersupplied-comitteremail", + "git.commit.committer.name": "usersupplied-comittername", + "git.commit.message": "usersupplied-message", + "git.commit.sha": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", + "git.repository_url": "git@github.com:DataDog/userrepo.git" + } + ] +] From 1346bc399a0e0455af18e5f8b3e313e799743ef8 Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko <121111529+nikita-tkachenko-datadog@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:33:15 +0200 Subject: [PATCH 16/29] Fix getting upstream branch name for unshallowing (#6026) --- .../trace/civisibility/git/tree/GitClient.java | 12 +++--------- .../civisibility/git/tree/GitDataUploaderImpl.java | 2 +- .../trace/civisibility/git/tree/GitClientTest.groovy | 6 +++--- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitClient.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitClient.java index 7af8bad63a00..c42ee432e898 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitClient.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitClient.java @@ -67,7 +67,7 @@ public boolean isShallow() throws IOException, TimeoutException, InterruptedExce } /** - * Returns the full symbolic name of the upstream (remote tracking) branch for the currently + * Returns the SHA of the head commit of the upstream (remote tracking) branch for the currently * checked-out local branch. If the local branch is not tracking any remote branches, a {@link * datadog.trace.civisibility.utils.ShellCommandExecutor.ShellCommandFailedException} exception * will be thrown. @@ -79,15 +79,9 @@ public boolean isShallow() throws IOException, TimeoutException, InterruptedExce * @throws InterruptedException If current thread was interrupted while waiting for Git command to * finish */ - public String getUpstreamBranch() throws IOException, TimeoutException, InterruptedException { + public String getUpstreamBranchSha() throws IOException, TimeoutException, InterruptedException { return commandExecutor - .executeCommand( - IOUtils::readFully, - "git", - "rev-parse", - "--abbrev-ref", - "--symbolic-full-name", - "@{upstream}") + .executeCommand(IOUtils::readFully, "git", "rev-parse", "@{upstream}") .trim(); } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitDataUploaderImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitDataUploaderImpl.java index 2e579edffea0..ca1423281024 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitDataUploaderImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitDataUploaderImpl.java @@ -150,7 +150,7 @@ private void unshallowRepository() throws IOException, TimeoutException, Interru } try { - String upstreamBranch = gitClient.getUpstreamBranch(); + String upstreamBranch = gitClient.getUpstreamBranchSha(); gitClient.unshallow(upstreamBranch); } catch (ShellCommandExecutor.ShellCommandFailedException e) { LOGGER.debug( diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitClientTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitClientTest.groovy index deaa57c2c418..68f29205469a 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitClientTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitClientTest.groovy @@ -43,16 +43,16 @@ class GitClientTest extends Specification { shallow } - def "test get upstream branch"() { + def "test get upstream branch SHA"() { given: givenGitRepo("ci/git/shallow/git") when: def gitClient = givenGitClient() - def upstreamBranch = gitClient.getUpstreamBranch() + def upstreamBranch = gitClient.getUpstreamBranchSha() then: - upstreamBranch == "origin/master" + upstreamBranch == "98b944cc44f18bfb78e3021de2999cdcda8efdf6" } def "test unshallow: #remoteSha"() { From 5142c9f1db0f93f2185dc4bb6d1ae8163f35a9e0 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch <stuart.mcculloch@datadoghq.com> Date: Fri, 13 Oct 2023 14:53:31 +0100 Subject: [PATCH 17/29] Add 'weblogic.net.http.HttpURLConnection' to list of traced connection classes (#6047) It overrides connect, getInputStream, and getOutputStream without delegating --- .../http_url_connection/HttpUrlConnectionInstrumentation.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/instrumentation/http-url-connection/src/main/java/datadog/trace/instrumentation/http_url_connection/HttpUrlConnectionInstrumentation.java b/dd-java-agent/instrumentation/http-url-connection/src/main/java/datadog/trace/instrumentation/http_url_connection/HttpUrlConnectionInstrumentation.java index 261c7a59cf74..62ba54122705 100644 --- a/dd-java-agent/instrumentation/http-url-connection/src/main/java/datadog/trace/instrumentation/http_url_connection/HttpUrlConnectionInstrumentation.java +++ b/dd-java-agent/instrumentation/http-url-connection/src/main/java/datadog/trace/instrumentation/http_url_connection/HttpUrlConnectionInstrumentation.java @@ -33,7 +33,9 @@ public HttpUrlConnectionInstrumentation() { public String[] knownMatchingTypes() { // we deliberately exclude various subclasses that are simple delegators return new String[] { - "sun.net.www.protocol.http.HttpURLConnection", "java.net.HttpURLConnection" + "sun.net.www.protocol.http.HttpURLConnection", + "java.net.HttpURLConnection", + "weblogic.net.http.HttpURLConnection" }; } From cdb2b63f868bbb0bf64c732afd40a2a9f27d8f19 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch <stuart.mcculloch@datadoghq.com> Date: Fri, 13 Oct 2023 14:53:46 +0100 Subject: [PATCH 18/29] Support tracing of custom (non-JDK) HttpURLConnection implementations (#6046) --- .../HttpUrlConnectionInstrumentation.java | 11 ++++++++++- .../trace/api/config/TraceInstrumentationConfig.java | 3 +++ .../java/datadog/trace/api/InstrumenterConfig.java | 12 ++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/instrumentation/http-url-connection/src/main/java/datadog/trace/instrumentation/http_url_connection/HttpUrlConnectionInstrumentation.java b/dd-java-agent/instrumentation/http-url-connection/src/main/java/datadog/trace/instrumentation/http_url_connection/HttpUrlConnectionInstrumentation.java index 62ba54122705..dbe30a2df06f 100644 --- a/dd-java-agent/instrumentation/http-url-connection/src/main/java/datadog/trace/instrumentation/http_url_connection/HttpUrlConnectionInstrumentation.java +++ b/dd-java-agent/instrumentation/http-url-connection/src/main/java/datadog/trace/instrumentation/http_url_connection/HttpUrlConnectionInstrumentation.java @@ -11,6 +11,7 @@ import com.google.auto.service.AutoService; import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.api.InstrumenterConfig; import datadog.trace.bootstrap.CallDepthThreadLocalMap; import datadog.trace.bootstrap.ContextStore; import datadog.trace.bootstrap.InstrumentationContext; @@ -23,7 +24,9 @@ @AutoService(Instrumenter.class) public class HttpUrlConnectionInstrumentation extends Instrumenter.Tracing - implements Instrumenter.ForBootstrap, Instrumenter.ForKnownTypes { + implements Instrumenter.ForBootstrap, + Instrumenter.ForKnownTypes, + Instrumenter.ForConfiguredType { public HttpUrlConnectionInstrumentation() { super("httpurlconnection"); @@ -39,6 +42,12 @@ public String[] knownMatchingTypes() { }; } + @Override + public String configuredMatchingType() { + // this won't match any class unless the property is set + return InstrumenterConfig.get().getHttpURLConnectionClassName(); + } + @Override public Map<String, String> contextStore() { return singletonMap("java.net.HttpURLConnection", HttpUrlState.class.getName()); diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java index 76909cefc0b9..aaec2c253ec5 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java @@ -55,6 +55,9 @@ public final class TraceInstrumentationConfig { public static final String JDBC_CONNECTION_CLASS_NAME = "trace.jdbc.connection.class.name"; + public static final String HTTP_URL_CONNECTION_CLASS_NAME = + "trace.http.url.connection.class.name"; + public static final String RUNTIME_CONTEXT_FIELD_INJECTION = "trace.runtime.context.field.injection"; public static final String SERIALVERSIONUID_FIELD_INJECTION = diff --git a/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java b/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java index 6fccabd5e620..56838ad3067f 100644 --- a/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java +++ b/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java @@ -26,6 +26,7 @@ import static datadog.trace.api.config.ProfilingConfig.PROFILING_DIRECT_ALLOCATION_ENABLED_DEFAULT; import static datadog.trace.api.config.ProfilingConfig.PROFILING_ENABLED; import static datadog.trace.api.config.ProfilingConfig.PROFILING_ENABLED_DEFAULT; +import static datadog.trace.api.config.TraceInstrumentationConfig.HTTP_URL_CONNECTION_CLASS_NAME; import static datadog.trace.api.config.TraceInstrumentationConfig.INTEGRATIONS_ENABLED; import static datadog.trace.api.config.TraceInstrumentationConfig.JDBC_CONNECTION_CLASS_NAME; import static datadog.trace.api.config.TraceInstrumentationConfig.JDBC_PREPARED_STATEMENT_CLASS_NAME; @@ -101,6 +102,8 @@ public class InstrumenterConfig { private final String jdbcPreparedStatementClassName; private final String jdbcConnectionClassName; + private final String httpURLConnectionClassName; + private final boolean directAllocationProfilingEnabled; private final List<String> excludedClasses; @@ -174,6 +177,8 @@ private InstrumenterConfig() { configProvider.getString(JDBC_PREPARED_STATEMENT_CLASS_NAME, ""); jdbcConnectionClassName = configProvider.getString(JDBC_CONNECTION_CLASS_NAME, ""); + httpURLConnectionClassName = configProvider.getString(HTTP_URL_CONNECTION_CLASS_NAME, ""); + directAllocationProfilingEnabled = configProvider.getBoolean( PROFILING_DIRECT_ALLOCATION_ENABLED, PROFILING_DIRECT_ALLOCATION_ENABLED_DEFAULT); @@ -287,6 +292,10 @@ public String getJdbcConnectionClassName() { return jdbcConnectionClassName; } + public String getHttpURLConnectionClassName() { + return httpURLConnectionClassName; + } + public boolean isDirectAllocationProfilingEnabled() { return directAllocationProfilingEnabled; } @@ -444,6 +453,9 @@ public String toString() { + ", jdbcConnectionClassName='" + jdbcConnectionClassName + '\'' + + ", httpURLConnectionClassName='" + + httpURLConnectionClassName + + '\'' + ", excludedClasses=" + excludedClasses + ", excludedClassesFile=" From 81a6d13b93e2877d078a2808345fadc46e04acff Mon Sep 17 00:00:00 2001 From: jean-philippe bempel <jean-philippe.bempel@datadoghq.com> Date: Tue, 10 Oct 2023 09:59:18 +0200 Subject: [PATCH 19/29] Introduce PII redaction based on keywords Redacts values based on a list of predefined words name are normalized in lower case and by removing special characters: `@`, `$`, `-` and `_` Redaction happening at: - expression language for reference, index and getmember operations - serialization for snapshot or template values including maps - capture of the values from instrumentation For conditions (log or span decoration probes) evaluation of a redacted values triggers an evaluation exception that will be reported as evaluation error into a snapshot. --- .../bootstrap/debugger/CapturedContext.java | 9 ++ .../debugger/el/ValueReferenceResolver.java | 2 + .../bootstrap/debugger/util/Redaction.java | 72 ++++++++++ .../debugger/util/RedactionTest.java | 21 +++ .../datadog/debugger/el/ProbeCondition.java | 46 +++++- .../el/expressions/GetMemberExpression.java | 8 +- .../el/expressions/IndexExpression.java | 9 +- .../el/expressions/ValueRefExpression.java | 8 +- .../debugger/el/ProbeConditionTest.java | 13 ++ .../expressions/ValueRefExpressionTest.java | 16 +++ .../test/resources/test_conditional_09.json | 6 + .../debugger/util/MoshiSnapshotHelper.java | 7 + .../debugger/util/SerializerWithLimits.java | 21 ++- .../debugger/util/StringTokenWriter.java | 3 + .../debugger/agent/CapturedSnapshotTest.java | 136 ++++++++++++++---- ...panDecorationProbeInstrumentationTest.java | 58 ++++++++ .../datadog/debugger/CapturedSnapshot27.java | 30 ++++ .../datadog/debugger/CapturedSnapshot28.java | 40 ++++++ 18 files changed, 474 insertions(+), 31 deletions(-) create mode 100644 dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java create mode 100644 dd-java-agent/agent-debugger/debugger-bootstrap/src/test/java/datadog/trace/bootstrap/debugger/util/RedactionTest.java create mode 100644 dd-java-agent/agent-debugger/debugger-el/src/test/resources/test_conditional_09.json create mode 100644 dd-java-agent/agent-debugger/src/test/resources/com/datadog/debugger/CapturedSnapshot27.java create mode 100644 dd-java-agent/agent-debugger/src/test/resources/com/datadog/debugger/CapturedSnapshot28.java diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/CapturedContext.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/CapturedContext.java index 87d7bbf2f935..707877540f90 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/CapturedContext.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/CapturedContext.java @@ -1,9 +1,12 @@ package datadog.trace.bootstrap.debugger; +import static datadog.trace.bootstrap.debugger.util.Redaction.REDACTED_VALUE; + import datadog.trace.bootstrap.debugger.el.ReflectiveFieldValueResolver; import datadog.trace.bootstrap.debugger.el.ValueReferenceResolver; import datadog.trace.bootstrap.debugger.el.ValueReferences; import datadog.trace.bootstrap.debugger.el.Values; +import datadog.trace.bootstrap.debugger.util.Redaction; import datadog.trace.bootstrap.debugger.util.TimeoutChecker; import java.util.ArrayList; import java.util.Collections; @@ -106,6 +109,9 @@ public Object getMember(Object target, String memberName) { if (target == Values.UNDEFINED_OBJECT) { return target; } + if (Redaction.isRedactedKeyword(memberName)) { + return REDACTED_VALUE; + } if (target instanceof CapturedValue) { Map<String, CapturedValue> fields = ((CapturedValue) target).fields; if (fields.containsKey(memberName)) { @@ -537,6 +543,9 @@ public static CapturedValue raw( private static CapturedValue build( String name, String declaredType, Object value, Limits limits, String notCapturedReason) { + if (Redaction.isRedactedKeyword(name)) { + value = REDACTED_VALUE; + } CapturedValue val = new CapturedValue( name, declaredType, value, limits, Collections.emptyMap(), notCapturedReason); diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ValueReferenceResolver.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ValueReferenceResolver.java index af3556000802..0650e3dfc8a6 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ValueReferenceResolver.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ValueReferenceResolver.java @@ -8,6 +8,8 @@ public interface ValueReferenceResolver { Object getMember(Object target, String name); + default void redacted(String expr) {} + default ValueReferenceResolver withExtensions(Map<String, Object> extensions) { return this; } diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java new file mode 100644 index 000000000000..0a1242c129bb --- /dev/null +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java @@ -0,0 +1,72 @@ +package datadog.trace.bootstrap.debugger.util; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class Redaction { + public static final String REDACTED_VALUE = "REDACTED"; + + /* + * based on sentry list: https://github.com/getsentry/sentry-python/blob/fefb454287b771ac31db4e30fa459d9be2f977b8/sentry_sdk/scrubber.py#L17-L58 + */ + private static final Set<String> KEYWORDS = + new HashSet<>( + Arrays.asList( + "password", + "passwd", + "secret", + "apikey", + "auth", + "credentials", + "mysqlpwd", + "privatekey", + "token", + "ipaddress", + "session", + // django + "csrftoken", + "sessionid", + // wsgi + "remoteaddr", + "xcsrftoken", + "xforwardedfor", + "setcookie", + "cookie", + "authorization", + "xapikey", + "xforwardedfor", + "xrealip")); + + public static boolean isRedactedKeyword(String name) { + if (name == null) { + return false; + } + name = normalize(name); + return KEYWORDS.contains(name); + } + + private static String normalize(String name) { + StringBuilder sb = null; + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + boolean isUpper = Character.isUpperCase(c); + boolean isRemovable = isRemovableChar(c); + if (isUpper || isRemovable || sb != null) { + if (sb == null) { + sb = new StringBuilder(); + } + if (isUpper) { + sb.append(Character.toLowerCase(c)); + } else if (!isRemovable) { + sb.append(c); + } + } + } + return sb != null ? sb.toString() : name; + } + + private static boolean isRemovableChar(char c) { + return c == '_' || c == '-' || c == '$' || c == '@'; + } +} diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/test/java/datadog/trace/bootstrap/debugger/util/RedactionTest.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/test/java/datadog/trace/bootstrap/debugger/util/RedactionTest.java new file mode 100644 index 000000000000..97f8bae88e00 --- /dev/null +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/test/java/datadog/trace/bootstrap/debugger/util/RedactionTest.java @@ -0,0 +1,21 @@ +package datadog.trace.bootstrap.debugger.util; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class RedactionTest { + + @Test + public void test() { + assertFalse(Redaction.isRedactedKeyword(null)); + assertFalse(Redaction.isRedactedKeyword("")); + assertFalse(Redaction.isRedactedKeyword("foobar")); + assertFalse(Redaction.isRedactedKeyword("@-_$")); + assertTrue(Redaction.isRedactedKeyword("password")); + assertTrue(Redaction.isRedactedKeyword("PassWord")); + assertTrue(Redaction.isRedactedKeyword("_Pass-Word_")); + assertTrue(Redaction.isRedactedKeyword("$pass_worD")); + assertTrue(Redaction.isRedactedKeyword("@passWord@")); + } +} diff --git a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/ProbeCondition.java b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/ProbeCondition.java index 8ded283ec935..2b56076d567b 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/ProbeCondition.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/ProbeCondition.java @@ -11,6 +11,7 @@ import datadog.trace.bootstrap.debugger.el.ValueReferenceResolver; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; +import java.util.Map; /** Implements expression language for probe condition */ public final class ProbeCondition implements DebuggerScript<Boolean> { @@ -103,14 +104,55 @@ public Boolean execute(ValueReferenceResolver valueRefResolver) { if (when == null) { return true; } - if (when.evaluate(valueRefResolver)) { - then.evaluate(valueRefResolver); + ProbeConditionResolver conditionResolver = new ProbeConditionResolver(valueRefResolver); + if (when.evaluate(conditionResolver)) { + then.evaluate(conditionResolver); + conditionResolver.checkRedacted(); return true; } + conditionResolver.checkRedacted(); return false; } public void accept(Visitor visitor) { when.accept(visitor); } + + private static class ProbeConditionResolver implements ValueReferenceResolver { + private final ValueReferenceResolver delegate; + private String expr; + private boolean redacted; + + public ProbeConditionResolver(ValueReferenceResolver delegate) { + this.delegate = delegate; + } + + @Override + public Object lookup(String name) { + return delegate.lookup(name); + } + + @Override + public Object getMember(Object target, String name) { + return delegate.getMember(target, name); + } + + @Override + public void redacted(String expr) { + this.redacted = true; + this.expr = expr; + } + + public void checkRedacted() { + if (redacted) { + throw new EvaluationException( + "Could not evaluate the expression because '" + expr + "' was redacted", expr); + } + } + + @Override + public ValueReferenceResolver withExtensions(Map<String, Object> extensions) { + return delegate.withExtensions(extensions); + } + } } diff --git a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/GetMemberExpression.java b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/GetMemberExpression.java index 0ca2e3e35f44..e0e923e9999f 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/GetMemberExpression.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/GetMemberExpression.java @@ -2,9 +2,11 @@ import com.datadog.debugger.el.EvaluationException; import com.datadog.debugger.el.Generated; +import com.datadog.debugger.el.PrettyPrintVisitor; import com.datadog.debugger.el.Value; import com.datadog.debugger.el.Visitor; import datadog.trace.bootstrap.debugger.el.ValueReferenceResolver; +import datadog.trace.bootstrap.debugger.util.Redaction; import java.util.Objects; public class GetMemberExpression implements ValueExpression<Value<?>> { @@ -23,7 +25,11 @@ public Value<?> evaluate(ValueReferenceResolver valueRefResolver) { return targetValue; } try { - return Value.of(valueRefResolver.getMember(targetValue.getValue(), memberName)); + Object member = valueRefResolver.getMember(targetValue.getValue(), memberName); + if (member == Redaction.REDACTED_VALUE) { + valueRefResolver.redacted(PrettyPrintVisitor.print(this)); + } + return Value.of(member); } catch (RuntimeException ex) { throw new EvaluationException(ex.getMessage(), memberName, ex); } diff --git a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/IndexExpression.java b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/IndexExpression.java index 7cd825bae506..bc767b7c41bb 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/IndexExpression.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/IndexExpression.java @@ -7,6 +7,7 @@ import com.datadog.debugger.el.values.ListValue; import com.datadog.debugger.el.values.MapValue; import datadog.trace.bootstrap.debugger.el.ValueReferenceResolver; +import datadog.trace.bootstrap.debugger.util.Redaction; public class IndexExpression implements ValueExpression<Value<?>> { @@ -31,7 +32,13 @@ public Value<?> evaluate(ValueReferenceResolver valueRefResolver) { } try { if (targetValue instanceof MapValue) { - result = ((MapValue) targetValue).get(keyValue.getValue()); + Object objKey = keyValue.getValue(); + if (objKey instanceof String && Redaction.isRedactedKeyword((String) objKey)) { + valueRefResolver.redacted(PrettyPrintVisitor.print(this)); + result = Value.of(Redaction.REDACTED_VALUE); + } else { + result = ((MapValue) targetValue).get(objKey); + } } if (targetValue instanceof ListValue) { result = ((ListValue) targetValue).get(keyValue.getValue()); diff --git a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ValueRefExpression.java b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ValueRefExpression.java index 11dabc9116e8..095f935e86b3 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ValueRefExpression.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ValueRefExpression.java @@ -2,9 +2,11 @@ import com.datadog.debugger.el.EvaluationException; import com.datadog.debugger.el.Generated; +import com.datadog.debugger.el.PrettyPrintVisitor; import com.datadog.debugger.el.Value; import com.datadog.debugger.el.Visitor; import datadog.trace.bootstrap.debugger.el.ValueReferenceResolver; +import datadog.trace.bootstrap.debugger.util.Redaction; import java.util.Objects; /** An expression taking a reference path and resolving to {@linkplain Value} */ @@ -18,7 +20,11 @@ public ValueRefExpression(String symbolName) { @Override public Value<?> evaluate(ValueReferenceResolver valueRefResolver) { try { - return Value.of(valueRefResolver.lookup(symbolName)); + Object symbol = valueRefResolver.lookup(symbolName); + if (symbol == Redaction.REDACTED_VALUE) { + valueRefResolver.redacted(PrettyPrintVisitor.print(this)); + } + return Value.of(symbol); } catch (RuntimeException ex) { throw new EvaluationException(ex.getMessage(), symbolName); } diff --git a/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/ProbeConditionTest.java b/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/ProbeConditionTest.java index 5a54a75877a9..c9dd388426ee 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/ProbeConditionTest.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/ProbeConditionTest.java @@ -180,6 +180,19 @@ void testIncorrectSyntax() { assertEquals("Unsupported operation 'gte'", ex.getMessage()); } + @Test + void redaction() throws IOException { + ProbeCondition probeCondition = load("/test_conditional_09.json"); + Map<String, Object> args = new HashMap<>(); + args.put("password", "secret123"); + ValueReferenceResolver ctx = RefResolverHelper.createResolver(args, null, null); + EvaluationException evaluationException = + assertThrows(EvaluationException.class, () -> probeCondition.execute(ctx)); + assertEquals( + "Could not evaluate the expression because 'password' was redacted", + evaluationException.getMessage()); + } + private static ProbeCondition load(String resourcePath) throws IOException { InputStream input = ProbeConditionTest.class.getResourceAsStream(resourcePath); Moshi moshi = diff --git a/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/expressions/ValueRefExpressionTest.java b/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/expressions/ValueRefExpressionTest.java index 4048f3293b7b..427f35b36ba2 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/expressions/ValueRefExpressionTest.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/expressions/ValueRefExpressionTest.java @@ -9,6 +9,7 @@ import com.datadog.debugger.el.Value; import datadog.trace.bootstrap.debugger.el.ValueReferenceResolver; import datadog.trace.bootstrap.debugger.el.ValueReferences; +import datadog.trace.bootstrap.debugger.util.Redaction; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -99,4 +100,19 @@ void contextRef() { assertEquals("Cannot find synthetic var: invalid", runtimeException.getMessage()); assertEquals("@invalid", print(invalidExpression)); } + + @Test + public void redacted() { + ValueRefExpression valueRef = new ValueRefExpression("password"); + class StoreSecret { + String password; + + public StoreSecret(String password) { + this.password = password; + } + } + StoreSecret instance = new StoreSecret("secret123"); + Value<?> val = valueRef.evaluate(RefResolverHelper.createResolver(instance)); + assertEquals(Redaction.REDACTED_VALUE, val.getValue()); + } } diff --git a/dd-java-agent/agent-debugger/debugger-el/src/test/resources/test_conditional_09.json b/dd-java-agent/agent-debugger/debugger-el/src/test/resources/test_conditional_09.json new file mode 100644 index 000000000000..11946a4ee690 --- /dev/null +++ b/dd-java-agent/agent-debugger/debugger-el/src/test/resources/test_conditional_09.json @@ -0,0 +1,6 @@ +{ + "dsl": "password == 'secret123'", + "json": { + "eq": [{"ref": "password"}, "secret123"] + } +} diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/MoshiSnapshotHelper.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/MoshiSnapshotHelper.java index eff3c679f267..7cec03f035c6 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/MoshiSnapshotHelper.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/MoshiSnapshotHelper.java @@ -44,6 +44,7 @@ public class MoshiSnapshotHelper { public static final String COLLECTION_SIZE_REASON = "collectionSize"; public static final String TIMEOUT_REASON = "timeout"; public static final String DEPTH_REASON = "depth"; + public static final String REDACTED_REASON = "redacted"; public static final String TYPE = "type"; public static final String VALUE = "value"; public static final String FIELDS = "fields"; @@ -461,6 +462,12 @@ public void notCaptured(SerializerWithLimits.NotCapturedReason reason) throws Ex jsonWriter.value(TIMEOUT_REASON); break; } + case REDACTED: + { + jsonWriter.name(NOT_CAPTURED_REASON); + jsonWriter.value(REDACTED_REASON); + break; + } default: throw new RuntimeException("Unsupported NotCapturedReason: " + reason); } diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/SerializerWithLimits.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/SerializerWithLimits.java index b98696f65eaf..f61f0e8af53c 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/SerializerWithLimits.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/SerializerWithLimits.java @@ -1,7 +1,10 @@ package com.datadog.debugger.util; +import static datadog.trace.bootstrap.debugger.util.Redaction.REDACTED_VALUE; + import datadog.trace.bootstrap.debugger.CapturedContext; import datadog.trace.bootstrap.debugger.Limits; +import datadog.trace.bootstrap.debugger.util.Redaction; import datadog.trace.bootstrap.debugger.util.TimeoutChecker; import datadog.trace.bootstrap.debugger.util.WellKnownClasses; import java.lang.reflect.Array; @@ -46,7 +49,8 @@ public static boolean isPrimitive(String type) { enum NotCapturedReason { MAX_DEPTH, FIELD_COUNT, - TIMEOUT + TIMEOUT, + REDACTED } public interface TokenWriter { @@ -116,6 +120,11 @@ public void serialize(Object value, String type, Limits limits) throws Exception throw new IllegalArgumentException("Type is required for serialization"); } tokenWriter.prologue(value, type); + if (value == REDACTED_VALUE) { + tokenWriter.notCaptured(NotCapturedReason.REDACTED); + tokenWriter.epilogue(value); + return; + } if (timeoutChecker.isTimedOut(System.currentTimeMillis())) { tokenWriter.notCaptured(NotCapturedReason.TIMEOUT); tokenWriter.epilogue(value); @@ -279,6 +288,9 @@ private void onField(Field field, Object value, Limits limits) throws Exception } else { typeName = value != null ? value.getClass().getTypeName() : field.getType().getTypeName(); } + if (Redaction.isRedactedKeyword(field.getName())) { + value = REDACTED_VALUE; + } serialize( value instanceof CapturedContext.CapturedValue ? ((CapturedContext.CapturedValue) value).getValue() @@ -423,7 +435,12 @@ private boolean serializeMapEntries(Set<? extends Map.Entry<?, ?>> entries, Limi Map.Entry<?, ?> entry = (Map.Entry<?, ?>) it.next(); // /!\ alien call /!\ tokenWriter.mapEntryPrologue(entry); Object keyObj = entry.getKey(); // /!\ alien call /!\ - Object valObj = entry.getValue(); // /!\ alien call /!\ + Object valObj; + if (keyObj instanceof String && Redaction.isRedactedKeyword((String) keyObj)) { + valObj = REDACTED_VALUE; + } else { + valObj = entry.getValue(); // /!\ alien call /!\ + } serialize( keyObj, keyObj != null ? keyObj.getClass().getTypeName() : Object.class.getTypeName(), diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/StringTokenWriter.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/StringTokenWriter.java index 54528ec9dddd..838f1c454916 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/StringTokenWriter.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/StringTokenWriter.java @@ -155,6 +155,9 @@ public void notCaptured(SerializerWithLimits.NotCapturedReason reason) { case FIELD_COUNT: sb.append(", ..."); break; + case REDACTED: + sb.append("{redacted}"); + break; default: throw new RuntimeException("Unsupported NotCapturedReason: " + reason); } diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java index a560bf69b9ac..9997c0023b48 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java @@ -7,6 +7,7 @@ import static com.datadog.debugger.util.TestHelper.setFieldInConfig; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; @@ -22,6 +23,7 @@ import com.datadog.debugger.el.DSL; import com.datadog.debugger.el.ProbeCondition; +import com.datadog.debugger.el.values.StringValue; import com.datadog.debugger.probe.LogProbe; import com.datadog.debugger.sink.DebuggerSink; import com.datadog.debugger.sink.ProbeStatusSink; @@ -78,6 +80,7 @@ public class CapturedSnapshotTest { private static final ProbeId PROBE_ID = new ProbeId("beae1807-f3b0-4ea8-a74f-826790c5e6f8", 0); private static final ProbeId PROBE_ID1 = new ProbeId("beae1807-f3b0-4ea8-a74f-826790c5e6f6", 0); private static final ProbeId PROBE_ID2 = new ProbeId("beae1807-f3b0-4ea8-a74f-826790c5e6f7", 0); + private static final ProbeId PROBE_ID3 = new ProbeId("beae1807-f3b0-4ea8-a74f-826790c5e6f8", 0); private static final String SERVICE_NAME = "service-name"; private static final JsonAdapter<CapturedContext.CapturedValue> VALUE_ADAPTER = new MoshiSnapshotTestHelper.CapturedValueAdapter(); @@ -141,8 +144,8 @@ public void singleLineProbe() throws IOException, URISyntaxException { int result = Reflect.on(testClass).call("main", "1").get(); Assertions.assertEquals(3, result); Snapshot snapshot = assertOneSnapshot(listener); - Assertions.assertNull(snapshot.getCaptures().getEntry()); - Assertions.assertNull(snapshot.getCaptures().getReturn()); + assertNull(snapshot.getCaptures().getEntry()); + assertNull(snapshot.getCaptures().getReturn()); Assertions.assertEquals(1, snapshot.getCaptures().getLines().size()); assertCaptureArgs(snapshot.getCaptures().getLines().get(8), "arg", "java.lang.String", "1"); assertCaptureLocals(snapshot.getCaptures().getLines().get(8), "var1", "int", "1"); @@ -395,8 +398,8 @@ public void outsideSynchronizedBlock() throws IOException, URISyntaxException { int result = Reflect.on(testClass).call("main", "synchronizedBlock").get(); Assertions.assertEquals(76, result); Snapshot snapshot = assertOneSnapshot(listener); - Assertions.assertNull(snapshot.getCaptures().getEntry()); - Assertions.assertNull(snapshot.getCaptures().getReturn()); + assertNull(snapshot.getCaptures().getEntry()); + assertNull(snapshot.getCaptures().getReturn()); Assertions.assertEquals(2, snapshot.getCaptures().getLines().size()); assertCaptureLocals(snapshot.getCaptures().getLines().get(LINE_START), "count", "int", "31"); assertCaptureLocals(snapshot.getCaptures().getLines().get(LINE_END), "count", "int", "76"); @@ -411,8 +414,8 @@ public void sourceFileProbe() throws IOException, URISyntaxException { int result = Reflect.on(testClass).call("main", "").get(); Assertions.assertEquals(48, result); Snapshot snapshot = assertOneSnapshot(listener); - Assertions.assertNull(snapshot.getCaptures().getEntry()); - Assertions.assertNull(snapshot.getCaptures().getReturn()); + assertNull(snapshot.getCaptures().getEntry()); + assertNull(snapshot.getCaptures().getReturn()); Assertions.assertEquals(1, snapshot.getCaptures().getLines().size()); Assertions.assertEquals(CLASS_NAME, snapshot.getProbe().getLocation().getType()); Assertions.assertEquals("f1", snapshot.getProbe().getLocation().getMethod()); @@ -428,8 +431,8 @@ public void simpleSourceFileProbe() throws IOException, URISyntaxException { int result = Reflect.on(testClass).call("main", "2").get(); Assertions.assertEquals(2, result); Snapshot snapshot = assertOneSnapshot(listener); - Assertions.assertNull(snapshot.getCaptures().getEntry()); - Assertions.assertNull(snapshot.getCaptures().getReturn()); + assertNull(snapshot.getCaptures().getEntry()); + assertNull(snapshot.getCaptures().getReturn()); Assertions.assertEquals(1, snapshot.getCaptures().getLines().size()); Assertions.assertEquals(CLASS_NAME, snapshot.getProbe().getLocation().getType()); Assertions.assertEquals("main", snapshot.getProbe().getLocation().getMethod()); @@ -448,8 +451,8 @@ public void sourceFileProbeFullPath() throws IOException, URISyntaxException { int result = Reflect.on(testClass).call("main", "2").get(); Assertions.assertEquals(2, result); Snapshot snapshot = assertOneSnapshot(listener); - Assertions.assertNull(snapshot.getCaptures().getEntry()); - Assertions.assertNull(snapshot.getCaptures().getReturn()); + assertNull(snapshot.getCaptures().getEntry()); + assertNull(snapshot.getCaptures().getReturn()); Assertions.assertEquals(1, snapshot.getCaptures().getLines().size()); Assertions.assertEquals(CLASS_NAME, snapshot.getProbe().getLocation().getType()); Assertions.assertEquals("main", snapshot.getProbe().getLocation().getMethod()); @@ -468,8 +471,8 @@ public void sourceFileProbeFullPathTopLevelClass() throws IOException, URISyntax int result = Reflect.on(testClass).call("main", "1").get(); Assertions.assertEquals(42 * 42, result); Snapshot snapshot = assertOneSnapshot(listener); - Assertions.assertNull(snapshot.getCaptures().getEntry()); - Assertions.assertNull(snapshot.getCaptures().getReturn()); + assertNull(snapshot.getCaptures().getEntry()); + assertNull(snapshot.getCaptures().getReturn()); Assertions.assertEquals(1, snapshot.getCaptures().getLines().size()); Assertions.assertEquals( "com.datadog.debugger.TopLevel01", snapshot.getProbe().getLocation().getType()); @@ -491,8 +494,8 @@ public void methodProbeLineProbeMix() throws IOException, URISyntaxException { Assertions.assertEquals(2, result); List<Snapshot> snapshots = assertSnapshots(listener, 2, PROBE_ID1, PROBE_ID2); Snapshot snapshot0 = snapshots.get(0); - Assertions.assertNull(snapshot0.getCaptures().getEntry()); - Assertions.assertNull(snapshot0.getCaptures().getReturn()); + assertNull(snapshot0.getCaptures().getEntry()); + assertNull(snapshot0.getCaptures().getReturn()); Assertions.assertEquals(1, snapshot0.getCaptures().getLines().size()); Assertions.assertEquals( "com.datadog.debugger.CapturedSnapshot11", snapshot0.getProbe().getLocation().getType()); @@ -518,8 +521,8 @@ public void sourceFileProbeScala() throws IOException, URISyntaxException { int result = Reflect.on(testClass).call("main", "").get(); Assertions.assertEquals(48, result); Snapshot snapshot = assertOneSnapshot(listener); - Assertions.assertNull(snapshot.getCaptures().getEntry()); - Assertions.assertNull(snapshot.getCaptures().getReturn()); + assertNull(snapshot.getCaptures().getEntry()); + assertNull(snapshot.getCaptures().getReturn()); Assertions.assertEquals(1, snapshot.getCaptures().getLines().size()); Assertions.assertEquals(CLASS_NAME, snapshot.getProbe().getLocation().getType()); Assertions.assertEquals("f1", snapshot.getProbe().getLocation().getMethod()); @@ -537,8 +540,8 @@ public void sourceFileProbeGroovy() throws IOException, URISyntaxException { int result = Reflect.on(testClass).call("main", "").get(); Assertions.assertEquals(48, result); Snapshot snapshot = assertOneSnapshot(listener); - Assertions.assertNull(snapshot.getCaptures().getEntry()); - Assertions.assertNull(snapshot.getCaptures().getReturn()); + assertNull(snapshot.getCaptures().getEntry()); + assertNull(snapshot.getCaptures().getReturn()); Assertions.assertEquals(1, snapshot.getCaptures().getLines().size()); Assertions.assertEquals(CLASS_NAME, snapshot.getProbe().getLocation().getType()); Assertions.assertEquals("f1", snapshot.getProbe().getLocation().getMethod()); @@ -559,8 +562,8 @@ public void sourceFileProbeKotlin() { int result = Reflect.on(companion).call("main", "").get(); Assertions.assertEquals(48, result); Snapshot snapshot = assertOneSnapshot(listener); - Assertions.assertNull(snapshot.getCaptures().getEntry()); - Assertions.assertNull(snapshot.getCaptures().getReturn()); + assertNull(snapshot.getCaptures().getEntry()); + assertNull(snapshot.getCaptures().getReturn()); Assertions.assertEquals(1, snapshot.getCaptures().getLines().size()); Assertions.assertEquals(CLASS_NAME, snapshot.getProbe().getLocation().getType()); Assertions.assertEquals("f1", snapshot.getProbe().getLocation().getMethod()); @@ -1025,7 +1028,7 @@ public void mergedProbesConditionMainErrorAdditionalTrue() Assertions.assertEquals("fld", evaluationErrors.get(0).getExpr()); Assertions.assertEquals( "Cannot dereference to field: fld", evaluationErrors.get(0).getMessage()); - Assertions.assertNull(snapshots.get(1).getEvaluationErrors()); + assertNull(snapshots.get(1).getEvaluationErrors()); } @Test @@ -1061,7 +1064,7 @@ public void mergedProbesConditionMainTrueAdditionalError() DSL.value("hello"))), "nullTyped.fld.fld.msg == 'hello'"); List<Snapshot> snapshots = doMergedProbeConditions(condition1, condition2, 2); - Assertions.assertNull(snapshots.get(0).getEvaluationErrors()); + assertNull(snapshots.get(0).getEvaluationErrors()); List<EvaluationError> evaluationErrors = snapshots.get(1).getEvaluationErrors(); Assertions.assertEquals(1, evaluationErrors.size()); Assertions.assertEquals("fld", evaluationErrors.get(0).getExpr()); @@ -1090,8 +1093,8 @@ public void mergedProbesConditionMixedLocation() throws IOException, URISyntaxEx int result = Reflect.on(testClass).call("main", "1").get(); Assertions.assertEquals(3, result); Assertions.assertEquals(2, listener.snapshots.size()); - Assertions.assertNull(listener.snapshots.get(0).getEvaluationErrors()); - Assertions.assertNull(listener.snapshots.get(1).getEvaluationErrors()); + assertNull(listener.snapshots.get(0).getEvaluationErrors()); + assertNull(listener.snapshots.get(1).getEvaluationErrors()); } @Test @@ -1607,6 +1610,91 @@ public void dupLineProbeSameTemplate() throws IOException, URISyntaxException { } } + @Test + public void keywordRedaction() throws IOException, URISyntaxException { + final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot27"; + final String LOG_TEMPLATE = + "arg={arg} secret={secret} password={this.password} fromMap={strMap['password']}"; + LogProbe probe1 = + createProbeBuilder(PROBE_ID, CLASS_NAME, "doit", null) + .template(LOG_TEMPLATE, parseTemplate(LOG_TEMPLATE)) + .captureSnapshot(true) + .evaluateAt(MethodLocation.EXIT) + .build(); + DebuggerTransformerTest.TestSnapshotListener listener = installProbes(CLASS_NAME, probe1); + Class<?> testClass = compileAndLoadClass(CLASS_NAME); + int result = Reflect.on(testClass).call("main", "secret123").get(); + Assertions.assertEquals(42, result); + Snapshot snapshot = assertOneSnapshot(listener); + assertEquals( + "arg=secret123 secret={redacted} password={redacted} fromMap={redacted}", + snapshot.getMessage()); + CapturedContext.CapturedValue secretLocalVar = + snapshot.getCaptures().getReturn().getLocals().get("secret"); + CapturedContext.CapturedValue secretValued = + VALUE_ADAPTER.fromJson(secretLocalVar.getStrValue()); + assertEquals("redacted", secretValued.getNotCapturedReason()); + Map<String, CapturedContext.CapturedValue> thisFields = + getFields(snapshot.getCaptures().getReturn().getArguments().get("this")); + CapturedContext.CapturedValue passwordField = thisFields.get("password"); + assertEquals("redacted", passwordField.getNotCapturedReason()); + Map<String, String> strMap = (Map<String, String>) thisFields.get("strMap").getValue(); + assertNull(strMap.get("password")); + } + + @Test + public void keywordRedactionConditions() throws IOException, URISyntaxException { + final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot27"; + LogProbe probe1 = + createProbeBuilder(PROBE_ID1, CLASS_NAME, "doit", null) + .when( + new ProbeCondition( + DSL.when( + DSL.contains( + DSL.getMember(DSL.ref("this"), "password"), new StringValue("123"))), + "contains(this.password, '123')")) + .captureSnapshot(true) + .evaluateAt(MethodLocation.EXIT) + .build(); + LogProbe probe2 = + createProbeBuilder(PROBE_ID2, CLASS_NAME, "doit", null) + .when( + new ProbeCondition( + DSL.when(DSL.eq(DSL.ref("password"), DSL.value("123"))), "password == '123'")) + .captureSnapshot(true) + .evaluateAt(MethodLocation.EXIT) + .build(); + LogProbe probe3 = + createProbeBuilder(PROBE_ID3, CLASS_NAME, "doit", null) + .when( + new ProbeCondition( + DSL.when( + DSL.eq( + DSL.index(DSL.ref("strMap"), DSL.value("password")), DSL.value("123"))), + "strMap['password'] == '123'")) + .captureSnapshot(true) + .evaluateAt(MethodLocation.EXIT) + .build(); + DebuggerTransformerTest.TestSnapshotListener listener = + installProbes(CLASS_NAME, probe1, probe2, probe3); + Class<?> testClass = compileAndLoadClass(CLASS_NAME); + int result = Reflect.on(testClass).call("main", "secret123").get(); + Assertions.assertEquals(42, result); + List<Snapshot> snapshots = assertSnapshots(listener, 3, PROBE_ID1, PROBE_ID2, PROBE_ID3); + assertEquals(1, snapshots.get(0).getEvaluationErrors().size()); + assertEquals( + "Could not evaluate the expression because 'this.password' was redacted", + snapshots.get(0).getEvaluationErrors().get(0).getMessage()); + assertEquals(1, snapshots.get(1).getEvaluationErrors().size()); + assertEquals( + "Could not evaluate the expression because 'password' was redacted", + snapshots.get(1).getEvaluationErrors().get(0).getMessage()); + assertEquals(1, snapshots.get(2).getEvaluationErrors().size()); + assertEquals( + "Could not evaluate the expression because 'strMap[\"password\"]' was redacted", + snapshots.get(2).getEvaluationErrors().get(0).getMessage()); + } + private DebuggerTransformerTest.TestSnapshotListener setupInstrumentTheWorldTransformer( String excludeFileName) { Config config = mock(Config.class); diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/SpanDecorationProbeInstrumentationTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/SpanDecorationProbeInstrumentationTest.java index 05a8bc15430d..827136847eb0 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/SpanDecorationProbeInstrumentationTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/SpanDecorationProbeInstrumentationTest.java @@ -18,6 +18,7 @@ import com.datadog.debugger.el.DSL; import com.datadog.debugger.el.ProbeCondition; import com.datadog.debugger.el.expressions.BooleanExpression; +import com.datadog.debugger.el.values.StringValue; import com.datadog.debugger.probe.LogProbe; import com.datadog.debugger.probe.SpanDecorationProbe; import com.datadog.debugger.sink.DebuggerSink; @@ -286,6 +287,63 @@ PROBE_ID, ACTIVE, singletonList(decoration), CLASS_NAME, "process", null, null) assertNotNull(intLocal); } + @Test + public void keywordRedaction() throws IOException, URISyntaxException { + final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot28"; + SpanDecorationProbe.Decoration decoration = + createDecoration("tag1", "{password} {this.password} {strMap['password']}"); + installSingleSpanDecoration( + CLASS_NAME, ACTIVE, decoration, "process", "int (java.lang.String)"); + Class<?> testClass = compileAndLoadClass(CLASS_NAME); + int result = Reflect.on(testClass).call("main", "secret123").get(); + assertEquals(42, result); + MutableSpan span = traceInterceptor.getFirstSpan(); + assertEquals("{redacted} {redacted} {redacted}", span.getTags().get("tag1")); + assertEquals(PROBE_ID.getId(), span.getTags().get("_dd.di.tag1.probe_id")); + } + + @Test + public void keywordRedactionConditions() throws IOException, URISyntaxException { + final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot28"; + SpanDecorationProbe.Decoration decoration1 = + createDecoration( + DSL.contains(DSL.getMember(DSL.ref("this"), "password"), new StringValue("123")), + "contains(this.password, '123')", + "tag1", + "foo"); + SpanDecorationProbe.Decoration decoration2 = + createDecoration( + DSL.eq(DSL.ref("password"), DSL.value("123")), "password == '123'", "tag2", "foo"); + SpanDecorationProbe.Decoration decoration3 = + createDecoration( + DSL.eq(DSL.index(DSL.ref("strMap"), DSL.value("password")), DSL.value("123")), + "strMap['password'] == '123'", + "tag3", + "foo"); + List<SpanDecorationProbe.Decoration> decorations = + Arrays.asList(decoration1, decoration2, decoration3); + installSingleSpanDecoration( + CLASS_NAME, ACTIVE, decorations, "process", "int (java.lang.String)"); + Class<?> testClass = compileAndLoadClass(CLASS_NAME); + int result = Reflect.on(testClass).call("main", "secret123").get(); + assertEquals(42, result); + assertFalse(traceInterceptor.getFirstSpan().getTags().containsKey("tag1")); + assertFalse(traceInterceptor.getFirstSpan().getTags().containsKey("tag2")); + assertFalse(traceInterceptor.getFirstSpan().getTags().containsKey("tag3")); + assertEquals(1, mockSink.getSnapshots().size()); + Snapshot snapshot = mockSink.getSnapshots().get(0); + assertEquals(3, snapshot.getEvaluationErrors().size()); + assertEquals( + "Could not evaluate the expression because 'this.password' was redacted", + snapshot.getEvaluationErrors().get(0).getMessage()); + assertEquals( + "Could not evaluate the expression because 'password' was redacted", + snapshot.getEvaluationErrors().get(1).getMessage()); + assertEquals( + "Could not evaluate the expression because 'strMap[\"password\"]' was redacted", + snapshot.getEvaluationErrors().get(2).getMessage()); + } + private SpanDecorationProbe.Decoration createDecoration(String tagName, String valueDsl) { List<SpanDecorationProbe.Tag> tags = Arrays.asList( diff --git a/dd-java-agent/agent-debugger/src/test/resources/com/datadog/debugger/CapturedSnapshot27.java b/dd-java-agent/agent-debugger/src/test/resources/com/datadog/debugger/CapturedSnapshot27.java new file mode 100644 index 000000000000..91ed6bbad964 --- /dev/null +++ b/dd-java-agent/agent-debugger/src/test/resources/com/datadog/debugger/CapturedSnapshot27.java @@ -0,0 +1,30 @@ +package com.datadog.debugger; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; + +public class CapturedSnapshot27 { + private HashMap<String, String> strMap = new HashMap<>(); + { + strMap.put("foo1", "bar1"); + strMap.put("foo3", "bar3"); + } + + private String password; + + + private int doit(String arg) { + password = arg; + String secret = arg; + strMap.put("password", arg); + return 42; + } + + public static int main(String arg) { + CapturedSnapshot27 cs27 = new CapturedSnapshot27(); + return cs27.doit(arg); + + } +} diff --git a/dd-java-agent/agent-debugger/src/test/resources/com/datadog/debugger/CapturedSnapshot28.java b/dd-java-agent/agent-debugger/src/test/resources/com/datadog/debugger/CapturedSnapshot28.java new file mode 100644 index 000000000000..8c33eae42fdb --- /dev/null +++ b/dd-java-agent/agent-debugger/src/test/resources/com/datadog/debugger/CapturedSnapshot28.java @@ -0,0 +1,40 @@ +package com.datadog.debugger; + +import datadog.trace.agent.tooling.TracerInstaller; +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.ScopeSource; +import datadog.trace.core.CoreTracer; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CapturedSnapshot28 { + private String password; + private final Map<String, String> strMap = new HashMap<>(); + { + strMap.put("foo1", "bar1"); + strMap.put("foo2", "bar2"); + strMap.put("foo3", "bar3"); + } + + public static int main(String arg) { + AgentTracer.TracerAPI tracerAPI = AgentTracer.get(); + AgentSpan span = tracerAPI.buildSpan("process").start(); + try (AgentScope scope = tracerAPI.activateSpan(span, ScopeSource.MANUAL)) { + return new CapturedSnapshot28().process(arg); + } finally { + span.finish(); + } + } + + private int process(String arg) { + password = arg; + String secret = arg; + strMap.put("password", arg); + return 42; + } +} From b26e364ed0815130159e7dd6c6fad16018c6705d Mon Sep 17 00:00:00 2001 From: Stuart McCulloch <stuart.mcculloch@datadoghq.com> Date: Fri, 13 Oct 2023 23:56:43 +0100 Subject: [PATCH 20/29] Adds support for configuration of StatsD client queue size: (#6043) DD_STATSD_CLIENT_QUEUE_SIZE=<items> as well as buffer size and timeout when using UDS: DD_STATSD_CLIENT_SOCKET_BUFFER=<bytes> DD_STATSD_CLIENT_SOCKET_TIMEOUT=<ms> System property equivalents: -Ddd.statsd.client.queue.size=<items> -Ddd.statsd.client.socket.buffer=<bytes> -Ddd.statsd.client.socket.timeout=<ms> --- .../monitor/DDAgentStatsDConnection.java | 51 ++++++++++++++++--- .../trace/api/config/GeneralConfig.java | 4 ++ .../main/java/datadog/trace/api/Config.java | 23 +++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/communication/src/main/java/datadog/communication/monitor/DDAgentStatsDConnection.java b/communication/src/main/java/datadog/communication/monitor/DDAgentStatsDConnection.java index 4a9e5113d1d1..0455e2ec3ad7 100644 --- a/communication/src/main/java/datadog/communication/monitor/DDAgentStatsDConnection.java +++ b/communication/src/main/java/datadog/communication/monitor/DDAgentStatsDConnection.java @@ -101,9 +101,7 @@ private void doConnect() { if (log.isDebugEnabled()) { log.debug("Creating StatsD client - {}", statsDAddress()); } - // when using UDS, set "entity-id" to "none" to avoid having the DogStatsD - // server add origin tags (see https://github.com/DataDog/jmxfetch/pull/264) - String entityID = port == 0 ? "none" : null; + NonBlockingStatsDClientBuilder clientBuilder = new NonBlockingStatsDClientBuilder() .threadFactory(STATSD_CLIENT_THREAD_FACTORY) @@ -112,12 +110,53 @@ private void doConnect() { .hostname(host) .port(port) .namedPipe(namedPipe) - .errorHandler(this) - .entityID(entityID); + .errorHandler(this); + + // when using UDS, set "entity-id" to "none" to avoid having the DogStatsD + // server add origin tags (see https://github.com/DataDog/jmxfetch/pull/264) + if (this.port == 0) { + clientBuilder.constantTags("dd.internal.card:none"); + clientBuilder.entityID("none"); + } else { + clientBuilder.entityID(null); + } + + Integer queueSize = Config.get().getStatsDClientQueueSize(); + if (queueSize != null) { + clientBuilder.queueSize(queueSize); + } + // when using UDS set the datagram size to 8k (2k on Mac due to lower OS default) + // but also make sure packet size isn't larger than the configured socket buffer if (this.port == 0) { - clientBuilder.maxPacketSizeBytes(Platform.isMac() ? 2048 : 8192); + Integer timeout = Config.get().getStatsDClientSocketTimeout(); + if (timeout != null) { + clientBuilder.timeout(timeout); + } + Integer bufferSize = Config.get().getStatsDClientSocketBuffer(); + if (bufferSize != null) { + clientBuilder.socketBufferSize(bufferSize); + } + int packetSize = Platform.isMac() ? 2048 : 8192; + if (bufferSize != null && bufferSize < packetSize) { + packetSize = bufferSize; + } + clientBuilder.maxPacketSizeBytes(packetSize); + } + + if (log.isDebugEnabled()) { + if (this.port == 0) { + log.debug( + "Configured StatsD client - queueSize={}, maxPacketSize={}, socketBuffer={}, socketTimeout={}", + clientBuilder.queueSize, + clientBuilder.maxPacketSizeBytes, + clientBuilder.socketBufferSize, + clientBuilder.timeout); + } else { + log.debug("Configured StatsD client - queueSize={}", clientBuilder.queueSize); + } } + try { statsd = clientBuilder.build(); if (log.isDebugEnabled()) { diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java index abf200cba16d..2ac336a40a9e 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java @@ -39,6 +39,10 @@ public final class GeneralConfig { public static final String DOGSTATSD_ARGS = "dogstatsd.args"; public static final String DOGSTATSD_NAMED_PIPE = "dogstatsd.pipe.name"; + public static final String STATSD_CLIENT_QUEUE_SIZE = "statsd.client.queue.size"; + public static final String STATSD_CLIENT_SOCKET_BUFFER = "statsd.client.socket.buffer"; + public static final String STATSD_CLIENT_SOCKET_TIMEOUT = "statsd.client.socket.timeout"; + public static final String RUNTIME_METRICS_ENABLED = "runtime.metrics.enabled"; public static final String RUNTIME_ID_ENABLED = "runtime-id.enabled"; diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index d87279c2fcf7..7b2e13261e67 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -206,6 +206,9 @@ import static datadog.trace.api.config.GeneralConfig.SERVICE_NAME; import static datadog.trace.api.config.GeneralConfig.SITE; import static datadog.trace.api.config.GeneralConfig.STARTUP_LOGS_ENABLED; +import static datadog.trace.api.config.GeneralConfig.STATSD_CLIENT_QUEUE_SIZE; +import static datadog.trace.api.config.GeneralConfig.STATSD_CLIENT_SOCKET_BUFFER; +import static datadog.trace.api.config.GeneralConfig.STATSD_CLIENT_SOCKET_TIMEOUT; import static datadog.trace.api.config.GeneralConfig.TAGS; import static datadog.trace.api.config.GeneralConfig.TELEMETRY_DEPENDENCY_COLLECTION_ENABLED; import static datadog.trace.api.config.GeneralConfig.TELEMETRY_HEARTBEAT_INTERVAL; @@ -561,6 +564,10 @@ static class HostNameHolder { private final String dogStatsDNamedPipe; private final int dogStatsDStartDelay; + private final Integer statsDClientQueueSize; + private final Integer statsDClientSocketBuffer; + private final Integer statsDClientSocketTimeout; + private final boolean runtimeMetricsEnabled; private final boolean jmxFetchEnabled; private final String jmxFetchConfigDir; @@ -1225,6 +1232,10 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins configProvider.getInteger( DOGSTATSD_START_DELAY, DEFAULT_DOGSTATSD_START_DELAY, JMX_FETCH_START_DELAY); + statsDClientQueueSize = configProvider.getInteger(STATSD_CLIENT_QUEUE_SIZE); + statsDClientSocketBuffer = configProvider.getInteger(STATSD_CLIENT_SOCKET_BUFFER); + statsDClientSocketTimeout = configProvider.getInteger(STATSD_CLIENT_SOCKET_TIMEOUT); + runtimeMetricsEnabled = configProvider.getBoolean(RUNTIME_METRICS_ENABLED, true); jmxFetchEnabled = @@ -2088,6 +2099,18 @@ public int getDogStatsDStartDelay() { return dogStatsDStartDelay; } + public Integer getStatsDClientQueueSize() { + return statsDClientQueueSize; + } + + public Integer getStatsDClientSocketBuffer() { + return statsDClientSocketBuffer; + } + + public Integer getStatsDClientSocketTimeout() { + return statsDClientSocketTimeout; + } + public boolean isRuntimeMetricsEnabled() { return runtimeMetricsEnabled; } From cc8439d822f00aac2ca616b77db8c9212c30dd6f Mon Sep 17 00:00:00 2001 From: jean-philippe bempel <jean-philippe.bempel@datadoghq.com> Date: Mon, 16 Oct 2023 09:55:00 +0200 Subject: [PATCH 21/29] Throw exception on expression language in any case --- .../debugger/el/ValueReferenceResolver.java | 2 - .../datadog/debugger/el/ProbeCondition.java | 46 +------------------ .../el/expressions/GetMemberExpression.java | 4 +- .../el/expressions/IndexExpression.java | 5 +- .../el/expressions/ValueRefExpression.java | 4 +- .../expressions/ValueRefExpressionTest.java | 11 +++-- .../debugger/agent/CapturedSnapshotTest.java | 2 +- ...panDecorationProbeInstrumentationTest.java | 24 ++++++++-- 8 files changed, 40 insertions(+), 58 deletions(-) diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ValueReferenceResolver.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ValueReferenceResolver.java index 0650e3dfc8a6..af3556000802 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ValueReferenceResolver.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ValueReferenceResolver.java @@ -8,8 +8,6 @@ public interface ValueReferenceResolver { Object getMember(Object target, String name); - default void redacted(String expr) {} - default ValueReferenceResolver withExtensions(Map<String, Object> extensions) { return this; } diff --git a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/ProbeCondition.java b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/ProbeCondition.java index 2b56076d567b..8ded283ec935 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/ProbeCondition.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/ProbeCondition.java @@ -11,7 +11,6 @@ import datadog.trace.bootstrap.debugger.el.ValueReferenceResolver; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; -import java.util.Map; /** Implements expression language for probe condition */ public final class ProbeCondition implements DebuggerScript<Boolean> { @@ -104,55 +103,14 @@ public Boolean execute(ValueReferenceResolver valueRefResolver) { if (when == null) { return true; } - ProbeConditionResolver conditionResolver = new ProbeConditionResolver(valueRefResolver); - if (when.evaluate(conditionResolver)) { - then.evaluate(conditionResolver); - conditionResolver.checkRedacted(); + if (when.evaluate(valueRefResolver)) { + then.evaluate(valueRefResolver); return true; } - conditionResolver.checkRedacted(); return false; } public void accept(Visitor visitor) { when.accept(visitor); } - - private static class ProbeConditionResolver implements ValueReferenceResolver { - private final ValueReferenceResolver delegate; - private String expr; - private boolean redacted; - - public ProbeConditionResolver(ValueReferenceResolver delegate) { - this.delegate = delegate; - } - - @Override - public Object lookup(String name) { - return delegate.lookup(name); - } - - @Override - public Object getMember(Object target, String name) { - return delegate.getMember(target, name); - } - - @Override - public void redacted(String expr) { - this.redacted = true; - this.expr = expr; - } - - public void checkRedacted() { - if (redacted) { - throw new EvaluationException( - "Could not evaluate the expression because '" + expr + "' was redacted", expr); - } - } - - @Override - public ValueReferenceResolver withExtensions(Map<String, Object> extensions) { - return delegate.withExtensions(extensions); - } - } } diff --git a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/GetMemberExpression.java b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/GetMemberExpression.java index e0e923e9999f..1c6c3eab0bac 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/GetMemberExpression.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/GetMemberExpression.java @@ -27,7 +27,9 @@ public Value<?> evaluate(ValueReferenceResolver valueRefResolver) { try { Object member = valueRefResolver.getMember(targetValue.getValue(), memberName); if (member == Redaction.REDACTED_VALUE) { - valueRefResolver.redacted(PrettyPrintVisitor.print(this)); + String expr = PrettyPrintVisitor.print(this); + throw new EvaluationException( + "Could not evaluate the expression because '" + expr + "' was redacted", expr); } return Value.of(member); } catch (RuntimeException ex) { diff --git a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/IndexExpression.java b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/IndexExpression.java index bc767b7c41bb..02daee0c165c 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/IndexExpression.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/IndexExpression.java @@ -34,8 +34,9 @@ public Value<?> evaluate(ValueReferenceResolver valueRefResolver) { if (targetValue instanceof MapValue) { Object objKey = keyValue.getValue(); if (objKey instanceof String && Redaction.isRedactedKeyword((String) objKey)) { - valueRefResolver.redacted(PrettyPrintVisitor.print(this)); - result = Value.of(Redaction.REDACTED_VALUE); + String expr = PrettyPrintVisitor.print(this); + throw new EvaluationException( + "Could not evaluate the expression because '" + expr + "' was redacted", expr); } else { result = ((MapValue) targetValue).get(objKey); } diff --git a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ValueRefExpression.java b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ValueRefExpression.java index 095f935e86b3..f1f53c212056 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ValueRefExpression.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ValueRefExpression.java @@ -22,7 +22,9 @@ public Value<?> evaluate(ValueReferenceResolver valueRefResolver) { try { Object symbol = valueRefResolver.lookup(symbolName); if (symbol == Redaction.REDACTED_VALUE) { - valueRefResolver.redacted(PrettyPrintVisitor.print(this)); + String expr = PrettyPrintVisitor.print(this); + throw new EvaluationException( + "Could not evaluate the expression because '" + expr + "' was redacted", expr); } return Value.of(symbol); } catch (RuntimeException ex) { diff --git a/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/expressions/ValueRefExpressionTest.java b/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/expressions/ValueRefExpressionTest.java index 427f35b36ba2..be89476be398 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/expressions/ValueRefExpressionTest.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/expressions/ValueRefExpressionTest.java @@ -5,11 +5,11 @@ import static org.junit.jupiter.api.Assertions.*; import com.datadog.debugger.el.DSL; +import com.datadog.debugger.el.EvaluationException; import com.datadog.debugger.el.RefResolverHelper; import com.datadog.debugger.el.Value; import datadog.trace.bootstrap.debugger.el.ValueReferenceResolver; import datadog.trace.bootstrap.debugger.el.ValueReferences; -import datadog.trace.bootstrap.debugger.util.Redaction; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -112,7 +112,12 @@ public StoreSecret(String password) { } } StoreSecret instance = new StoreSecret("secret123"); - Value<?> val = valueRef.evaluate(RefResolverHelper.createResolver(instance)); - assertEquals(Redaction.REDACTED_VALUE, val.getValue()); + EvaluationException evaluationException = + assertThrows( + EvaluationException.class, + () -> valueRef.evaluate(RefResolverHelper.createResolver(instance))); + assertEquals( + "Could not evaluate the expression because 'password' was redacted", + evaluationException.getMessage()); } } diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java index 9997c0023b48..7a58caf1f987 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java @@ -1627,7 +1627,7 @@ public void keywordRedaction() throws IOException, URISyntaxException { Assertions.assertEquals(42, result); Snapshot snapshot = assertOneSnapshot(listener); assertEquals( - "arg=secret123 secret={redacted} password={redacted} fromMap={redacted}", + "arg=secret123 secret={Could not evaluate the expression because 'secret' was redacted} password={Could not evaluate the expression because 'this.password' was redacted} fromMap={Could not evaluate the expression because 'strMap[\"password\"]' was redacted}", snapshot.getMessage()); CapturedContext.CapturedValue secretLocalVar = snapshot.getCaptures().getReturn().getLocals().get("secret"); diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/SpanDecorationProbeInstrumentationTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/SpanDecorationProbeInstrumentationTest.java index 827136847eb0..9f3ef8c766e0 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/SpanDecorationProbeInstrumentationTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/SpanDecorationProbeInstrumentationTest.java @@ -290,16 +290,32 @@ PROBE_ID, ACTIVE, singletonList(decoration), CLASS_NAME, "process", null, null) @Test public void keywordRedaction() throws IOException, URISyntaxException { final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot28"; - SpanDecorationProbe.Decoration decoration = - createDecoration("tag1", "{password} {this.password} {strMap['password']}"); + SpanDecorationProbe.Decoration decoration1 = createDecoration("tag1", "{password}"); + SpanDecorationProbe.Decoration decoration2 = createDecoration("tag2", "{this.password}"); + SpanDecorationProbe.Decoration decoration3 = createDecoration("tag3", "{strMap['password']}"); + List<SpanDecorationProbe.Decoration> decorations = + Arrays.asList(decoration1, decoration2, decoration3); installSingleSpanDecoration( - CLASS_NAME, ACTIVE, decoration, "process", "int (java.lang.String)"); + CLASS_NAME, ACTIVE, decorations, "process", "int (java.lang.String)"); Class<?> testClass = compileAndLoadClass(CLASS_NAME); int result = Reflect.on(testClass).call("main", "secret123").get(); assertEquals(42, result); MutableSpan span = traceInterceptor.getFirstSpan(); - assertEquals("{redacted} {redacted} {redacted}", span.getTags().get("tag1")); + assertFalse(span.getTags().containsKey("tag1")); assertEquals(PROBE_ID.getId(), span.getTags().get("_dd.di.tag1.probe_id")); + assertEquals( + "Could not evaluate the expression because 'password' was redacted", + span.getTags().get("_dd.di.tag1.evaluation_error")); + assertFalse(span.getTags().containsKey("tag2")); + assertEquals(PROBE_ID.getId(), span.getTags().get("_dd.di.tag2.probe_id")); + assertEquals( + "Could not evaluate the expression because 'this.password' was redacted", + span.getTags().get("_dd.di.tag2.evaluation_error")); + assertFalse(span.getTags().containsKey("tag3")); + assertEquals(PROBE_ID.getId(), span.getTags().get("_dd.di.tag3.probe_id")); + assertEquals( + "Could not evaluate the expression because 'strMap[\"password\"]' was redacted", + span.getTags().get("_dd.di.tag3.evaluation_error")); } @Test From a58b8a274eddb5c2b853c33a2dae95bbf7b3aa43 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel <jean-philippe.bempel@datadoghq.com> Date: Mon, 16 Oct 2023 14:22:29 +0200 Subject: [PATCH 22/29] fix bug identifier normalization --- .../java/datadog/trace/bootstrap/debugger/util/Redaction.java | 2 +- .../datadog/trace/bootstrap/debugger/util/RedactionTest.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java index 0a1242c129bb..b549938352cb 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java @@ -54,7 +54,7 @@ private static String normalize(String name) { boolean isRemovable = isRemovableChar(c); if (isUpper || isRemovable || sb != null) { if (sb == null) { - sb = new StringBuilder(); + sb = new StringBuilder(name.substring(0, i)); } if (isUpper) { sb.append(Character.toLowerCase(c)); diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/test/java/datadog/trace/bootstrap/debugger/util/RedactionTest.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/test/java/datadog/trace/bootstrap/debugger/util/RedactionTest.java index 97f8bae88e00..36a3c5a818b0 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/test/java/datadog/trace/bootstrap/debugger/util/RedactionTest.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/test/java/datadog/trace/bootstrap/debugger/util/RedactionTest.java @@ -14,6 +14,7 @@ public void test() { assertFalse(Redaction.isRedactedKeyword("@-_$")); assertTrue(Redaction.isRedactedKeyword("password")); assertTrue(Redaction.isRedactedKeyword("PassWord")); + assertTrue(Redaction.isRedactedKeyword("pass-word")); assertTrue(Redaction.isRedactedKeyword("_Pass-Word_")); assertTrue(Redaction.isRedactedKeyword("$pass_worD")); assertTrue(Redaction.isRedactedKeyword("@passWord@")); From f282e8c0d643cfeb9d2a85b12029dec7b19a4187 Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko <121111529+nikita-tkachenko-datadog@users.noreply.github.com> Date: Mon, 16 Oct 2023 15:59:40 +0200 Subject: [PATCH 23/29] Split JUnit 5 instrumentation logic into framework-specific modules (#5907) --- .../junit5/CucumberTracingListener.java | 192 ++++++++++++ .../instrumentation/junit5/CucumberUtils.java | 98 ++++++ .../junit5/JUnit5CucumberInstrumentation.java | 94 ++++++ .../JUnit5CucumberItrInstrumentation.java | 110 +++++++ .../junit-5.3/spock/build.gradle | 2 + .../junit5/JUnit5SpockInstrumentation.java | 94 ++++++ .../junit5/JUnit5SpockItrInstrumentation.java | 110 +++++++ .../junit5/SpockTracingListener.java | 228 ++++++++++++++ .../instrumentation/junit5/SpockUtils.java | 128 ++++++++ .../junit5/CompositeEngineListener.java | 49 +++ .../junit5/JUnit5Instrumentation.java | 59 ++-- .../junit5/JUnit5ItrInstrumentation.java | 20 +- .../junit5/JUnitPlatformLauncherUtils.java | 150 --------- .../junit5/JUnitPlatformUtils.java | 291 ++---------------- .../junit5/TracingListener.java | 275 ++++------------- 15 files changed, 1232 insertions(+), 668 deletions(-) create mode 100644 dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/CucumberTracingListener.java create mode 100644 dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/CucumberUtils.java create mode 100644 dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberInstrumentation.java create mode 100644 dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberItrInstrumentation.java create mode 100644 dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockInstrumentation.java create mode 100644 dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockItrInstrumentation.java create mode 100644 dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/SpockTracingListener.java create mode 100644 dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java create mode 100644 dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/CompositeEngineListener.java delete mode 100644 dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnitPlatformLauncherUtils.java diff --git a/dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/CucumberTracingListener.java b/dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/CucumberTracingListener.java new file mode 100644 index 000000000000..6a8602efc494 --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/CucumberTracingListener.java @@ -0,0 +1,192 @@ +package datadog.trace.instrumentation.junit5; + +import datadog.trace.api.Pair; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.engine.support.descriptor.ClasspathResourceSource; + +public class CucumberTracingListener implements EngineExecutionListener { + + private final String testFramework; + private final String testFrameworkVersion; + + public CucumberTracingListener(TestEngine testEngine) { + testFramework = testEngine.getId(); + testFrameworkVersion = CucumberUtils.getCucumberVersion(testEngine); + } + + @Override + public void dynamicTestRegistered(TestDescriptor testDescriptor) { + // no op + } + + @Override + public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) { + // no op + } + + @Override + public void executionStarted(final TestDescriptor testDescriptor) { + if (testDescriptor.isContainer()) { + containerExecutionStarted(testDescriptor); + } else if (testDescriptor.isTest()) { + testCaseExecutionStarted(testDescriptor); + } + } + + @Override + public void executionFinished( + TestDescriptor testDescriptor, TestExecutionResult testExecutionResult) { + if (testDescriptor.isContainer()) { + containerExecutionFinished(testDescriptor, testExecutionResult); + } else if (testDescriptor.isTest()) { + testCaseExecutionFinished(testDescriptor, testExecutionResult); + } + } + + private void containerExecutionStarted(final TestDescriptor testDescriptor) { + if (!CucumberUtils.isFeature(testDescriptor.getUniqueId())) { + return; + } + + String testSuiteName = testDescriptor.getLegacyReportingName(); + List<String> tags = + testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteStart( + testSuiteName, testFramework, testFrameworkVersion, null, tags, false); + } + + private void containerExecutionFinished( + final TestDescriptor testDescriptor, final TestExecutionResult testExecutionResult) { + if (!CucumberUtils.isFeature(testDescriptor.getUniqueId())) { + return; + } + + String testSuiteName = testDescriptor.getLegacyReportingName(); + Throwable throwable = testExecutionResult.getThrowable().orElse(null); + if (throwable != null) { + if (JUnitPlatformUtils.isAssumptionFailure(throwable)) { + + String reason = throwable.getMessage(); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteSkip(testSuiteName, null, reason); + + for (TestDescriptor child : testDescriptor.getChildren()) { + executionSkipped(child, reason); + } + + } else { + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteFailure( + testSuiteName, null, throwable); + } + } + + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteFinish(testSuiteName, null); + } + + private void testCaseExecutionStarted(final TestDescriptor testDescriptor) { + TestSource testSource = testDescriptor.getSource().orElse(null); + if (testSource instanceof ClasspathResourceSource) { + testResourceExecutionStarted(testDescriptor, (ClasspathResourceSource) testSource); + } + } + + private void testResourceExecutionStarted( + TestDescriptor testDescriptor, ClasspathResourceSource testSource) { + String classpathResourceName = testSource.getClasspathResourceName(); + + Pair<String, String> names = + CucumberUtils.getFeatureAndScenarioNames(testDescriptor, classpathResourceName); + String testSuiteName = names.getLeft(); + String testName = names.getRight(); + + List<String> tags = + testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); + + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( + testSuiteName, + testName, + null, + testFramework, + testFrameworkVersion, + null, + tags, + null, + null, + null); + } + + private void testCaseExecutionFinished( + final TestDescriptor testDescriptor, final TestExecutionResult testExecutionResult) { + TestSource testSource = testDescriptor.getSource().orElse(null); + if (testSource instanceof ClasspathResourceSource) { + testResourceExecutionFinished( + testDescriptor, testExecutionResult, (ClasspathResourceSource) testSource); + } + } + + private void testResourceExecutionFinished( + TestDescriptor testDescriptor, + TestExecutionResult testExecutionResult, + ClasspathResourceSource testSource) { + String classpathResourceName = testSource.getClasspathResourceName(); + + Pair<String, String> names = + CucumberUtils.getFeatureAndScenarioNames(testDescriptor, classpathResourceName); + String testSuiteName = names.getLeft(); + String testName = names.getRight(); + + Throwable throwable = testExecutionResult.getThrowable().orElse(null); + if (throwable != null) { + if (JUnitPlatformUtils.isAssumptionFailure(throwable)) { + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSkip( + testSuiteName, null, testName, null, null, throwable.getMessage()); + } else { + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestFailure( + testSuiteName, null, testName, null, null, throwable); + } + } + + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestFinish( + testSuiteName, null, testName, null, null); + } + + @Override + public void executionSkipped(final TestDescriptor testDescriptor, final String reason) { + TestSource testSource = testDescriptor.getSource().orElse(null); + if (testSource instanceof ClasspathResourceSource) { + testResourceExecutionSkipped(testDescriptor, (ClasspathResourceSource) testSource, reason); + } + } + + private void testResourceExecutionSkipped( + TestDescriptor testDescriptor, ClasspathResourceSource testSource, String reason) { + String classpathResourceName = testSource.getClasspathResourceName(); + Pair<String, String> names = + CucumberUtils.getFeatureAndScenarioNames(testDescriptor, classpathResourceName); + String testSuiteName = names.getLeft(); + String testName = names.getRight(); + + List<String> tags = + testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); + + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestIgnore( + testSuiteName, + testName, + null, + testFramework, + testFrameworkVersion, + null, + tags, + null, + null, + null, + reason); + } +} diff --git a/dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/CucumberUtils.java b/dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/CucumberUtils.java new file mode 100644 index 000000000000..ea3ea5c7ce35 --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/CucumberUtils.java @@ -0,0 +1,98 @@ +package datadog.trace.instrumentation.junit5; + +import datadog.trace.api.Pair; +import datadog.trace.api.civisibility.config.SkippableTest; +import java.io.InputStream; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import java.util.Optional; +import java.util.Properties; +import javax.annotation.Nullable; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.ClasspathResourceSource; + +public abstract class CucumberUtils { + + public static @Nullable String getCucumberVersion(TestEngine cucumberEngine) { + try (InputStream cucumberPropsStream = + cucumberEngine + .getClass() + .getClassLoader() + .getResourceAsStream( + "META-INF/maven/io.cucumber/cucumber-junit-platform-engine/pom.properties")) { + Properties cucumberProps = new Properties(); + cucumberProps.load(cucumberPropsStream); + String version = cucumberProps.getProperty("version"); + if (version != null) { + return version; + } + } catch (Exception e) { + // fallback below + } + return cucumberEngine.getVersion().orElse(null); + } + + public static Pair<String, String> getFeatureAndScenarioNames( + TestDescriptor testDescriptor, String fallbackFeatureName) { + String featureName = fallbackFeatureName; + + Deque<TestDescriptor> scenarioDescriptors = new ArrayDeque<>(); + scenarioDescriptors.push(testDescriptor); + + TestDescriptor current = testDescriptor; + while (true) { + Optional<TestDescriptor> parent = current.getParent(); + if (!parent.isPresent()) { + break; + } + + current = parent.get(); + UniqueId currentId = current.getUniqueId(); + if (isFeature(currentId)) { + featureName = current.getDisplayName(); + break; + + } else { + scenarioDescriptors.push(current); + } + } + + StringBuilder scenarioName = new StringBuilder(); + while (!scenarioDescriptors.isEmpty()) { + TestDescriptor descriptor = scenarioDescriptors.pop(); + scenarioName.append(descriptor.getDisplayName()); + if (!scenarioDescriptors.isEmpty()) { + scenarioName.append('.'); + } + } + + return Pair.of(featureName, scenarioName.toString()); + } + + public static boolean isFeature(UniqueId uniqueId) { + List<UniqueId.Segment> segments = uniqueId.getSegments(); + UniqueId.Segment lastSegment = segments.listIterator(segments.size()).previous(); + return "feature".equals(lastSegment.getType()); + } + + public static SkippableTest toSkippableTest(TestDescriptor testDescriptor) { + TestSource testSource = testDescriptor.getSource().orElse(null); + if (testSource instanceof ClasspathResourceSource) { + ClasspathResourceSource classpathResourceSource = (ClasspathResourceSource) testSource; + String classpathResourceName = classpathResourceSource.getClasspathResourceName(); + + Pair<String, String> names = + getFeatureAndScenarioNames(testDescriptor, classpathResourceName); + String testSuiteName = names.getLeft(); + String testName = names.getRight(); + return new SkippableTest(testSuiteName, testName, null, null); + + } else { + return null; + } + } +} diff --git a/dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberInstrumentation.java b/dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberInstrumentation.java new file mode 100644 index 000000000000..2f437e1c1960 --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberInstrumentation.java @@ -0,0 +1,94 @@ +package datadog.trace.instrumentation.junit5; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers.hasClassNamed; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.cucumber.junit.platform.engine.CucumberTestEngine; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatcher; +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService; + +@AutoService(Instrumenter.class) +public class JUnit5CucumberInstrumentation extends Instrumenter.CiVisibility + implements Instrumenter.ForSingleType { + + public JUnit5CucumberInstrumentation() { + super("ci-visibility", "junit-5", "junit-5-cucumber"); + } + + @Override + public ElementMatcher<ClassLoader> classLoaderMatcher() { + return hasClassNamed("io.cucumber.junit.platform.engine.CucumberTestEngine"); + } + + @Override + public String instrumentedType() { + return "org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".JUnitPlatformUtils", + packageName + ".CucumberUtils", + packageName + ".TestEventsHandlerHolder", + packageName + ".CucumberTracingListener", + packageName + ".CompositeEngineListener", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("execute").and(takesArgument(0, named("org.junit.platform.engine.ExecutionRequest"))), + JUnit5CucumberInstrumentation.class.getName() + "$CucumberAdvice"); + } + + @SuppressFBWarnings( + value = "UC_USELESS_OBJECT", + justification = "executionRequest is the argument of the original method") + public static class CucumberAdvice { + + @Advice.OnMethodEnter + public static void addTracingListener( + @Advice.This TestEngine testEngine, + @Advice.Argument(value = 0, readOnly = false) ExecutionRequest executionRequest) { + if (!(testEngine instanceof CucumberTestEngine)) { + // wrong test engine + return; + } + + if (JUnitPlatformUtils.isTestInProgress()) { + // a test case that is in progress starts a new JUnit instance. + // It might be done in order to achieve classloader isolation + // (for example, spring-boot uses this technique). + // We are already tracking the active test case, + // and do not want to report the "embedded" JUnit execution + // as a separate module + return; + } + + CucumberTracingListener tracingListener = new CucumberTracingListener(testEngine); + EngineExecutionListener originalListener = executionRequest.getEngineExecutionListener(); + EngineExecutionListener compositeListener = + new CompositeEngineListener(tracingListener, originalListener); + executionRequest = + new ExecutionRequest( + executionRequest.getRootTestDescriptor(), + compositeListener, + executionRequest.getConfigurationParameters()); + } + + // JUnit 5.3.0 and above + public static void muzzleCheck(final SameThreadHierarchicalTestExecutorService service) { + service.invokeAll(null); + } + } +} diff --git a/dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberItrInstrumentation.java b/dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberItrInstrumentation.java new file mode 100644 index 000000000000..38b605727794 --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/cucumber/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberItrInstrumentation.java @@ -0,0 +1,110 @@ +package datadog.trace.instrumentation.junit5; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers.hasClassNamed; +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.extendsClass; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.api.Config; +import datadog.trace.api.civisibility.InstrumentationBridge; +import datadog.trace.api.civisibility.config.SkippableTest; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Collection; +import java.util.Set; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.support.hierarchical.Node; +import org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService; + +@AutoService(Instrumenter.class) +public class JUnit5CucumberItrInstrumentation extends Instrumenter.CiVisibility + implements Instrumenter.ForTypeHierarchy { + + public JUnit5CucumberItrInstrumentation() { + super("ci-visibility", "junit-5", "junit-5-cucumber"); + } + + @Override + public ElementMatcher<ClassLoader> classLoaderMatcher() { + return hasClassNamed("io.cucumber.junit.platform.engine.CucumberTestEngine"); + } + + @Override + public boolean isApplicable(Set<TargetSystem> enabledSystems) { + return super.isApplicable(enabledSystems) && Config.get().isCiVisibilityItrEnabled(); + } + + @Override + public String hierarchyMarkerType() { + return "io.cucumber.junit.platform.engine.CucumberTestEngine"; + } + + @Override + public ElementMatcher<TypeDescription> hierarchyMatcher() { + return extendsClass(named("io.cucumber.junit.platform.engine.NodeDescriptor")) + // legacy Cucumber versions + .or(extendsClass(named("io.cucumber.junit.platform.engine.PickleDescriptor"))); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".CucumberUtils", packageName + ".TestEventsHandlerHolder", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("shouldBeSkipped").and(takesArguments(1)), + JUnit5CucumberItrInstrumentation.class.getName() + "$JUnit5ItrAdvice"); + } + + /** + * !!!!!!!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!!!! Do not use or refer to any classes from {@code + * org.junit.platform.launcher} package in here: in some Gradle projects this package is not + * available in CL where this instrumentation is injected + */ + public static class JUnit5ItrAdvice { + + @SuppressFBWarnings( + value = "UC_USELESS_OBJECT", + justification = "skipResult is the return value of the instrumented method") + @Advice.OnMethodExit + public static void shouldBeSkipped( + @Advice.This TestDescriptor testDescriptor, + @Advice.Return(readOnly = false) Node.SkipResult skipResult) { + if (skipResult.isSkipped()) { + return; + } + + if (TestEventsHandlerHolder.TEST_EVENTS_HANDLER == null) { + // should only happen in integration tests + // because we cannot avoid instrumenting ourselves + return; + } + + Collection<TestTag> tags = testDescriptor.getTags(); + for (TestTag tag : tags) { + if (InstrumentationBridge.ITR_UNSKIPPABLE_TAG.equals(tag.getName())) { + return; + } + } + + SkippableTest test = CucumberUtils.toSkippableTest(testDescriptor); + if (test != null && TestEventsHandlerHolder.TEST_EVENTS_HANDLER.skip(test)) { + skipResult = Node.SkipResult.skip(InstrumentationBridge.ITR_SKIP_REASON); + } + } + + // JUnit 5.3.0 and above + public static void muzzleCheck(final SameThreadHierarchicalTestExecutorService service) { + service.invokeAll(null); + } + } +} diff --git a/dd-java-agent/instrumentation/junit-5.3/spock/build.gradle b/dd-java-agent/instrumentation/junit-5.3/spock/build.gradle index d472ad120f10..69a2cb2f59a8 100644 --- a/dd-java-agent/instrumentation/junit-5.3/spock/build.gradle +++ b/dd-java-agent/instrumentation/junit-5.3/spock/build.gradle @@ -15,6 +15,8 @@ addTestSuiteForDir('latestDepTest', 'test') dependencies { implementation project(':dd-java-agent:instrumentation:junit-5.3') + compileOnly group: 'org.junit.platform', name: 'junit-platform-launcher', version: '1.7.2' + compileOnly group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.7.2' compileOnly group: 'org.spockframework', name: 'spock-core', version: "2.0-groovy-${spockGroovyVersion}" testImplementation testFixtures(project(':dd-java-agent:agent-ci-visibility')) diff --git a/dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockInstrumentation.java b/dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockInstrumentation.java new file mode 100644 index 000000000000..329ab1e19dfb --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockInstrumentation.java @@ -0,0 +1,94 @@ +package datadog.trace.instrumentation.junit5; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers.hasClassNamed; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatcher; +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService; +import org.spockframework.runtime.SpockEngine; + +@AutoService(Instrumenter.class) +public class JUnit5SpockInstrumentation extends Instrumenter.CiVisibility + implements Instrumenter.ForSingleType { + + public JUnit5SpockInstrumentation() { + super("ci-visibility", "junit-5", "junit-5-spock"); + } + + @Override + public ElementMatcher<ClassLoader> classLoaderMatcher() { + return hasClassNamed("org.spockframework.runtime.SpockEngine"); + } + + @Override + public String instrumentedType() { + return "org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".JUnitPlatformUtils", + packageName + ".SpockUtils", + packageName + ".TestEventsHandlerHolder", + packageName + ".SpockTracingListener", + packageName + ".CompositeEngineListener", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("execute").and(takesArgument(0, named("org.junit.platform.engine.ExecutionRequest"))), + JUnit5SpockInstrumentation.class.getName() + "$SpockAdvice"); + } + + @SuppressFBWarnings( + value = "UC_USELESS_OBJECT", + justification = "executionRequest is the argument of the original method") + public static class SpockAdvice { + + @Advice.OnMethodEnter + public static void addTracingListener( + @Advice.This TestEngine testEngine, + @Advice.Argument(value = 0, readOnly = false) ExecutionRequest executionRequest) { + if (!(testEngine instanceof SpockEngine)) { + // wrong test engine + return; + } + + if (JUnitPlatformUtils.isTestInProgress()) { + // a test case that is in progress starts a new JUnit instance. + // It might be done in order to achieve classloader isolation + // (for example, spring-boot uses this technique). + // We are already tracking the active test case, + // and do not want to report the "embedded" JUnit execution + // as a separate module + return; + } + + SpockTracingListener tracingListener = new SpockTracingListener(testEngine); + EngineExecutionListener originalListener = executionRequest.getEngineExecutionListener(); + EngineExecutionListener compositeListener = + new CompositeEngineListener(tracingListener, originalListener); + executionRequest = + new ExecutionRequest( + executionRequest.getRootTestDescriptor(), + compositeListener, + executionRequest.getConfigurationParameters()); + } + + // JUnit 5.3.0 and above + public static void muzzleCheck(final SameThreadHierarchicalTestExecutorService service) { + service.invokeAll(null); + } + } +} diff --git a/dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockItrInstrumentation.java b/dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockItrInstrumentation.java new file mode 100644 index 000000000000..9c711eb8f0ce --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockItrInstrumentation.java @@ -0,0 +1,110 @@ +package datadog.trace.instrumentation.junit5; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers.hasClassNamed; +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.extendsClass; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.api.Config; +import datadog.trace.api.civisibility.InstrumentationBridge; +import datadog.trace.api.civisibility.config.SkippableTest; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Collection; +import java.util.Set; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.support.hierarchical.Node; +import org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService; +import org.spockframework.runtime.SpockNode; + +@AutoService(Instrumenter.class) +public class JUnit5SpockItrInstrumentation extends Instrumenter.CiVisibility + implements Instrumenter.ForTypeHierarchy { + + public JUnit5SpockItrInstrumentation() { + super("ci-visibility", "junit-5", "junit-5-spock"); + } + + @Override + public ElementMatcher<ClassLoader> classLoaderMatcher() { + return hasClassNamed("org.spockframework.runtime.SpockEngine"); + } + + @Override + public boolean isApplicable(Set<TargetSystem> enabledSystems) { + return super.isApplicable(enabledSystems) && Config.get().isCiVisibilityItrEnabled(); + } + + @Override + public String hierarchyMarkerType() { + return "org.spockframework.runtime.SpockNode"; + } + + @Override + public ElementMatcher<TypeDescription> hierarchyMatcher() { + return extendsClass(named(hierarchyMarkerType())); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".JUnitPlatformUtils", + packageName + ".SpockUtils", + packageName + ".TestEventsHandlerHolder", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("shouldBeSkipped").and(takesArguments(1)), + JUnit5SpockItrInstrumentation.class.getName() + "$JUnit5ItrAdvice"); + } + + /** + * !!!!!!!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!!!! Do not use or refer to any classes from {@code + * org.junit.platform.launcher} package in here: in some Gradle projects this package is not + * available in CL where this instrumentation is injected + */ + public static class JUnit5ItrAdvice { + + @SuppressFBWarnings( + value = "UC_USELESS_OBJECT", + justification = "skipResult is the return value of the instrumented method") + @Advice.OnMethodExit + public static void shouldBeSkipped( + @Advice.This SpockNode<?> spockNode, + @Advice.Return(readOnly = false) Node.SkipResult skipResult) { + if (skipResult.isSkipped()) { + return; + } + + if (TestEventsHandlerHolder.TEST_EVENTS_HANDLER == null) { + // should only happen in integration tests + // because we cannot avoid instrumenting ourselves + return; + } + + Collection<TestTag> tags = SpockUtils.getTags(spockNode); + for (TestTag tag : tags) { + if (InstrumentationBridge.ITR_UNSKIPPABLE_TAG.equals(tag.getName())) { + return; + } + } + + SkippableTest test = SpockUtils.toSkippableTest(spockNode); + if (test != null && TestEventsHandlerHolder.TEST_EVENTS_HANDLER.skip(test)) { + skipResult = Node.SkipResult.skip(InstrumentationBridge.ITR_SKIP_REASON); + } + } + + // JUnit 5.3.0 and above + public static void muzzleCheck(final SameThreadHierarchicalTestExecutorService service) { + service.invokeAll(null); + } + } +} diff --git a/dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/SpockTracingListener.java b/dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/SpockTracingListener.java new file mode 100644 index 000000000000..af233d1c2f49 --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/SpockTracingListener.java @@ -0,0 +1,228 @@ +package datadog.trace.instrumentation.junit5; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.descriptor.MethodSource; + +public class SpockTracingListener implements EngineExecutionListener { + + private final String testFramework; + private final String testFrameworkVersion; + + public SpockTracingListener(TestEngine testEngine) { + testFramework = testEngine.getId(); + testFrameworkVersion = testEngine.getVersion().orElse(null); + } + + @Override + public void dynamicTestRegistered(TestDescriptor testDescriptor) { + // no op + } + + @Override + public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) { + // no op + } + + @Override + public void executionStarted(final TestDescriptor testDescriptor) { + if (testDescriptor.isContainer()) { + containerExecutionStarted(testDescriptor); + } else if (testDescriptor.isTest()) { + testCaseExecutionStarted(testDescriptor); + } + } + + @Override + public void executionFinished( + TestDescriptor testDescriptor, TestExecutionResult testExecutionResult) { + if (testDescriptor.isContainer()) { + containerExecutionFinished(testDescriptor, testExecutionResult); + } else if (testDescriptor.isTest()) { + testCaseExecutionFinished(testDescriptor, testExecutionResult); + } + } + + private void containerExecutionStarted(final TestDescriptor testDescriptor) { + if (!SpockUtils.isSpec(testDescriptor)) { + return; + } + + Class<?> testClass = JUnitPlatformUtils.getJavaClass(testDescriptor); + String testSuiteName = + testClass != null ? testClass.getName() : testDescriptor.getLegacyReportingName(); + + List<String> tags = + testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteStart( + testSuiteName, testFramework, testFrameworkVersion, testClass, tags, false); + } + + private void containerExecutionFinished( + final TestDescriptor testDescriptor, final TestExecutionResult testExecutionResult) { + if (!SpockUtils.isSpec(testDescriptor)) { + return; + } + + Class<?> testClass = JUnitPlatformUtils.getJavaClass(testDescriptor); + String testSuiteName = + testClass != null ? testClass.getName() : testDescriptor.getLegacyReportingName(); + + Throwable throwable = testExecutionResult.getThrowable().orElse(null); + if (throwable != null) { + if (JUnitPlatformUtils.isAssumptionFailure(throwable)) { + + String reason = throwable.getMessage(); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteSkip( + testSuiteName, testClass, reason); + + for (TestDescriptor child : testDescriptor.getChildren()) { + executionSkipped(child, reason); + } + + } else { + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteFailure( + testSuiteName, testClass, throwable); + } + } + + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteFinish(testSuiteName, testClass); + } + + private void testCaseExecutionStarted(final TestDescriptor testDescriptor) { + TestSource testSource = testDescriptor.getSource().orElse(null); + if (testSource instanceof MethodSource) { + testMethodExecutionStarted(testDescriptor, (MethodSource) testSource); + } + } + + private void testMethodExecutionStarted(TestDescriptor testDescriptor, MethodSource testSource) { + String testSuitName = testSource.getClassName(); + String displayName = testDescriptor.getDisplayName(); + + String testParameters = JUnitPlatformUtils.getParameters(testSource, displayName); + List<String> tags = + testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); + + Class<?> testClass = testSource.getJavaClass(); + Method testMethod = SpockUtils.getTestMethod(testSource); + String testMethodName = testSource.getMethodName(); + + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( + testSuitName, + displayName, + null, + testFramework, + testFrameworkVersion, + testParameters, + tags, + testClass, + testMethodName, + testMethod); + } + + private void testCaseExecutionFinished( + final TestDescriptor testDescriptor, final TestExecutionResult testExecutionResult) { + TestSource testSource = testDescriptor.getSource().orElse(null); + if (testSource instanceof MethodSource) { + testMethodExecutionFinished(testDescriptor, testExecutionResult, (MethodSource) testSource); + } + } + + private static void testMethodExecutionFinished( + TestDescriptor testDescriptor, + TestExecutionResult testExecutionResult, + MethodSource testSource) { + String testSuiteName = testSource.getClassName(); + Class<?> testClass = testSource.getJavaClass(); + String displayName = testDescriptor.getDisplayName(); + String testParameters = JUnitPlatformUtils.getParameters(testSource, displayName); + + Throwable throwable = testExecutionResult.getThrowable().orElse(null); + if (throwable != null) { + if (JUnitPlatformUtils.isAssumptionFailure(throwable)) { + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSkip( + testSuiteName, testClass, displayName, null, testParameters, throwable.getMessage()); + } else { + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestFailure( + testSuiteName, testClass, displayName, null, testParameters, throwable); + } + } + + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestFinish( + testSuiteName, testClass, displayName, null, testParameters); + } + + @Override + public void executionSkipped(final TestDescriptor testDescriptor, final String reason) { + TestSource testSource = testDescriptor.getSource().orElse(null); + + if (testSource instanceof ClassSource) { + // The annotation @Disabled is kept at type level. + containerExecutionSkipped(testDescriptor, reason); + + } else if (testSource instanceof MethodSource) { + // The annotation @Disabled is kept at method level. + testMethodExecutionSkipped(testDescriptor, (MethodSource) testSource, reason); + } + } + + private void containerExecutionSkipped(final TestDescriptor testDescriptor, final String reason) { + if (!SpockUtils.isSpec(testDescriptor)) { + return; + } + + Class<?> testClass = JUnitPlatformUtils.getJavaClass(testDescriptor); + String testSuiteName = + testClass != null ? testClass.getName() : testDescriptor.getLegacyReportingName(); + + List<String> tags = + testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); + + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteStart( + testSuiteName, testFramework, testFrameworkVersion, testClass, tags, false); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteSkip(testSuiteName, testClass, reason); + + for (TestDescriptor child : testDescriptor.getChildren()) { + executionSkipped(child, reason); + } + + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteFinish(testSuiteName, testClass); + } + + private void testMethodExecutionSkipped( + final TestDescriptor testDescriptor, final MethodSource methodSource, final String reason) { + String testSuiteName = methodSource.getClassName(); + String displayName = testDescriptor.getDisplayName(); + + String testParameters = JUnitPlatformUtils.getParameters(methodSource, displayName); + List<String> tags = + testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); + + Class<?> testClass = methodSource.getJavaClass(); + Method testMethod = SpockUtils.getTestMethod(methodSource); + String testMethodName = methodSource.getMethodName(); + + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestIgnore( + testSuiteName, + displayName, + null, + testFramework, + testFrameworkVersion, + testParameters, + tags, + testClass, + testMethodName, + testMethod, + reason); + } +} diff --git a/dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java b/dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java new file mode 100644 index 000000000000..457b4cba642d --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/spock/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java @@ -0,0 +1,128 @@ +package datadog.trace.instrumentation.junit5; + +import datadog.trace.api.civisibility.config.SkippableTest; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.junit.platform.commons.util.ClassLoaderUtils; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.spockframework.runtime.SpockNode; +import org.spockframework.runtime.model.FeatureMetadata; +import org.spockframework.runtime.model.SpecElementInfo; + +public class SpockUtils { + + private static final MethodHandle GET_TEST_TAGS; + private static final MethodHandle GET_TEST_TAG_VALUE; + + static { + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + ClassLoader defaultClassLoader = ClassLoaderUtils.getDefaultClassLoader(); + GET_TEST_TAGS = accessGetTestTags(lookup, defaultClassLoader); + GET_TEST_TAG_VALUE = accessGetTestTagValue(lookup, defaultClassLoader); + } + + private static MethodHandle accessGetTestTags( + MethodHandles.Lookup lookup, ClassLoader classLoader) { + try { + Class<?> testTaggable = + classLoader.loadClass("org.spockframework.runtime.model.ITestTaggable"); + Method method = testTaggable.getDeclaredMethod("getTestTags"); + return lookup.unreflect(method); + } catch (Throwable throwable) { + return null; + } + } + + private static MethodHandle accessGetTestTagValue( + MethodHandles.Lookup lookup, ClassLoader classLoader) { + try { + Class<?> testTaggable = classLoader.loadClass("org.spockframework.runtime.model.TestTag"); + Method method = testTaggable.getDeclaredMethod("getValue"); + return lookup.unreflect(method); + } catch (Throwable throwable) { + return null; + } + } + + /* + * ITestTaggable and TestTag classes are accessed via reflection + * since they're available starting with Spock 2.2, + * and we support Spock 2.0 + */ + public static Collection<TestTag> getTags(SpockNode<?> spockNode) { + try { + Collection<TestTag> junitPlatformTestTags = new ArrayList<>(); + SpecElementInfo<?, ?> nodeInfo = spockNode.getNodeInfo(); + Collection<?> testTags = (Collection<?>) GET_TEST_TAGS.invoke(nodeInfo); + for (Object testTag : testTags) { + String tagValue = (String) GET_TEST_TAG_VALUE.invoke(testTag); + TestTag junitPlatformTestTag = TestTag.create(tagValue); + junitPlatformTestTags.add(junitPlatformTestTag); + } + return junitPlatformTestTags; + + } catch (Throwable throwable) { + // ignore + return Collections.emptyList(); + } + } + + public static Method getTestMethod(MethodSource methodSource) { + String methodName = methodSource.getMethodName(); + if (methodName == null) { + return null; + } + + Class<?> testClass = methodSource.getJavaClass(); + if (testClass == null) { + return null; + } + + try { + for (Method declaredMethod : testClass.getDeclaredMethods()) { + FeatureMetadata featureMetadata = declaredMethod.getAnnotation(FeatureMetadata.class); + if (featureMetadata == null) { + continue; + } + + if (methodName.equals(featureMetadata.name())) { + return declaredMethod; + } + } + + } catch (Throwable e) { + // ignore + } + return null; + } + + public static SkippableTest toSkippableTest(SpockNode spockNode) { + TestSource testSource = spockNode.getSource().orElse(null); + if (testSource instanceof MethodSource) { + MethodSource methodSource = (MethodSource) testSource; + String testSuiteName = methodSource.getClassName(); + String displayName = spockNode.getDisplayName(); + String testParameters = JUnitPlatformUtils.getParameters(methodSource, displayName); + return new SkippableTest(testSuiteName, displayName, testParameters, null); + + } else { + return null; + } + } + + public static boolean isSpec(TestDescriptor testDescriptor) { + UniqueId uniqueId = testDescriptor.getUniqueId(); + List<UniqueId.Segment> segments = uniqueId.getSegments(); + UniqueId.Segment lastSegment = segments.get(segments.size() - 1); + return "spec".equals(lastSegment.getType()); + } +} diff --git a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/CompositeEngineListener.java b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/CompositeEngineListener.java new file mode 100644 index 000000000000..005572f9d97c --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/CompositeEngineListener.java @@ -0,0 +1,49 @@ +package datadog.trace.instrumentation.junit5; + +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.ReportEntry; + +public class CompositeEngineListener implements EngineExecutionListener { + + private final EngineExecutionListener tracingListener; + private final EngineExecutionListener delegate; + + public CompositeEngineListener( + EngineExecutionListener tracingListener, EngineExecutionListener delegate) { + this.tracingListener = tracingListener; + this.delegate = delegate; + } + + @Override + public void dynamicTestRegistered(TestDescriptor testDescriptor) { + // tracing listener is not interested in this event + delegate.dynamicTestRegistered(testDescriptor); + } + + @Override + public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) { + // tracing listener is not interested in this event + delegate.dynamicTestRegistered(testDescriptor); + } + + @Override + public void executionStarted(TestDescriptor testDescriptor) { + tracingListener.executionStarted(testDescriptor); + delegate.executionStarted(testDescriptor); + } + + @Override + public void executionSkipped(TestDescriptor testDescriptor, String reason) { + tracingListener.executionSkipped(testDescriptor, reason); + delegate.executionSkipped(testDescriptor, reason); + } + + @Override + public void executionFinished( + TestDescriptor testDescriptor, TestExecutionResult testExecutionResult) { + tracingListener.executionFinished(testDescriptor, testExecutionResult); + delegate.executionFinished(testDescriptor, testExecutionResult); + } +} diff --git a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnit5Instrumentation.java b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnit5Instrumentation.java index 4600e6687342..c3bbc5449335 100644 --- a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnit5Instrumentation.java +++ b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnit5Instrumentation.java @@ -2,56 +2,56 @@ import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface; import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; -import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import com.google.auto.service.AutoService; import datadog.trace.agent.tooling.Instrumenter; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.util.ArrayList; -import java.util.Collection; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.ExecutionRequest; import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService; -import org.junit.platform.launcher.TestExecutionListener; -import org.junit.platform.launcher.core.LauncherConfig; @AutoService(Instrumenter.class) public class JUnit5Instrumentation extends Instrumenter.CiVisibility implements Instrumenter.ForTypeHierarchy { public JUnit5Instrumentation() { - super("junit", "junit-5"); + super("ci-visibility", "junit-5"); } @Override public String hierarchyMarkerType() { - return "org.junit.platform.launcher.core.LauncherConfig"; + return "org.junit.platform.engine.TestEngine"; } @Override public ElementMatcher<TypeDescription> hierarchyMatcher() { - return implementsInterface(named(hierarchyMarkerType())); + return implementsInterface(named(hierarchyMarkerType())) + // JUnit 4 has a dedicated instrumentation + .and(not(named("org.junit.vintage.engine.VintageTestEngine"))) + // suites are only used to organize other test engines + .and(not(named("org.junit.platform.suite.engine.SuiteTestEngine"))); } @Override public String[] helperClassNames() { return new String[] { packageName + ".JUnitPlatformUtils", - packageName + ".JUnitPlatformUtils$Cucumber", - packageName + ".JUnitPlatformUtils$Spock", - packageName + ".JUnitPlatformLauncherUtils", - packageName + ".JUnitPlatformLauncherUtils$Cucumber", packageName + ".TestEventsHandlerHolder", packageName + ".TracingListener", + packageName + ".CompositeEngineListener", }; } @Override public void adviceTransformations(AdviceTransformation transformation) { transformation.applyAdvice( - named("getAdditionalTestExecutionListeners").and(takesNoArguments()), + named("execute").and(takesArgument(0, named("org.junit.platform.engine.ExecutionRequest"))), JUnit5Instrumentation.class.getName() + "$JUnit5Advice"); } @@ -59,11 +59,20 @@ public static class JUnit5Advice { @SuppressFBWarnings( value = "UC_USELESS_OBJECT", - justification = "listeners is the return value of the instrumented method") - @Advice.OnMethodExit + justification = "executionRequest is the argument of the original method") + @Advice.OnMethodEnter public static void addTracingListener( - @Advice.This LauncherConfig config, - @Advice.Return(readOnly = false) Collection<TestExecutionListener> listeners) { + @Advice.This TestEngine testEngine, + @Advice.Argument(value = 0, readOnly = false) ExecutionRequest executionRequest) { + String testEngineClassName = testEngine.getClass().getName(); + if (testEngineClassName.startsWith("io.cucumber") + || testEngineClassName.startsWith("org.spockframework")) { + // Cucumber and Spock have dedicated instrumentations. + // We can only filter out calls to their engines at runtime, + // since they do not declare their own "execute" method, + // but inherit it from their parent class + return; + } if (JUnitPlatformUtils.isTestInProgress()) { // a test case that is in progress starts a new JUnit instance. @@ -75,13 +84,15 @@ public static void addTracingListener( return; } - Collection<TestEngine> testEngines = JUnitPlatformLauncherUtils.getTestEngines(config); - final TracingListener listener = new TracingListener(testEngines); - - Collection<TestExecutionListener> modifiedListeners = new ArrayList<>(listeners); - modifiedListeners.add(listener); - - listeners = modifiedListeners; + TracingListener tracingListener = new TracingListener(testEngine); + EngineExecutionListener originalListener = executionRequest.getEngineExecutionListener(); + EngineExecutionListener compositeListener = + new CompositeEngineListener(tracingListener, originalListener); + executionRequest = + new ExecutionRequest( + executionRequest.getRootTestDescriptor(), + compositeListener, + executionRequest.getConfigurationParameters()); } // JUnit 5.3.0 and above diff --git a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnit5ItrInstrumentation.java b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnit5ItrInstrumentation.java index fbce425de01b..d6bde993a84c 100644 --- a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnit5ItrInstrumentation.java +++ b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnit5ItrInstrumentation.java @@ -1,7 +1,9 @@ package datadog.trace.instrumentation.junit5; import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.nameStartsWith; import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import com.google.auto.service.AutoService; @@ -25,7 +27,7 @@ public class JUnit5ItrInstrumentation extends Instrumenter.CiVisibility implements Instrumenter.ForTypeHierarchy { public JUnit5ItrInstrumentation() { - super("junit", "junit-5"); + super("ci-visibility", "junit-5"); } @Override @@ -41,16 +43,17 @@ public String hierarchyMarkerType() { @Override public ElementMatcher<TypeDescription> hierarchyMatcher() { return implementsInterface(named(hierarchyMarkerType())) - .and(implementsInterface(named("org.junit.platform.engine.TestDescriptor"))); + .and(implementsInterface(named("org.junit.platform.engine.TestDescriptor"))) + // Cucumber has a dedicated instrumentation + .and(not(nameStartsWith("io.cucumber"))) + // Spock has a dedicated instrumentation + .and(not(nameStartsWith("org.spockframework"))); } @Override public String[] helperClassNames() { return new String[] { - packageName + ".JUnitPlatformUtils", - packageName + ".JUnitPlatformUtils$Cucumber", - packageName + ".JUnitPlatformUtils$Spock", - packageName + ".TestEventsHandlerHolder", + packageName + ".JUnitPlatformUtils", packageName + ".TestEventsHandlerHolder", }; } @@ -62,8 +65,7 @@ public void adviceTransformations(AdviceTransformation transformation) { } /** - * !!!!!!!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!!!! Do not use or refer to {@code - * datadog.trace.instrumentation.junit5.JunitPlatformLauncherUtils} or any classes from {@code + * !!!!!!!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!!!! Do not use or refer to any classes from {@code * org.junit.platform.launcher} package in here: in some Gradle projects this package is not * available in CL where this instrumentation is injected */ @@ -86,7 +88,7 @@ public static void shouldBeSkipped( return; } - Collection<TestTag> tags = JUnitPlatformUtils.getTags(testDescriptor); + Collection<TestTag> tags = testDescriptor.getTags(); for (TestTag tag : tags) { if (InstrumentationBridge.ITR_UNSKIPPABLE_TAG.equals(tag.getName())) { return; diff --git a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnitPlatformLauncherUtils.java b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnitPlatformLauncherUtils.java deleted file mode 100644 index 4999431c84a4..000000000000 --- a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnitPlatformLauncherUtils.java +++ /dev/null @@ -1,150 +0,0 @@ -package datadog.trace.instrumentation.junit5; - -import datadog.trace.api.Pair; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.util.ArrayDeque; -import java.util.Collection; -import java.util.Deque; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.ServiceLoader; -import java.util.Set; -import javax.annotation.Nullable; -import org.junit.platform.commons.util.ClassLoaderUtils; -import org.junit.platform.engine.TestEngine; -import org.junit.platform.engine.TestSource; -import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.support.descriptor.ClassSource; -import org.junit.platform.engine.support.descriptor.MethodSource; -import org.junit.platform.launcher.TestIdentifier; -import org.junit.platform.launcher.TestPlan; -import org.junit.platform.launcher.core.LauncherConfig; - -/** - * A dedicated utility class for any logic that has to work with {@code org.junit.platform.launcher} - * package or its children. - */ -public abstract class JUnitPlatformLauncherUtils { - - private static final MethodHandle GET_UNIQUE_ID_OBJECT; - - static { - /* - * We have to support older versions of JUnit 5 that do not have certain methods that we would - * like to use. We try to get method handles in runtime, and if we fail to do it there's a - * fallback to alternative (less efficient) ways of getting the required info - */ - MethodHandles.Lookup lookup = MethodHandles.publicLookup(); - GET_UNIQUE_ID_OBJECT = accessGetUniqueIdObject(lookup); - } - - private static MethodHandle accessGetUniqueIdObject(MethodHandles.Lookup lookup) { - try { - MethodType returnsUniqueId = MethodType.methodType(UniqueId.class); - return lookup.findVirtual(TestIdentifier.class, "getUniqueIdObject", returnsUniqueId); - } catch (Exception e) { - // assuming we're dealing with an older framework version - // that does not have the methods we need; - // fallback logic will be used in corresponding utility methods - return null; - } - } - - private JUnitPlatformLauncherUtils() {} - - public static Class<?> getJavaClass(TestIdentifier testIdentifier) { - TestSource testSource = testIdentifier.getSource().orElse(null); - if (testSource instanceof ClassSource) { - ClassSource classSource = (ClassSource) testSource; - return classSource.getJavaClass(); - - } else if (testSource instanceof MethodSource) { - MethodSource methodSource = (MethodSource) testSource; - return JUnitPlatformUtils.getTestClass(methodSource); - - } else { - return null; - } - } - - public static boolean isSuite(TestIdentifier testIdentifier) { - UniqueId uniqueId = getUniqueId(testIdentifier); - List<UniqueId.Segment> segments = uniqueId.getSegments(); - UniqueId.Segment lastSegment = segments.get(segments.size() - 1); - return "class".equals(lastSegment.getType()) // "regular" JUnit test class - || "nested-class".equals(lastSegment.getType()) // nested JUnit test class - || "spec".equals(lastSegment.getType()) // Spock specification - || "feature".equals(lastSegment.getType()); // Cucumber feature - } - - public static @Nullable String getTestEngineId(final TestIdentifier testIdentifier) { - UniqueId uniqueId = getUniqueId(testIdentifier); - return JUnitPlatformUtils.getTestEngineId(uniqueId); - } - - public static UniqueId getUniqueId(TestIdentifier testIdentifier) { - if (GET_UNIQUE_ID_OBJECT != null) { - try { - return (UniqueId) GET_UNIQUE_ID_OBJECT.invokeExact(testIdentifier); - } catch (Throwable e) { - // fallback to slower mechanism below - } - } - return UniqueId.parse(testIdentifier.getUniqueId()); - } - - public static Collection<TestEngine> getTestEngines(LauncherConfig config) { - Set<TestEngine> engines = new LinkedHashSet<>(); - if (config.isTestEngineAutoRegistrationEnabled()) { - ClassLoader defaultClassLoader = ClassLoaderUtils.getDefaultClassLoader(); - ServiceLoader.load(TestEngine.class, defaultClassLoader).forEach(engines::add); - } - engines.addAll(config.getAdditionalTestEngines()); - return engines; - } - - public static final class Cucumber { - public static Pair<String, String> getFeatureAndScenarioNames( - TestPlan testPlan, TestIdentifier scenario, String fallbackFeatureName) { - String featureName = fallbackFeatureName; - - Deque<TestIdentifier> scenarioIdentifiers = new ArrayDeque<>(); - scenarioIdentifiers.push(scenario); - - try { - TestIdentifier current = scenario; - while (true) { - current = testPlan.getParent(current).orElse(null); - if (current == null) { - break; - } - - UniqueId currentId = getUniqueId(current); - if (JUnitPlatformUtils.Cucumber.isFeature(currentId)) { - featureName = current.getDisplayName(); - break; - - } else { - scenarioIdentifiers.push(current); - } - } - - } catch (Exception e) { - // ignore - } - - StringBuilder scenarioName = new StringBuilder(); - while (!scenarioIdentifiers.isEmpty()) { - TestIdentifier identifier = scenarioIdentifiers.pop(); - scenarioName.append(identifier.getDisplayName()); - if (!scenarioIdentifiers.isEmpty()) { - scenarioName.append('.'); - } - } - - return Pair.of(featureName, scenarioName.toString()); - } - } -} 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 87c9fd9d74cf..52d1cccc346b 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,47 +1,30 @@ package datadog.trace.instrumentation.junit5; -import datadog.trace.api.Pair; import datadog.trace.api.civisibility.config.SkippableTest; 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.Strings; -import java.io.InputStream; -import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Method; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Deque; import java.util.List; -import java.util.ListIterator; -import java.util.Optional; -import java.util.Properties; -import javax.annotation.Nullable; import org.junit.platform.commons.JUnitException; -import org.junit.platform.commons.util.ClassLoaderUtils; import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.TestSource; -import org.junit.platform.engine.TestTag; import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.support.descriptor.ClasspathResourceSource; +import org.junit.platform.engine.support.descriptor.ClassSource; import org.junit.platform.engine.support.descriptor.MethodSource; /** - * !!!!!!!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!!!! Do not use or refer to {@code - * datadog.trace.instrumentation.junit5.JunitPlatformLauncherUtils} or any classes from {@code + * !!!!!!!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!!!! Do not use or refer to any classes from {@code * org.junit.platform.launcher} package in here: in some Gradle projects this package is not * available in CL where this instrumentation is injected. * - * <p>Should you have to do something with those classes, do it in {@code - * datadog.trace.instrumentation.junit5.JunitPlatformLauncherUtils} + * <p>Should you have to do something with those classes, do it in a dedicated utility class */ public abstract class JUnitPlatformUtils { @@ -132,42 +115,16 @@ public static String getParameters(MethodSource methodSource, String displayName return "{\"metadata\":{\"test_name\":\"" + Strings.escapeToJson(displayName) + "\"}}"; } - public static String getTestName( - String displayName, MethodSource methodSource, String testEngineId) { - return Spock.ENGINE_ID.equals(testEngineId) ? displayName : methodSource.getMethodName(); - } - - @Nullable - public static Method getTestMethod(MethodSource methodSource, String testEngineId) { - return Spock.ENGINE_ID.equals(testEngineId) - ? Spock.getSpockTestMethod(methodSource) - : getTestMethod(methodSource); - } - public static SkippableTest toSkippableTest(TestDescriptor testDescriptor) { TestSource testSource = testDescriptor.getSource().orElse(null); if (testSource instanceof MethodSource) { MethodSource methodSource = (MethodSource) testSource; String testSuiteName = methodSource.getClassName(); String displayName = testDescriptor.getDisplayName(); - UniqueId uniqueId = testDescriptor.getUniqueId(); - String testEngineId = uniqueId.getEngineId().orElse(null); - String testName = getTestName(displayName, methodSource, testEngineId); - + String testName = methodSource.getMethodName(); String testParameters = getParameters(methodSource, displayName); - return new SkippableTest(testSuiteName, testName, testParameters, null); - } else if (testSource instanceof ClasspathResourceSource) { - ClasspathResourceSource classpathResourceSource = (ClasspathResourceSource) testSource; - String classpathResourceName = classpathResourceSource.getClasspathResourceName(); - - Pair<String, String> names = - Cucumber.getFeatureAndScenarioNames(testDescriptor, classpathResourceName); - String testSuiteName = names.getLeft(); - String testName = names.getRight(); - return new SkippableTest(testSuiteName, testName, null, null); - } else { return null; } @@ -199,236 +156,26 @@ public static boolean isTestInProgress() { return InternalSpanTypes.TEST.toString().equals(span.getSpanType()); } - public static Collection<TestTag> getTags(TestDescriptor testDescriptor) { - String testEngineId = getTestEngineId(testDescriptor); - if (Spock.ENGINE_ID.equals(testEngineId)) { - return Spock.getTags(testDescriptor); - } else { - return testDescriptor.getTags(); - } - } - - public static @Nullable String getTestEngineId(final TestDescriptor testDescriptor) { - return getTestEngineId(testDescriptor.getUniqueId()); - } - - public static @Nullable String getTestEngineId(final UniqueId uniqueId) { - List<UniqueId.Segment> segments = uniqueId.getSegments(); - ListIterator<UniqueId.Segment> iterator = segments.listIterator(segments.size()); - // Iterating from the end of the list, - // since we want the last segment with type "engine". - // In case junit-platform-suite engine is used, - // its segment will be the first, - // and the actual engine that runs the test - // will be in some later segment - while (iterator.hasPrevious()) { - UniqueId.Segment segment = iterator.previous(); - if ("engine".equals(segment.getType())) { - return segment.getValue(); - } - } - return null; - } - - public static final class Spock { - public static final String ENGINE_ID = "spock"; - - private static final Class<Annotation> SPOCK_FEATURE_METADATA; - private static final MethodHandle SPOCK_FEATURE_NAME; - private static final MethodHandle GET_SPOCK_NODE_INFO; - private static final MethodHandle GET_TEST_TAGS; - private static final MethodHandle GET_TEST_TAG_VALUE; - - static { - /* - * Spock's classes are accessed via reflection and method handles - * since they are loaded by a different classloader in some envs - */ - MethodHandles.Lookup lookup = MethodHandles.publicLookup(); - ClassLoader defaultClassLoader = ClassLoaderUtils.getDefaultClassLoader(); - SPOCK_FEATURE_METADATA = accessSpockFeatureMetadata(defaultClassLoader); - SPOCK_FEATURE_NAME = accessSpockFeatureName(lookup, SPOCK_FEATURE_METADATA); - GET_SPOCK_NODE_INFO = accessGetSpockNodeInfo(lookup, defaultClassLoader); - GET_TEST_TAGS = accessGetTestTags(lookup, defaultClassLoader); - GET_TEST_TAG_VALUE = accessGetTestTagValue(lookup, defaultClassLoader); - } - - private static MethodHandle accessGetSpockNodeInfo( - MethodHandles.Lookup lookup, ClassLoader classLoader) { - try { - Class<?> spockNodeClass = classLoader.loadClass("org.spockframework.runtime.SpockNode"); - Method method = spockNodeClass.getDeclaredMethod("getNodeInfo"); - return lookup.unreflect(method); - } catch (Throwable throwable) { - return null; - } - } - - private static MethodHandle accessGetTestTags( - MethodHandles.Lookup lookup, ClassLoader classLoader) { - try { - Class<?> testTaggable = - classLoader.loadClass("org.spockframework.runtime.model.ITestTaggable"); - Method method = testTaggable.getDeclaredMethod("getTestTags"); - return lookup.unreflect(method); - } catch (Throwable throwable) { - return null; - } - } - - private static MethodHandle accessGetTestTagValue( - MethodHandles.Lookup lookup, ClassLoader classLoader) { - try { - Class<?> testTaggable = classLoader.loadClass("org.spockframework.runtime.model.TestTag"); - Method method = testTaggable.getDeclaredMethod("getValue"); - return lookup.unreflect(method); - } catch (Throwable throwable) { - return null; - } - } - - private static Class<Annotation> accessSpockFeatureMetadata(ClassLoader classLoader) { - try { - return (Class<Annotation>) - classLoader.loadClass("org.spockframework.runtime.model.FeatureMetadata"); - } catch (Exception e) { - return null; - } - } - - private static MethodHandle accessSpockFeatureName( - MethodHandles.Lookup lookup, Class<Annotation> spockFeatureMetadata) { - if (spockFeatureMetadata == null) { - return null; - } - try { - MethodType returnsString = MethodType.methodType(String.class); - return lookup.findVirtual(spockFeatureMetadata, "name", returnsString); - } catch (Exception e) { - return null; - } - } - - public static Method getSpockTestMethod(MethodSource methodSource) { - String methodName = methodSource.getMethodName(); - if (methodName == null) { - return null; - } - - Class<?> testClass = getTestClass(methodSource); - if (testClass == null) { - return null; - } - - if (SPOCK_FEATURE_METADATA == null || SPOCK_FEATURE_NAME == null) { - return null; - } - - try { - for (Method declaredMethod : testClass.getDeclaredMethods()) { - Annotation featureMetadata = declaredMethod.getAnnotation(SPOCK_FEATURE_METADATA); - if (featureMetadata == null) { - continue; - } - - String annotatedName = (String) SPOCK_FEATURE_NAME.invoke(featureMetadata); - if (methodName.equals(annotatedName)) { - return declaredMethod; - } - } + public static Class<?> getJavaClass(TestDescriptor testDescriptor) { + TestSource testSource = testDescriptor.getSource().orElse(null); + if (testSource instanceof ClassSource) { + ClassSource classSource = (ClassSource) testSource; + return classSource.getJavaClass(); - } catch (Throwable e) { - // ignore - } + } else if (testSource instanceof MethodSource) { + MethodSource methodSource = (MethodSource) testSource; + return getTestClass(methodSource); + } else { return null; } - - public static Collection<TestTag> getTags(TestDescriptor testDescriptor) { - if (GET_SPOCK_NODE_INFO == null) { - return Collections.emptyList(); - } - Collection<TestTag> junitPlatformTestTags = new ArrayList<>(); - try { - Object nodeInfo = GET_SPOCK_NODE_INFO.invoke(testDescriptor); - Collection<?> testTags = (Collection<?>) GET_TEST_TAGS.invoke(nodeInfo); - for (Object testTag : testTags) { - String tagValue = (String) GET_TEST_TAG_VALUE.invoke(testTag); - TestTag junitPlatformTestTag = TestTag.create(tagValue); - junitPlatformTestTags.add(junitPlatformTestTag); - } - - } catch (Throwable throwable) { - // ignore - } - return junitPlatformTestTags; - } } - public static final class Cucumber { - public static final String ENGINE_ID = "cucumber"; - - public static @Nullable String getCucumberVersion(TestEngine cucumberEngine) { - try (InputStream cucumberPropsStream = - cucumberEngine - .getClass() - .getClassLoader() - .getResourceAsStream( - "META-INF/maven/io.cucumber/cucumber-junit-platform-engine/pom.properties")) { - Properties cucumberProps = new Properties(); - cucumberProps.load(cucumberPropsStream); - String version = cucumberProps.getProperty("version"); - if (version != null) { - return version; - } - } catch (Exception e) { - // fallback below - } - // might return "DEVELOPMENT" even for releases - return cucumberEngine.getVersion().orElse(null); - } - - public static Pair<String, String> getFeatureAndScenarioNames( - TestDescriptor testDescriptor, String fallbackFeatureName) { - String featureName = fallbackFeatureName; - - Deque<TestDescriptor> scenarioDescriptors = new ArrayDeque<>(); - scenarioDescriptors.push(testDescriptor); - - TestDescriptor current = testDescriptor; - while (true) { - Optional<TestDescriptor> parent = current.getParent(); - if (!parent.isPresent()) { - break; - } - - current = parent.get(); - UniqueId currentId = current.getUniqueId(); - if (isFeature(currentId)) { - featureName = current.getDisplayName(); - break; - - } else { - scenarioDescriptors.push(current); - } - } - - StringBuilder scenarioName = new StringBuilder(); - while (!scenarioDescriptors.isEmpty()) { - TestDescriptor descriptor = scenarioDescriptors.pop(); - scenarioName.append(descriptor.getDisplayName()); - if (!scenarioDescriptors.isEmpty()) { - scenarioName.append('.'); - } - } - - return Pair.of(featureName, scenarioName.toString()); - } - - public static boolean isFeature(UniqueId uniqueId) { - List<UniqueId.Segment> segments = uniqueId.getSegments(); - UniqueId.Segment lastSegment = segments.listIterator(segments.size()).previous(); - return "feature".equals(lastSegment.getType()); - } + public static boolean isSuite(TestDescriptor testDescriptor) { + UniqueId uniqueId = testDescriptor.getUniqueId(); + List<UniqueId.Segment> segments = uniqueId.getSegments(); + UniqueId.Segment lastSegment = segments.get(segments.size() - 1); + return "class".equals(lastSegment.getType()) // "regular" JUnit test class + || "nested-class".equals(lastSegment.getType()); // nested JUnit test class } } diff --git a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TracingListener.java b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TracingListener.java index 78bc4e7917b1..9b04a54a8a52 100644 --- a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TracingListener.java +++ b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TracingListener.java @@ -1,100 +1,82 @@ package datadog.trace.instrumentation.junit5; -import datadog.trace.api.Pair; import java.lang.reflect.Method; -import java.util.Collection; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; -import javax.annotation.Nullable; +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.TestSource; import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.engine.support.descriptor.ClassSource; -import org.junit.platform.engine.support.descriptor.ClasspathResourceSource; import org.junit.platform.engine.support.descriptor.MethodSource; -import org.junit.platform.launcher.TestExecutionListener; -import org.junit.platform.launcher.TestIdentifier; -import org.junit.platform.launcher.TestPlan; -public class TracingListener implements TestExecutionListener { +public class TracingListener implements EngineExecutionListener { - private static final String JUNIT_VINTAGE_ENGINE = "junit-vintage"; + private final String testFramework; + private final String testFrameworkVersion; - private final Map<String, String> versionByTestEngineId = new HashMap<>(); - - private volatile TestPlan testPlan; - - public TracingListener(Collection<TestEngine> testEngines) { - for (TestEngine testEngine : testEngines) { - String engineId = testEngine.getId(); - String engineVersion = - !JUnitPlatformUtils.Cucumber.ENGINE_ID.equals(engineId) - ? testEngine.getVersion().orElse(null) - : JUnitPlatformUtils.Cucumber.getCucumberVersion(testEngine); - versionByTestEngineId.put(engineId, engineVersion); - } + public TracingListener(TestEngine testEngine) { + String engineId = testEngine.getId(); + testFramework = engineId != null && engineId.startsWith("junit") ? "junit5" : engineId; + testFrameworkVersion = testEngine.getVersion().orElse(null); } @Override - public void testPlanExecutionStarted(final TestPlan testPlan) { - this.testPlan = testPlan; + public void dynamicTestRegistered(TestDescriptor testDescriptor) { + // no op } @Override - public void testPlanExecutionFinished(final TestPlan testPlan) { + public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) { // no op } @Override - public void executionStarted(final TestIdentifier testIdentifier) { - if (testIdentifier.isContainer()) { - containerExecutionStarted(testIdentifier); - } else if (testIdentifier.isTest()) { - testCaseExecutionStarted(testIdentifier); + public void executionStarted(final TestDescriptor testDescriptor) { + if (testDescriptor.isContainer()) { + containerExecutionStarted(testDescriptor); + } else if (testDescriptor.isTest()) { + testCaseExecutionStarted(testDescriptor); } } @Override public void executionFinished( - final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) { - if (testIdentifier.isContainer()) { - containerExecutionFinished(testIdentifier, testExecutionResult); - } else if (testIdentifier.isTest()) { - testCaseExecutionFinished(testIdentifier, testExecutionResult); + TestDescriptor testDescriptor, TestExecutionResult testExecutionResult) { + if (testDescriptor.isContainer()) { + containerExecutionFinished(testDescriptor, testExecutionResult); + } else if (testDescriptor.isTest()) { + testCaseExecutionFinished(testDescriptor, testExecutionResult); } } - private void containerExecutionStarted(final TestIdentifier testIdentifier) { - if (!JUnitPlatformLauncherUtils.isSuite(testIdentifier)) { + private void containerExecutionStarted(final TestDescriptor testDescriptor) { + if (!JUnitPlatformUtils.isSuite(testDescriptor)) { return; } - Class<?> testClass = JUnitPlatformLauncherUtils.getJavaClass(testIdentifier); + Class<?> testClass = JUnitPlatformUtils.getJavaClass(testDescriptor); String testSuiteName = - testClass != null ? testClass.getName() : testIdentifier.getLegacyReportingName(); - - String testEngineId = JUnitPlatformLauncherUtils.getTestEngineId(testIdentifier); - String testFramework = getTestFramework(testEngineId); - String testFrameworkVersion = versionByTestEngineId.get(testEngineId); + testClass != null ? testClass.getName() : testDescriptor.getLegacyReportingName(); List<String> tags = - testIdentifier.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); + testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteStart( testSuiteName, testFramework, testFrameworkVersion, testClass, tags, false); } private void containerExecutionFinished( - final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) { - if (!JUnitPlatformLauncherUtils.isSuite(testIdentifier)) { + final TestDescriptor testDescriptor, final TestExecutionResult testExecutionResult) { + if (!JUnitPlatformUtils.isSuite(testDescriptor)) { return; } - Class<?> testClass = JUnitPlatformLauncherUtils.getJavaClass(testIdentifier); + Class<?> testClass = JUnitPlatformUtils.getJavaClass(testDescriptor); String testSuiteName = - testClass != null ? testClass.getName() : testIdentifier.getLegacyReportingName(); + testClass != null ? testClass.getName() : testDescriptor.getLegacyReportingName(); Throwable throwable = testExecutionResult.getThrowable().orElse(null); if (throwable != null) { @@ -104,7 +86,7 @@ private void containerExecutionFinished( TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteSkip( testSuiteName, testClass, reason); - for (TestIdentifier child : testPlan.getChildren(testIdentifier)) { + for (TestDescriptor child : testDescriptor.getChildren()) { executionSkipped(child, reason); } @@ -117,36 +99,24 @@ private void containerExecutionFinished( TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteFinish(testSuiteName, testClass); } - private void testCaseExecutionStarted(final TestIdentifier testIdentifier) { - TestSource testSource = testIdentifier.getSource().orElse(null); + private void testCaseExecutionStarted(final TestDescriptor testDescriptor) { + TestSource testSource = testDescriptor.getSource().orElse(null); if (testSource instanceof MethodSource) { - testMethodExecutionStarted(testIdentifier, (MethodSource) testSource); - - } else if (testSource instanceof ClasspathResourceSource) { - testResourceExecutionStarted(testIdentifier, (ClasspathResourceSource) testSource); + testMethodExecutionStarted(testDescriptor, (MethodSource) testSource); } } - private void testMethodExecutionStarted(TestIdentifier testIdentifier, MethodSource testSource) { - String testEngineId = JUnitPlatformLauncherUtils.getTestEngineId(testIdentifier); - if (JUNIT_VINTAGE_ENGINE.equals(testEngineId)) { - // vintage tests are traced with JUnit 4 instrumentation - return; - } - - String testFramework = getTestFramework(testEngineId); - String testFrameworkVersion = versionByTestEngineId.get(testEngineId); - + private void testMethodExecutionStarted(TestDescriptor testDescriptor, MethodSource testSource) { String testSuitName = testSource.getClassName(); - String displayName = testIdentifier.getDisplayName(); - String testName = JUnitPlatformUtils.getTestName(displayName, testSource, testEngineId); + String displayName = testDescriptor.getDisplayName(); + String testName = testSource.getMethodName(); String testParameters = JUnitPlatformUtils.getParameters(testSource, displayName); List<String> tags = - testIdentifier.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); + testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); Class<?> testClass = JUnitPlatformUtils.getTestClass(testSource); - Method testMethod = JUnitPlatformUtils.getTestMethod(testSource, testEngineId); + Method testMethod = JUnitPlatformUtils.getTestMethod(testSource); String testMethodName = testSource.getMethodName(); TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( @@ -162,62 +132,22 @@ private void testMethodExecutionStarted(TestIdentifier testIdentifier, MethodSou testMethod); } - private void testResourceExecutionStarted( - TestIdentifier testIdentifier, ClasspathResourceSource testSource) { - String testEngineId = JUnitPlatformLauncherUtils.getTestEngineId(testIdentifier); - if (!JUnitPlatformUtils.Cucumber.ENGINE_ID.equals(testEngineId)) { - return; - } - - String classpathResourceName = testSource.getClasspathResourceName(); - - Pair<String, String> names = - JUnitPlatformLauncherUtils.Cucumber.getFeatureAndScenarioNames( - testPlan, testIdentifier, classpathResourceName); - String testSuiteName = names.getLeft(); - String testName = names.getRight(); - - String testFramework = getTestFramework(testEngineId); - String testFrameworkVersion = versionByTestEngineId.get(testEngineId); - - List<String> tags = - testIdentifier.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); - - TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( - testSuiteName, - testName, - null, - testFramework, - testFrameworkVersion, - null, - tags, - null, - null, - null); - } - private void testCaseExecutionFinished( - final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) { - TestSource testSource = testIdentifier.getSource().orElse(null); + final TestDescriptor testDescriptor, final TestExecutionResult testExecutionResult) { + TestSource testSource = testDescriptor.getSource().orElse(null); if (testSource instanceof MethodSource) { - testMethodExecutionFinished(testIdentifier, testExecutionResult, (MethodSource) testSource); - - } else if (testSource instanceof ClasspathResourceSource) { - testResourceExecutionFinished( - testIdentifier, testExecutionResult, (ClasspathResourceSource) testSource); + testMethodExecutionFinished(testDescriptor, testExecutionResult, (MethodSource) testSource); } } private static void testMethodExecutionFinished( - TestIdentifier testIdentifier, + TestDescriptor testDescriptor, TestExecutionResult testExecutionResult, MethodSource testSource) { - String testEngineId = JUnitPlatformLauncherUtils.getTestEngineId(testIdentifier); - String testSuiteName = testSource.getClassName(); Class<?> testClass = JUnitPlatformUtils.getTestClass(testSource); - String displayName = testIdentifier.getDisplayName(); - String testName = JUnitPlatformUtils.getTestName(displayName, testSource, testEngineId); + String displayName = testDescriptor.getDisplayName(); + String testName = testSource.getMethodName(); String testParameters = JUnitPlatformUtils.getParameters(testSource, displayName); Throwable throwable = testExecutionResult.getThrowable().orElse(null); @@ -235,76 +165,37 @@ private static void testMethodExecutionFinished( testSuiteName, testClass, testName, null, testParameters); } - private void testResourceExecutionFinished( - TestIdentifier testIdentifier, - TestExecutionResult testExecutionResult, - ClasspathResourceSource testSource) { - String testEngineId = JUnitPlatformLauncherUtils.getTestEngineId(testIdentifier); - if (!JUnitPlatformUtils.Cucumber.ENGINE_ID.equals(testEngineId)) { - return; - } - - String classpathResourceName = testSource.getClasspathResourceName(); - - Pair<String, String> names = - JUnitPlatformLauncherUtils.Cucumber.getFeatureAndScenarioNames( - testPlan, testIdentifier, classpathResourceName); - String testSuiteName = names.getLeft(); - String testName = names.getRight(); - - Throwable throwable = testExecutionResult.getThrowable().orElse(null); - if (throwable != null) { - if (JUnitPlatformUtils.isAssumptionFailure(throwable)) { - TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSkip( - testSuiteName, null, testName, null, null, throwable.getMessage()); - } else { - TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestFailure( - testSuiteName, null, testName, null, null, throwable); - } - } - - TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestFinish( - testSuiteName, null, testName, null, null); - } - @Override - public void executionSkipped(final TestIdentifier testIdentifier, final String reason) { - TestSource testSource = testIdentifier.getSource().orElse(null); + public void executionSkipped(final TestDescriptor testDescriptor, final String reason) { + TestSource testSource = testDescriptor.getSource().orElse(null); if (testSource instanceof ClassSource) { // The annotation @Disabled is kept at type level. - containerExecutionSkipped(testIdentifier, reason); + containerExecutionSkipped(testDescriptor, reason); } else if (testSource instanceof MethodSource) { // The annotation @Disabled is kept at method level. - testMethodExecutionSkipped(testIdentifier, (MethodSource) testSource, reason); - - } else if (testSource instanceof ClasspathResourceSource) { - testResourceExecutionSkipped(testIdentifier, (ClasspathResourceSource) testSource, reason); + testMethodExecutionSkipped(testDescriptor, (MethodSource) testSource, reason); } } - private void containerExecutionSkipped(final TestIdentifier testIdentifier, final String reason) { - if (!JUnitPlatformLauncherUtils.isSuite(testIdentifier)) { + private void containerExecutionSkipped(final TestDescriptor testDescriptor, final String reason) { + if (!JUnitPlatformUtils.isSuite(testDescriptor)) { return; } - Class<?> testClass = JUnitPlatformLauncherUtils.getJavaClass(testIdentifier); + Class<?> testClass = JUnitPlatformUtils.getJavaClass(testDescriptor); String testSuiteName = - testClass != null ? testClass.getName() : testIdentifier.getLegacyReportingName(); - - String testEngineId = JUnitPlatformLauncherUtils.getTestEngineId(testIdentifier); - String testFramework = getTestFramework(testEngineId); - String testFrameworkVersion = versionByTestEngineId.get(testEngineId); + testClass != null ? testClass.getName() : testDescriptor.getLegacyReportingName(); List<String> tags = - testIdentifier.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); + testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteStart( testSuiteName, testFramework, testFrameworkVersion, testClass, tags, false); TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteSkip(testSuiteName, testClass, reason); - for (TestIdentifier child : testPlan.getChildren(testIdentifier)) { + for (TestDescriptor child : testDescriptor.getChildren()) { executionSkipped(child, reason); } @@ -312,21 +203,17 @@ private void containerExecutionSkipped(final TestIdentifier testIdentifier, fina } private void testMethodExecutionSkipped( - final TestIdentifier testIdentifier, final MethodSource methodSource, final String reason) { - String testEngineId = JUnitPlatformLauncherUtils.getTestEngineId(testIdentifier); - String testFramework = getTestFramework(testEngineId); - String testFrameworkVersion = versionByTestEngineId.get(testEngineId); - + final TestDescriptor testDescriptor, final MethodSource methodSource, final String reason) { String testSuiteName = methodSource.getClassName(); - String displayName = testIdentifier.getDisplayName(); - String testName = JUnitPlatformUtils.getTestName(displayName, methodSource, testEngineId); + String displayName = testDescriptor.getDisplayName(); + String testName = methodSource.getMethodName(); String testParameters = JUnitPlatformUtils.getParameters(methodSource, displayName); List<String> tags = - testIdentifier.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); + testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); Class<?> testClass = JUnitPlatformUtils.getTestClass(methodSource); - Method testMethod = JUnitPlatformUtils.getTestMethod(methodSource, testEngineId); + Method testMethod = JUnitPlatformUtils.getTestMethod(methodSource); String testMethodName = methodSource.getMethodName(); TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestIgnore( @@ -342,42 +229,4 @@ private void testMethodExecutionSkipped( testMethod, reason); } - - private void testResourceExecutionSkipped( - TestIdentifier testIdentifier, ClasspathResourceSource testSource, String reason) { - String testEngineId = JUnitPlatformLauncherUtils.getTestEngineId(testIdentifier); - if (!JUnitPlatformUtils.Cucumber.ENGINE_ID.equals(testEngineId)) { - return; - } - - String classpathResourceName = testSource.getClasspathResourceName(); - Pair<String, String> names = - JUnitPlatformLauncherUtils.Cucumber.getFeatureAndScenarioNames( - testPlan, testIdentifier, classpathResourceName); - String testSuiteName = names.getLeft(); - String testName = names.getRight(); - - String testFramework = getTestFramework(testEngineId); - String testFrameworkVersion = versionByTestEngineId.get(testEngineId); - - List<String> tags = - testIdentifier.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); - - TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestIgnore( - testSuiteName, - testName, - null, - testFramework, - testFrameworkVersion, - null, - tags, - null, - null, - null, - reason); - } - - private static @Nullable String getTestFramework(String testEngineId) { - return testEngineId != null && testEngineId.startsWith("junit") ? "junit5" : testEngineId; - } } From 3f1ccd4bf398f69371f0362d8d28c99728eeb092 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel <jean-philippe.bempel@datadoghq.com> Date: Mon, 16 Oct 2023 22:52:51 +0200 Subject: [PATCH 24/29] make REDACTED_VALUE unique --- .../bootstrap/debugger/util/Redaction.java | 4 +++- .../el/expressions/GetMemberExpression.java | 17 +++++++++-------- .../el/expressions/ValueRefExpression.java | 17 +++++++++-------- .../debugger/agent/CapturedSnapshotTest.java | 10 +++++----- .../agent/LogProbesInstrumentationTest.java | 2 +- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java index b549938352cb..3e47fd84b672 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java @@ -5,7 +5,9 @@ import java.util.Set; public class Redaction { - public static final String REDACTED_VALUE = "REDACTED"; + // Need to be a unique instance (new String) for reference equality (==) and + // avoid internalization (intern) by the JVM because it's a string constant + public static final String REDACTED_VALUE = new String("REDACTED"); /* * based on sentry list: https://github.com/getsentry/sentry-python/blob/fefb454287b771ac31db4e30fa459d9be2f977b8/sentry_sdk/scrubber.py#L17-L58 diff --git a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/GetMemberExpression.java b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/GetMemberExpression.java index 1c6c3eab0bac..a38e4a52d7e3 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/GetMemberExpression.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/GetMemberExpression.java @@ -24,17 +24,18 @@ public Value<?> evaluate(ValueReferenceResolver valueRefResolver) { if (targetValue == Value.undefined()) { return targetValue; } + Object member; try { - Object member = valueRefResolver.getMember(targetValue.getValue(), memberName); - if (member == Redaction.REDACTED_VALUE) { - String expr = PrettyPrintVisitor.print(this); - throw new EvaluationException( - "Could not evaluate the expression because '" + expr + "' was redacted", expr); - } - return Value.of(member); + member = valueRefResolver.getMember(targetValue.getValue(), memberName); } catch (RuntimeException ex) { - throw new EvaluationException(ex.getMessage(), memberName, ex); + throw new EvaluationException(ex.getMessage(), PrettyPrintVisitor.print(this), ex); } + if (member == Redaction.REDACTED_VALUE) { + String expr = PrettyPrintVisitor.print(this); + throw new EvaluationException( + "Could not evaluate the expression because '" + expr + "' was redacted", expr); + } + return Value.of(member); } @Generated diff --git a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ValueRefExpression.java b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ValueRefExpression.java index f1f53c212056..752e024a6c9f 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ValueRefExpression.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ValueRefExpression.java @@ -19,17 +19,18 @@ public ValueRefExpression(String symbolName) { @Override public Value<?> evaluate(ValueReferenceResolver valueRefResolver) { + Object symbol; try { - Object symbol = valueRefResolver.lookup(symbolName); - if (symbol == Redaction.REDACTED_VALUE) { - String expr = PrettyPrintVisitor.print(this); - throw new EvaluationException( - "Could not evaluate the expression because '" + expr + "' was redacted", expr); - } - return Value.of(symbol); + symbol = valueRefResolver.lookup(symbolName); } catch (RuntimeException ex) { - throw new EvaluationException(ex.getMessage(), symbolName); + throw new EvaluationException(ex.getMessage(), PrettyPrintVisitor.print(this)); } + if (symbol == Redaction.REDACTED_VALUE) { + String expr = PrettyPrintVisitor.print(this); + throw new EvaluationException( + "Could not evaluate the expression because '" + expr + "' was redacted", expr); + } + return Value.of(symbol); } @Generated diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java index 7a58caf1f987..7b2c9a1a2672 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java @@ -926,7 +926,7 @@ public void nullCondition() throws IOException, URISyntaxException { Assertions.assertEquals(1, listener.snapshots.size()); List<EvaluationError> evaluationErrors = listener.snapshots.get(0).getEvaluationErrors(); Assertions.assertEquals(1, evaluationErrors.size()); - Assertions.assertEquals("fld", evaluationErrors.get(0).getExpr()); + Assertions.assertEquals("nullTyped.fld.fld", evaluationErrors.get(0).getExpr()); Assertions.assertEquals( "Cannot dereference to field: fld", evaluationErrors.get(0).getMessage()); } @@ -1005,7 +1005,7 @@ public void mergedProbesConditionMainErrorAdditionalFalse() List<Snapshot> snapshots = doMergedProbeConditions(condition1, condition2, 1); List<EvaluationError> evaluationErrors = snapshots.get(0).getEvaluationErrors(); Assertions.assertEquals(1, evaluationErrors.size()); - Assertions.assertEquals("fld", evaluationErrors.get(0).getExpr()); + Assertions.assertEquals("nullTyped.fld.fld", evaluationErrors.get(0).getExpr()); Assertions.assertEquals( "Cannot dereference to field: fld", evaluationErrors.get(0).getMessage()); } @@ -1025,7 +1025,7 @@ public void mergedProbesConditionMainErrorAdditionalTrue() List<Snapshot> snapshots = doMergedProbeConditions(condition1, condition2, 2); List<EvaluationError> evaluationErrors = snapshots.get(0).getEvaluationErrors(); Assertions.assertEquals(1, evaluationErrors.size()); - Assertions.assertEquals("fld", evaluationErrors.get(0).getExpr()); + Assertions.assertEquals("nullTyped.fld.fld", evaluationErrors.get(0).getExpr()); Assertions.assertEquals( "Cannot dereference to field: fld", evaluationErrors.get(0).getMessage()); assertNull(snapshots.get(1).getEvaluationErrors()); @@ -1046,7 +1046,7 @@ public void mergedProbesConditionMainFalseAdditionalError() List<Snapshot> snapshots = doMergedProbeConditions(condition1, condition2, 1); List<EvaluationError> evaluationErrors = snapshots.get(0).getEvaluationErrors(); Assertions.assertEquals(1, evaluationErrors.size()); - Assertions.assertEquals("fld", evaluationErrors.get(0).getExpr()); + Assertions.assertEquals("nullTyped.fld.fld", evaluationErrors.get(0).getExpr()); Assertions.assertEquals( "Cannot dereference to field: fld", evaluationErrors.get(0).getMessage()); } @@ -1067,7 +1067,7 @@ public void mergedProbesConditionMainTrueAdditionalError() assertNull(snapshots.get(0).getEvaluationErrors()); List<EvaluationError> evaluationErrors = snapshots.get(1).getEvaluationErrors(); Assertions.assertEquals(1, evaluationErrors.size()); - Assertions.assertEquals("fld", evaluationErrors.get(0).getExpr()); + Assertions.assertEquals("nullTyped.fld.fld", evaluationErrors.get(0).getExpr()); Assertions.assertEquals( "Cannot dereference to field: fld", evaluationErrors.get(0).getMessage()); } diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/LogProbesInstrumentationTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/LogProbesInstrumentationTest.java index 382d3d295e55..8b5172d6579b 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/LogProbesInstrumentationTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/LogProbesInstrumentationTest.java @@ -283,7 +283,7 @@ public void lineTemplateNullFieldLog() throws IOException, URISyntaxException { "this is log line with field={Cannot dereference to field: intValue}", snapshot.getMessage()); assertEquals(1, snapshot.getEvaluationErrors().size()); - assertEquals("intValue", snapshot.getEvaluationErrors().get(0).getExpr()); + assertEquals("nullObject.intValue", snapshot.getEvaluationErrors().get(0).getExpr()); assertEquals( "Cannot dereference to field: intValue", snapshot.getEvaluationErrors().get(0).getMessage()); From b21f9edd6a525987ef6d1a87c9d9ff11eff46861 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel <jean-philippe.bempel@datadoghq.com> Date: Tue, 17 Oct 2023 07:39:14 +0200 Subject: [PATCH 25/29] fix spotbugs --- .../java/datadog/trace/bootstrap/debugger/util/Redaction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java index 3e47fd84b672..76197d134afb 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java @@ -7,7 +7,7 @@ public class Redaction { // Need to be a unique instance (new String) for reference equality (==) and // avoid internalization (intern) by the JVM because it's a string constant - public static final String REDACTED_VALUE = new String("REDACTED"); + public static final String REDACTED_VALUE = new String("REDACTED".toCharArray()); /* * based on sentry list: https://github.com/getsentry/sentry-python/blob/fefb454287b771ac31db4e30fa459d9be2f977b8/sentry_sdk/scrubber.py#L17-L58 From 20e4084f7a0bc357fc3a8ae87b18034f1b52fbdb Mon Sep 17 00:00:00 2001 From: jean-philippe bempel <jean-philippe.bempel@datadoghq.com> Date: Mon, 16 Oct 2023 14:20:53 +0200 Subject: [PATCH 26/29] Add config fo custom redacted identifiers Add property/env `dynamic.instrumentation.redacted.identifiers` to customize the redaction data based fields or key names --- .../bootstrap/debugger/util/Redaction.java | 74 ++++++++++++------- .../debugger/util/RedactionTest.java | 16 +++- .../datadog/debugger/agent/DebuggerAgent.java | 12 +-- .../trace/api/config/DebuggerConfig.java | 2 + .../main/java/datadog/trace/api/Config.java | 7 ++ 5 files changed, 72 insertions(+), 39 deletions(-) diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java index 76197d134afb..f8a80037f48c 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/Redaction.java @@ -1,8 +1,10 @@ package datadog.trace.bootstrap.debugger.util; +import datadog.trace.api.Config; import java.util.Arrays; -import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; public class Redaction { // Need to be a unique instance (new String) for reference equality (==) and @@ -12,33 +14,49 @@ public class Redaction { /* * based on sentry list: https://github.com/getsentry/sentry-python/blob/fefb454287b771ac31db4e30fa459d9be2f977b8/sentry_sdk/scrubber.py#L17-L58 */ - private static final Set<String> KEYWORDS = - new HashSet<>( - Arrays.asList( - "password", - "passwd", - "secret", - "apikey", - "auth", - "credentials", - "mysqlpwd", - "privatekey", - "token", - "ipaddress", - "session", - // django - "csrftoken", - "sessionid", - // wsgi - "remoteaddr", - "xcsrftoken", - "xforwardedfor", - "setcookie", - "cookie", - "authorization", - "xapikey", - "xforwardedfor", - "xrealip")); + private static final Set<String> KEYWORDS = ConcurrentHashMap.newKeySet(); + + static { + KEYWORDS.addAll( + Arrays.asList( + "password", + "passwd", + "secret", + "apikey", + "auth", + "credentials", + "mysqlpwd", + "privatekey", + "token", + "ipaddress", + "session", + // django + "csrftoken", + "sessionid", + // wsgi + "remoteaddr", + "xcsrftoken", + "xforwardedfor", + "setcookie", + "cookie", + "authorization", + "xapikey", + "xforwardedfor", + "xrealip")); + } + + private static final Pattern COMMA_PATTERN = Pattern.compile(","); + + public static void addUserDefinedKeywords(Config config) { + String redactedIdentifiers = config.getDebuggerRedactedIdentifiers(); + if (redactedIdentifiers == null) { + return; + } + String[] identifiers = COMMA_PATTERN.split(redactedIdentifiers); + for (String identifier : identifiers) { + KEYWORDS.add(normalize(identifier)); + } + } public static boolean isRedactedKeyword(String name) { if (name == null) { diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/test/java/datadog/trace/bootstrap/debugger/util/RedactionTest.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/test/java/datadog/trace/bootstrap/debugger/util/RedactionTest.java index 36a3c5a818b0..349cebf93720 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/test/java/datadog/trace/bootstrap/debugger/util/RedactionTest.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/test/java/datadog/trace/bootstrap/debugger/util/RedactionTest.java @@ -2,12 +2,13 @@ import static org.junit.jupiter.api.Assertions.*; +import datadog.trace.api.Config; import org.junit.jupiter.api.Test; class RedactionTest { @Test - public void test() { + public void basic() { assertFalse(Redaction.isRedactedKeyword(null)); assertFalse(Redaction.isRedactedKeyword("")); assertFalse(Redaction.isRedactedKeyword("foobar")); @@ -19,4 +20,17 @@ public void test() { assertTrue(Redaction.isRedactedKeyword("$pass_worD")); assertTrue(Redaction.isRedactedKeyword("@passWord@")); } + + @Test + public void userDefinedKeywords() { + final String REDACTED_IDENTIFIERS = "dd.dynamic.instrumentation.redacted.identifiers"; + System.setProperty(REDACTED_IDENTIFIERS, "_MotDePasse,$Passwort"); + try { + Redaction.addUserDefinedKeywords(Config.get()); + assertTrue(Redaction.isRedactedKeyword("mot-de-passe")); + assertTrue(Redaction.isRedactedKeyword("Passwort")); + } finally { + System.clearProperty(REDACTED_IDENTIFIERS); + } + } } diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java index cc0c8f18f9a6..a4a876936147 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java @@ -12,6 +12,7 @@ import datadog.remoteconfig.SizeCheckedInputStream; import datadog.trace.api.Config; import datadog.trace.bootstrap.debugger.DebuggerContext; +import datadog.trace.bootstrap.debugger.util.Redaction; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; @@ -21,7 +22,6 @@ import java.lang.ref.WeakReference; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,14 +43,10 @@ public static synchronized void run( log.info("Starting Dynamic Instrumentation"); ClassesToRetransformFinder classesToRetransformFinder = new ClassesToRetransformFinder(); setupSourceFileTracking(instrumentation, classesToRetransformFinder); - String finalDebuggerSnapshotUrl = config.getFinalDebuggerSnapshotUrl(); - String agentUrl = config.getAgentUrl(); - boolean isSnapshotUploadThroughAgent = Objects.equals(finalDebuggerSnapshotUrl, agentUrl); - + Redaction.addUserDefinedKeywords(config); DDAgentFeaturesDiscovery ddAgentFeaturesDiscovery = sco.featuresDiscovery(config); ddAgentFeaturesDiscovery.discoverIfOutdated(); agentVersion = ddAgentFeaturesDiscovery.getVersion(); - DebuggerSink debuggerSink = new DebuggerSink(config); debuggerSink.start(); ConfigurationUpdater configurationUpdater = @@ -71,19 +67,15 @@ public static synchronized void run( setupInstrumentTheWorldTransformer( config, instrumentation, debuggerSink, statsdMetricForwarder); } - String probeFileLocation = config.getDebuggerProbeFileLocation(); - if (probeFileLocation != null) { Path probeFilePath = Paths.get(probeFileLocation); loadFromFile(probeFilePath, configurationUpdater, config.getDebuggerMaxPayloadSize()); return; } - configurationPoller = sco.configurationPoller(config); if (configurationPoller != null) { subscribeConfigurationPoller(config, configurationUpdater); - try { /* Note: shutdown hooks are tricky because JVM holds reference for them forever preventing diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/DebuggerConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/DebuggerConfig.java index 0cbfd4639139..8fa102e7e781 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/DebuggerConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/DebuggerConfig.java @@ -22,6 +22,8 @@ public final class DebuggerConfig { "dynamic.instrumentation.instrument.the.world"; public static final String DEBUGGER_EXCLUDE_FILES = "dynamic.instrumentation.exclude.files"; public static final String DEBUGGER_CAPTURE_TIMEOUT = "dynamic.instrumentation.capture.timeout"; + public static final String DEBUGGER_REDACTED_IDENTIFIERS = + "dynamic.instrumentation.redacted.identifiers"; private DebuggerConfig() {} } diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 6c01f35faca6..e24a41ad5ac3 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -179,6 +179,7 @@ import static datadog.trace.api.config.DebuggerConfig.DEBUGGER_METRICS_ENABLED; import static datadog.trace.api.config.DebuggerConfig.DEBUGGER_POLL_INTERVAL; import static datadog.trace.api.config.DebuggerConfig.DEBUGGER_PROBE_FILE_LOCATION; +import static datadog.trace.api.config.DebuggerConfig.DEBUGGER_REDACTED_IDENTIFIERS; import static datadog.trace.api.config.DebuggerConfig.DEBUGGER_UPLOAD_BATCH_SIZE; import static datadog.trace.api.config.DebuggerConfig.DEBUGGER_UPLOAD_FLUSH_INTERVAL; import static datadog.trace.api.config.DebuggerConfig.DEBUGGER_UPLOAD_TIMEOUT; @@ -726,6 +727,7 @@ static class HostNameHolder { private final boolean debuggerInstrumentTheWorld; private final String debuggerExcludeFiles; private final int debuggerCaptureTimeout; + private final String debuggerRedactedIdentifiers; private final boolean awsPropagationEnabled; private final boolean sqsPropagationEnabled; @@ -1665,6 +1667,7 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins debuggerExcludeFiles = configProvider.getString(DEBUGGER_EXCLUDE_FILES); debuggerCaptureTimeout = configProvider.getInteger(DEBUGGER_CAPTURE_TIMEOUT, DEFAULT_DEBUGGER_CAPTURE_TIMEOUT); + debuggerRedactedIdentifiers = configProvider.getString(DEBUGGER_REDACTED_IDENTIFIERS, null); awsPropagationEnabled = isPropagationEnabled(true, "aws", "aws-sdk"); sqsPropagationEnabled = isPropagationEnabled(true, "sqs"); @@ -2761,6 +2764,10 @@ public String getDebuggerProbeFileLocation() { return debuggerProbeFileLocation; } + public String getDebuggerRedactedIdentifiers() { + return debuggerRedactedIdentifiers; + } + public boolean isAwsPropagationEnabled() { return awsPropagationEnabled; } From 8f830dd22694797e109bcde0ec82ffe82832c2bb Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko <121111529+nikita-tkachenko-datadog@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:35:06 +0200 Subject: [PATCH 27/29] Add support for Karate testing framework (#6041) --- .../events/TestEventsHandlerImpl.java | 3 +- .../civisibility/CiVisibilityTest.groovy | 6 +- .../junit4/JUnit4Instrumentation.java | 18 +- .../instrumentation/karate/build.gradle | 68 ++++ .../karate/KarateInstrumentation.java | 51 +++ .../karate/KarateTracingHook.java | 158 +++++++++ .../instrumentation/karate/KarateUtils.java | 65 ++++ .../karate/TestEventsHandlerHolder.java | 35 ++ .../karate/src/test/groovy/KarateTest.groovy | 308 ++++++++++++++++++ .../java/org/example/TestFailedKarate.java | 11 + .../org/example/TestParameterizedKarate.java | 11 + .../java/org/example/TestSucceedKarate.java | 11 + .../org/example/TestUnskippableKarate.java | 11 + .../test/java/org/example/test_failed.feature | 7 + .../org/example/test_parameterized.feature | 13 + .../java/org/example/test_succeed.feature | 9 + .../java/org/example/test_unskippable.feature | 10 + gradle/configure_tests.gradle | 2 +- .../events/TestEventsHandler.java | 3 +- .../datadog/trace/util/MethodHandles.java | 4 +- .../main/java/datadog/trace/util/Strings.java | 10 +- settings.gradle | 3 +- 22 files changed, 801 insertions(+), 16 deletions(-) create mode 100644 dd-java-agent/instrumentation/karate/build.gradle create mode 100644 dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateInstrumentation.java create mode 100644 dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateTracingHook.java create mode 100644 dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateUtils.java create mode 100644 dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/TestEventsHandlerHolder.java create mode 100644 dd-java-agent/instrumentation/karate/src/test/groovy/KarateTest.groovy create mode 100644 dd-java-agent/instrumentation/karate/src/test/java/org/example/TestFailedKarate.java create mode 100644 dd-java-agent/instrumentation/karate/src/test/java/org/example/TestParameterizedKarate.java create mode 100644 dd-java-agent/instrumentation/karate/src/test/java/org/example/TestSucceedKarate.java create mode 100644 dd-java-agent/instrumentation/karate/src/test/java/org/example/TestUnskippableKarate.java create mode 100644 dd-java-agent/instrumentation/karate/src/test/java/org/example/test_failed.feature create mode 100644 dd-java-agent/instrumentation/karate/src/test/java/org/example/test_parameterized.feature create mode 100644 dd-java-agent/instrumentation/karate/src/test/java/org/example/test_succeed.feature create mode 100644 dd-java-agent/instrumentation/karate/src/test/java/org/example/test_unskippable.feature 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 445998661c79..022f7d0882ed 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 @@ -14,7 +14,6 @@ import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.annotation.Nullable; @@ -258,7 +257,7 @@ public void onTestIgnore( final @Nullable String testFramework, final @Nullable String testFrameworkVersion, final @Nullable String testParameters, - final @Nullable List<String> categories, + final @Nullable Collection<String> categories, final @Nullable Class<?> testClass, final @Nullable String testMethodName, final @Nullable Method testMethod, diff --git a/dd-java-agent/agent-ci-visibility/src/testFixtures/groovy/datadog/trace/civisibility/CiVisibilityTest.groovy b/dd-java-agent/agent-ci-visibility/src/testFixtures/groovy/datadog/trace/civisibility/CiVisibilityTest.groovy index 421731b198bd..cd8188612613 100644 --- a/dd-java-agent/agent-ci-visibility/src/testFixtures/groovy/datadog/trace/civisibility/CiVisibilityTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/testFixtures/groovy/datadog/trace/civisibility/CiVisibilityTest.groovy @@ -367,7 +367,7 @@ abstract class CiVisibilityTest extends AgentTestRunner { return testSuiteId } - void testSpan(final TraceAssert trace, + long testSpan(final TraceAssert trace, final int index, final Long testSessionId, final Long testModuleId, @@ -385,7 +385,10 @@ abstract class CiVisibilityTest extends AgentTestRunner { def testFramework = expectedTestFramework() def testFrameworkVersion = expectedTestFrameworkVersion() + def testId trace.span(index) { + testId = span.getSpanId() + parent() operationName expectedOperationPrefix() + ".test" resourceName "$testSuite.$testName" @@ -446,6 +449,7 @@ abstract class CiVisibilityTest extends AgentTestRunner { defaultTags() } } + return testId } String component = component() diff --git a/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Instrumentation.java b/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Instrumentation.java index 542111ebdcf9..a604d1ab3b7e 100644 --- a/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Instrumentation.java +++ b/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Instrumentation.java @@ -12,6 +12,7 @@ import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; import org.junit.rules.RuleChain; +import org.junit.runner.Runner; import org.junit.runner.notification.RunListener; import org.junit.runner.notification.RunNotifier; @@ -33,7 +34,10 @@ public ElementMatcher<TypeDescription> hierarchyMatcher() { return extendsClass(named(hierarchyMarkerType())) // do not instrument our internal runner // that is used to run instrumentation integration tests - .and(not(extendsClass(named("datadog.trace.agent.test.SpockRunner")))); + .and(not(extendsClass(named("datadog.trace.agent.test.SpockRunner")))) + // do not instrument Karate JUnit 4 runner + // since Karate has a dedicated instrumentation + .and(not(extendsClass(named("com.intuit.karate.junit4.Karate")))); } @Override @@ -56,7 +60,17 @@ public void adviceTransformations(AdviceTransformation transformation) { public static class JUnit4Advice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void addTracingListener(@Advice.Argument(0) final RunNotifier runNotifier) { + public static void addTracingListener( + @Advice.This final Runner runner, @Advice.Argument(0) final RunNotifier runNotifier) { + String runnerClassName = runner.getClass().getName(); + if ("datadog.trace.agent.test.SpockRunner".equals(runnerClassName) + || "com.intuit.karate.junit4.Karate".equals(runnerClassName)) { + // checking class names in hierarchyMatcher alone is not enough: + // for example, Karate calls #run method of its super class, + // that was transformed + return; + } + // No public accessor to get already installed listeners. // The installed RunListeners list are obtained using reflection. final List<RunListener> runListeners = JUnit4Utils.runListenersFromRunNotifier(runNotifier); diff --git a/dd-java-agent/instrumentation/karate/build.gradle b/dd-java-agent/instrumentation/karate/build.gradle new file mode 100644 index 000000000000..a641986cac2f --- /dev/null +++ b/dd-java-agent/instrumentation/karate/build.gradle @@ -0,0 +1,68 @@ +apply from: "$rootDir/gradle/java.gradle" + +muzzle { + pass { + group = 'com.intuit.karate' + module = 'karate-core' + versions = '[1.0.0,1.4.0)' + } + // Karate 1.4.0+ is compiled with Java 11 + pass { + group = 'com.intuit.karate' + module = 'karate-core' + versions = '[1.4.0,)' + javaVersion = 11 + } +} + +addTestSuiteForDir('latestDepTest', 'test') + +dependencies { + compileOnly group: 'com.intuit.karate', name: 'karate-core', version: '1.0.0' + + testImplementation testFixtures(project(':dd-java-agent:agent-ci-visibility')) + testImplementation group: 'org.junit.platform', name: 'junit-platform-launcher', version: '1.8.2' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.8.2' + + testImplementation (group: 'com.intuit.karate', name: 'karate-core', version: '1.0.0') { + // excluding logback to avoid conflicts with deps.testLogging + exclude group: 'ch.qos.logback', module: 'logback-classic' + } + testImplementation (group: 'com.intuit.karate', name: 'karate-junit5', version: '1.0.0') { + // excluding logback to avoid conflicts with deps.testLogging + exclude group: 'ch.qos.logback', module: 'logback-classic' + } + + latestDepTestImplementation (group: 'com.intuit.karate', name: 'karate-core', version: '+') { + // excluding logback to avoid conflicts with deps.testLogging + exclude group: 'ch.qos.logback', module: 'logback-classic' + } + latestDepTestImplementation (group: 'com.intuit.karate', name: 'karate-junit5', version: '+') { + // excluding logback to avoid conflicts with deps.testLogging + exclude group: 'ch.qos.logback', module: 'logback-classic' + } +} + +// Using recommended Karate project layout where Karate feature files sit in same /test/java folders as their java counterparts +sourceSets { + test { + resources { + srcDir file('src/test/java') + exclude '**/*.java' + } + } + latestDepTest { + resources { + srcDir file('src/test/java') + exclude '**/*.java' + } + } +} + +configurations.matching({ it.name.startsWith('latestDepTest') }).each({ + it.resolutionStrategy { + // Karate 1.4.0+ is compiled with Java 11 + force group: 'com.intuit.karate', name: 'karate-core', version: (JavaVersion.current().java11Compatible ? '+' : '1.3.1') + force group: 'com.intuit.karate', name: 'karate-junit5', version: (JavaVersion.current().java11Compatible ? '+' : '1.3.1') + } +}) diff --git a/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateInstrumentation.java b/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateInstrumentation.java new file mode 100644 index 000000000000..2be047bac092 --- /dev/null +++ b/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateInstrumentation.java @@ -0,0 +1,51 @@ +package datadog.trace.instrumentation.karate; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; + +import com.google.auto.service.AutoService; +import com.intuit.karate.Runner; +import com.intuit.karate.RuntimeHook; +import datadog.trace.agent.tooling.Instrumenter; +import net.bytebuddy.asm.Advice; + +@AutoService(Instrumenter.class) +public class KarateInstrumentation extends Instrumenter.CiVisibility + implements Instrumenter.ForSingleType { + + public KarateInstrumentation() { + super("ci-visibility", "karate"); + } + + @Override + public String instrumentedType() { + return "com.intuit.karate.Runner$Builder"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".TestEventsHandlerHolder", + packageName + ".KarateUtils", + packageName + ".KarateTracingHook" + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + isConstructor(), KarateInstrumentation.class.getName() + "$KarateAdvice"); + } + + public static class KarateAdvice { + @Advice.OnMethodExit + public static void onRunnerBuilderConstructorExit( + @Advice.This Runner.Builder<?> runnerBuilder) { + runnerBuilder.hook(new KarateTracingHook()); + } + + // Karate 1.0.0 and above + public static void muzzleCheck(RuntimeHook runtimeHook) { + runtimeHook.beforeSuite(null); + } + } +} diff --git a/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateTracingHook.java b/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateTracingHook.java new file mode 100644 index 000000000000..d5ceb88c9765 --- /dev/null +++ b/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateTracingHook.java @@ -0,0 +1,158 @@ +package datadog.trace.instrumentation.karate; + +import com.intuit.karate.FileUtils; +import com.intuit.karate.RuntimeHook; +import com.intuit.karate.Suite; +import com.intuit.karate.core.Feature; +import com.intuit.karate.core.FeatureResult; +import com.intuit.karate.core.FeatureRuntime; +import com.intuit.karate.core.Scenario; +import com.intuit.karate.core.ScenarioResult; +import com.intuit.karate.core.ScenarioRuntime; +import com.intuit.karate.core.Step; +import com.intuit.karate.core.StepResult; +import datadog.trace.api.Config; +import datadog.trace.api.civisibility.InstrumentationBridge; +import datadog.trace.api.civisibility.config.SkippableTest; +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.Tags; +import java.util.Collection; + +// FIXME nikita: do not trace Karate tests in JUnit 4 / JUnit 5 instrumentations +public class KarateTracingHook implements RuntimeHook { + + private static final String FRAMEWORK_NAME = "karate"; + private static final String FRAMEWORK_VERSION = FileUtils.KARATE_VERSION; + + @Override + public boolean beforeFeature(FeatureRuntime fr) { + Feature feature = KarateUtils.getFeature(fr); + Suite suite = fr.suite; + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteStart( + feature.getNameForReport(), + FRAMEWORK_NAME, + FRAMEWORK_VERSION, + null, + KarateUtils.getCategories(feature.getTags()), + suite.parallel); + return true; + } + + @Override + public void afterFeature(FeatureRuntime fr) { + String featureName = KarateUtils.getFeature(fr).getNameForReport(); + FeatureResult result = fr.result; + if (result.isFailed()) { + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteFailure( + featureName, null, result.getErrorMessagesCombined()); + } else if (result.isEmpty()) { + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteSkip(featureName, null, null); + } + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteFinish(featureName, null); + } + + @Override + public boolean beforeScenario(ScenarioRuntime sr) { + Scenario scenario = sr.scenario; + Feature feature = scenario.getFeature(); + + String featureName = feature.getNameForReport(); + String scenarioName = KarateUtils.getScenarioName(scenario); + String parameters = KarateUtils.getParameters(scenario); + Collection<String> categories = scenario.getTagsEffective().getTagKeys(); + + if (Config.get().isCiVisibilityItrEnabled() + && !categories.contains(InstrumentationBridge.ITR_UNSKIPPABLE_TAG)) { + SkippableTest skippableTest = new SkippableTest(featureName, scenarioName, parameters, null); + if (TestEventsHandlerHolder.TEST_EVENTS_HANDLER.skip(skippableTest)) { + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestIgnore( + featureName, + scenarioName, + scenario.getRefId(), + FRAMEWORK_NAME, + FRAMEWORK_VERSION, + parameters, + categories, + null, + null, + null, + InstrumentationBridge.ITR_SKIP_REASON); + return false; + } + } + + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( + featureName, + scenarioName, + scenario.getRefId(), + FRAMEWORK_NAME, + FRAMEWORK_VERSION, + parameters, + categories, + null, + null, + null); + return true; + } + + @Override + public void afterScenario(ScenarioRuntime sr) { + Scenario scenario = sr.scenario; + Feature feature = scenario.getFeature(); + + String featureName = feature.getNameForReport(); + String scenarioName = KarateUtils.getScenarioName(scenario); + + ScenarioResult result = sr.result; + if (result.isFailed()) { + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestFailure( + featureName, + null, + scenarioName, + scenario.getRefId(), + KarateUtils.getParameters(scenario), + result.getError()); + } else if (result.getStepResults().isEmpty()) { + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSkip( + featureName, + null, + scenarioName, + scenario.getRefId(), + KarateUtils.getParameters(scenario), + null); + } + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestFinish( + featureName, null, scenarioName, scenario.getRefId(), KarateUtils.getParameters(scenario)); + } + + @Override + public boolean beforeStep(Step step, ScenarioRuntime sr) { + AgentSpan span = AgentTracer.startSpan("karate", "karate.step"); + AgentScope scope = AgentTracer.activateSpan(span); + String stepName = step.getPrefix() + " " + step.getText(); + span.setResourceName(stepName); + span.setTag(Tags.COMPONENT, "karate"); + span.setTag("step.name", stepName); + span.setTag("step.startLine", step.getLine()); + span.setTag("step.endLine", step.getEndLine()); + span.setTag("step.docString", step.getDocString()); + return true; + } + + @Override + public void afterStep(StepResult result, ScenarioRuntime sr) { + AgentSpan span = AgentTracer.activeSpan(); + if (span == null) { + return; + } + + AgentScope scope = AgentTracer.activeScope(); + if (scope != null) { + scope.close(); + } + + span.finish(); + } +} 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 new file mode 100644 index 000000000000..a1590404ba13 --- /dev/null +++ b/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateUtils.java @@ -0,0 +1,65 @@ +package datadog.trace.instrumentation.karate; + +import com.intuit.karate.core.Feature; +import com.intuit.karate.core.FeatureRuntime; +import com.intuit.karate.core.Scenario; +import com.intuit.karate.core.Tag; +import datadog.trace.util.MethodHandles; +import datadog.trace.util.Strings; +import java.lang.invoke.MethodHandle; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class KarateUtils { + + private KarateUtils() {} + + private static final MethodHandles METHOD_HANDLES = + new MethodHandles(FeatureRuntime.class.getClassLoader()); + private static final MethodHandle FEATURE_RUNTIME_FEATURE_GETTER = + METHOD_HANDLES.privateFieldGetter("com.intuit.karate.core.FeatureRuntime", "feature"); + private static final MethodHandle FEATURE_RUNTIME_FEATURE_CALL_GETTER = + METHOD_HANDLES.privateFieldGetter("com.intuit.karate.core.FeatureRuntime", "featureCall"); + private static final MethodHandle FEATURE_CALL_FEATURE_GETTER = + METHOD_HANDLES.privateFieldGetter("com.intuit.karate.core.FeatureCall", "feature"); + + public static Feature getFeature(FeatureRuntime featureRuntime) { + if (FEATURE_RUNTIME_FEATURE_CALL_GETTER != null) { + Object featureCall = + METHOD_HANDLES.invoke(FEATURE_RUNTIME_FEATURE_CALL_GETTER, featureRuntime); + if (featureCall != null && FEATURE_CALL_FEATURE_GETTER != null) { + return METHOD_HANDLES.invoke(FEATURE_CALL_FEATURE_GETTER, featureCall); + } + } else if (FEATURE_RUNTIME_FEATURE_GETTER != null) { + // Karate versions prior to 1.3.0 + return METHOD_HANDLES.invoke(FEATURE_RUNTIME_FEATURE_GETTER, featureRuntime); + } + return null; + } + + public static String getScenarioName(Scenario scenario) { + String scenarioName = scenario.getName(); + if (Strings.isNotBlank(scenarioName)) { + return scenarioName; + } else { + return scenario.getRefId(); + } + } + + public static List<String> getCategories(List<Tag> tags) { + if (tags == null) { + return Collections.emptyList(); + } + + List<String> categories = new ArrayList<>(tags.size()); + for (Tag tag : tags) { + categories.add(tag.getName()); + } + return categories; + } + + public static String getParameters(Scenario scenario) { + return scenario.getExampleData() != null ? Strings.toJson(scenario.getExampleData()) : null; + } +} diff --git a/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/TestEventsHandlerHolder.java b/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/TestEventsHandlerHolder.java new file mode 100644 index 000000000000..b82381b1b0ab --- /dev/null +++ b/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/TestEventsHandlerHolder.java @@ -0,0 +1,35 @@ +package datadog.trace.instrumentation.karate; + +import datadog.trace.api.civisibility.InstrumentationBridge; +import datadog.trace.api.civisibility.events.TestEventsHandler; +import datadog.trace.util.AgentThreadFactory; +import java.nio.file.Paths; + +public abstract class TestEventsHandlerHolder { + + public static volatile TestEventsHandler TEST_EVENTS_HANDLER; + + static { + start(); + Runtime.getRuntime() + .addShutdownHook( + AgentThreadFactory.newAgentThread( + AgentThreadFactory.AgentThread.CI_TEST_EVENTS_SHUTDOWN_HOOK, + TestEventsHandlerHolder::stop, + false)); + } + + public static void start() { + TEST_EVENTS_HANDLER = + InstrumentationBridge.createTestEventsHandler("karate", Paths.get("").toAbsolutePath()); + } + + public static void stop() { + if (TEST_EVENTS_HANDLER != null) { + TEST_EVENTS_HANDLER.close(); + TEST_EVENTS_HANDLER = null; + } + } + + private TestEventsHandlerHolder() {} +} diff --git a/dd-java-agent/instrumentation/karate/src/test/groovy/KarateTest.groovy b/dd-java-agent/instrumentation/karate/src/test/groovy/KarateTest.groovy new file mode 100644 index 000000000000..9b50d3a501d0 --- /dev/null +++ b/dd-java-agent/instrumentation/karate/src/test/groovy/KarateTest.groovy @@ -0,0 +1,308 @@ +import com.intuit.karate.FileUtils +import com.intuit.karate.KarateException +import datadog.trace.agent.test.asserts.ListWriterAssert +import datadog.trace.agent.test.asserts.TraceAssert +import datadog.trace.api.DDTags +import datadog.trace.api.DisableTestTrace +import datadog.trace.api.civisibility.CIConstants +import datadog.trace.api.civisibility.config.SkippableTest +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.civisibility.CiVisibilityTest +import datadog.trace.instrumentation.karate.TestEventsHandlerHolder +import org.example.TestFailedKarate +import org.example.TestParameterizedKarate +import org.example.TestSucceedKarate +import org.example.TestUnskippableKarate +import org.junit.jupiter.api.Assumptions +import org.junit.jupiter.engine.JupiterTestEngine +import org.junit.platform.engine.DiscoverySelector +import org.junit.platform.launcher.core.LauncherConfig +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder +import org.junit.platform.launcher.core.LauncherFactory + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass + +@DisableTestTrace(reason = "avoid self-tracing") +class KarateTest extends CiVisibilityTest { + + def "test success generates spans"() { + setup: + runTests(TestSucceedKarate) + + expect: + ListWriterAssert.assertTraces(TEST_WRITER, 3, false, SORT_TRACES_BY_DESC_SIZE_THEN_BY_NAMES, { + long testSessionId + long testModuleId + long testSuiteId + trace(3, true) { + testSessionId = testSessionSpan(it, 1, CIConstants.TEST_PASS) + testModuleId = testModuleSpan(it, 0, testSessionId, CIConstants.TEST_PASS) + testSuiteId = testSuiteSpan(it, 2, testSessionId, testModuleId, "[org/example/test_succeed] test succeed", CIConstants.TEST_PASS, null, null, false, ['foo'], false) + } + trace(2, true) { + long testId = testSpan(it, 1, testSessionId, testModuleId, testSuiteId, "[org/example/test_succeed] test succeed", "first scenario", null, CIConstants.TEST_PASS, null, null, false, ['bar', 'foo'], false, false) + karateStepSpan(it, 0, testId, "* print 'first'", 6, 6) + } + trace(2, true) { + long testId = testSpan(it, 1, testSessionId, testModuleId, testSuiteId, "[org/example/test_succeed] test succeed", "second scenario", null, CIConstants.TEST_PASS, null, null, false, ['foo'], false, false) + karateStepSpan(it, 0, testId, "* print 'second'", 9, 9) + } + }) + } + + def "test parameterized generates spans"() { + setup: + runTests(TestParameterizedKarate) + + expect: + ListWriterAssert.assertTraces(TEST_WRITER, 3, false, SORT_TRACES_BY_START, { + long testSessionId + long testModuleId + long testSuiteId + trace(3, true) { + testSessionId = testSessionSpan(it, 1, CIConstants.TEST_PASS) + testModuleId = testModuleSpan(it, 0, testSessionId, CIConstants.TEST_PASS) + testSuiteId = testSuiteSpan(it, 2, testSessionId, testModuleId, "[org/example/test_parameterized] test parameterized", CIConstants.TEST_PASS, null, null, false, null, false) + } + trace(4, true) { + long testId = testSpan(it, 3, testSessionId, testModuleId, testSuiteId, "[org/example/test_parameterized] test parameterized", "first scenario as an outline", null, CIConstants.TEST_PASS, testTags_0, null, false, null, false, false) + karateStepSpan(it, 0, testId, "Given def p = 'a'", 6, 6) + karateStepSpan(it, 2, testId, "When def response = p + p", 7, 7) + karateStepSpan(it, 1, testId, "Then match response == value", 8, 8) + } + trace(4, true) { + long testId = testSpan(it, 3, testSessionId, testModuleId, testSuiteId, "[org/example/test_parameterized] test parameterized", "first scenario as an outline", null, CIConstants.TEST_PASS, testTags_1, null, false, null, false, false) + karateStepSpan(it, 0, testId, "Given def p = 'b'", 6, 6) + karateStepSpan(it, 2, testId, "When def response = p + p", 7, 7) + karateStepSpan(it, 1, testId, "Then match response == value", 8, 8) + } + }) + + where: + testTags_0 = [(Tags.TEST_PARAMETERS): '{"param":"\\\'a\\\'","value":"aa"}'] + testTags_1 = [(Tags.TEST_PARAMETERS): '{"param":"\\\'b\\\'","value":"bb"}'] + } + + def "test failed generates spans"() { + setup: + runTests(TestFailedKarate) + + expect: + def exception = new KarateException("did not evaluate to 'true': false\nclasspath:org/example/test_failed.feature:7") + ListWriterAssert.assertTraces(TEST_WRITER, 3, false, SORT_TRACES_BY_DESC_SIZE_THEN_BY_NAMES, { + long testSessionId + long testModuleId + long testSuiteId + trace(3, true) { + testSessionId = testSessionSpan(it, 1, CIConstants.TEST_FAIL) + testModuleId = testModuleSpan(it, 0, testSessionId, CIConstants.TEST_FAIL) + testSuiteId = testSuiteSpan(it, 2, testSessionId, testModuleId, "[org/example/test_failed] test failed", CIConstants.TEST_FAIL, null, exception, false, null, false) + } + trace(2, true) { + long testId = testSpan(it, 1, testSessionId, testModuleId, testSuiteId, "[org/example/test_failed] test failed", "first scenario", null, CIConstants.TEST_PASS, null, null, false, null, false, false) + karateStepSpan(it, 0, testId, "* print 'first'", 4, 4) + } + trace(2, true) { + long testId = testSpan(it, 1, testSessionId, testModuleId, testSuiteId, "[org/example/test_failed] test failed", "second scenario", null, CIConstants.TEST_FAIL, null, exception, false, null, false, false) + karateStepSpan(it, 0, testId, "* assert false", 7, 7) + } + }) + } + + def "test ITR skipping"() { + setup: + Assumptions.assumeTrue(isSkippingSupported(FileUtils.KARATE_VERSION), "Current Karate version $FileUtils.KARATE_VERSION does not support testSkipping") + + givenSkippableTests([ + new SkippableTest("[org/example/test_succeed] test succeed", "first scenario", null, null), + ]) + runTests(TestSucceedKarate) + + expect: + ListWriterAssert.assertTraces(TEST_WRITER, 3, false, SORT_TRACES_BY_START, { + long testSessionId + long testModuleId + long testSuiteId + trace(3, true) { + testSessionId = testSessionSpan(it, 1, CIConstants.TEST_PASS) + testModuleId = testModuleSpan(it, 0, testSessionId, CIConstants.TEST_PASS, [ + (DDTags.CI_ITR_TESTS_SKIPPED): true, + (Tags.TEST_ITR_TESTS_SKIPPING_ENABLED): true, + (Tags.TEST_ITR_TESTS_SKIPPING_TYPE): "test", + (Tags.TEST_ITR_TESTS_SKIPPING_COUNT): 1, + ]) + testSuiteId = testSuiteSpan(it, 2, testSessionId, testModuleId, "[org/example/test_succeed] test succeed", CIConstants.TEST_PASS, null, null, false, ['foo'], false) + } + trace(1, true) { + testSpan(it, 0, testSessionId, testModuleId, testSuiteId, "[org/example/test_succeed] test succeed", "first scenario", null, CIConstants.TEST_SKIP, testTags, null, false, ['bar', 'foo'], false, false) + } + trace(2, true) { + long testId = testSpan(it, 1, testSessionId, testModuleId, testSuiteId, "[org/example/test_succeed] test succeed", "second scenario", null, CIConstants.TEST_PASS, null, null, false, ['foo'], false, false) + karateStepSpan(it, 0, testId, "* print 'second'", 9, 9) + } + }) + + where: + testTags = [(Tags.TEST_SKIP_REASON): "Skipped by Datadog Intelligent Test Runner", (Tags.TEST_SKIPPED_BY_ITR): true ] + } + + def "test parameterized ITR skipping"() { + setup: + Assumptions.assumeTrue(isSkippingSupported(FileUtils.KARATE_VERSION), "Current Karate version $FileUtils.KARATE_VERSION does not support testSkipping") + + givenSkippableTests([ + new SkippableTest("[org/example/test_parameterized] test parameterized", "first scenario as an outline", '{"param":"\\\'a\\\'","value":"aa"}', null), + ]) + + runTests(TestParameterizedKarate) + + expect: + ListWriterAssert.assertTraces(TEST_WRITER, 3, false, SORT_TRACES_BY_START, { + long testSessionId + long testModuleId + long testSuiteId + trace(3, true) { + testSessionId = testSessionSpan(it, 1, CIConstants.TEST_PASS) + testModuleId = testModuleSpan(it, 0, testSessionId, CIConstants.TEST_PASS, [ + (DDTags.CI_ITR_TESTS_SKIPPED): true, + (Tags.TEST_ITR_TESTS_SKIPPING_ENABLED): true, + (Tags.TEST_ITR_TESTS_SKIPPING_TYPE): "test", + (Tags.TEST_ITR_TESTS_SKIPPING_COUNT): 1, + ]) + testSuiteId = testSuiteSpan(it, 2, testSessionId, testModuleId, "[org/example/test_parameterized] test parameterized", CIConstants.TEST_PASS, null, null, false, null, false) + } + trace(1, true) { + testSpan(it, 0, testSessionId, testModuleId, testSuiteId, "[org/example/test_parameterized] test parameterized", "first scenario as an outline", null, CIConstants.TEST_SKIP, testTags_0, null, false, null, false, false) + } + trace(4, true) { + long testId = testSpan(it, 3, testSessionId, testModuleId, testSuiteId, "[org/example/test_parameterized] test parameterized", "first scenario as an outline", null, CIConstants.TEST_PASS, testTags_1, null, false, null, false, false) + karateStepSpan(it, 0, testId, "Given def p = 'b'", 6, 6) + karateStepSpan(it, 2, testId, "When def response = p + p", 7, 7) + karateStepSpan(it, 1, testId, "Then match response == value", 8, 8) + } + }) + + where: + testTags_0 = [ + (Tags.TEST_PARAMETERS): '{"param":"\\\'a\\\'","value":"aa"}', + (Tags.TEST_SKIP_REASON): "Skipped by Datadog Intelligent Test Runner", + (Tags.TEST_SKIPPED_BY_ITR): true + ] + testTags_1 = [(Tags.TEST_PARAMETERS): '{"param":"\\\'b\\\'","value":"bb"}'] + } + + def "test ITR unskippable"() { + setup: + Assumptions.assumeTrue(isSkippingSupported(FileUtils.KARATE_VERSION), "Current Karate version $FileUtils.KARATE_VERSION does not support testSkipping") + + givenSkippableTests([ + new SkippableTest("[org/example/test_unskippable] test unskippable", "first scenario", null, null), + ]) + runTests(TestUnskippableKarate) + + expect: + ListWriterAssert.assertTraces(TEST_WRITER, 3, false, SORT_TRACES_BY_START, { + long testSessionId + long testModuleId + long testSuiteId + trace(3, true) { + testSessionId = testSessionSpan(it, 1, CIConstants.TEST_PASS) + testModuleId = testModuleSpan(it, 0, testSessionId, CIConstants.TEST_PASS, [ + (Tags.TEST_ITR_TESTS_SKIPPING_ENABLED): true, + (Tags.TEST_ITR_TESTS_SKIPPING_TYPE): "test", + (Tags.TEST_ITR_TESTS_SKIPPING_COUNT): 0, + ]) + testSuiteId = testSuiteSpan(it, 2, testSessionId, testModuleId, "[org/example/test_unskippable] test unskippable", CIConstants.TEST_PASS, null, null, false, ['foo'], false) + } + trace(2, true) { + long testId = testSpan(it, 1, testSessionId, testModuleId, testSuiteId, "[org/example/test_unskippable] test unskippable", "first scenario", null, CIConstants.TEST_PASS, testTags, null, false, ['bar', 'datadog_itr_unskippable', 'foo'], false, false) + karateStepSpan(it, 0, testId, "* print 'first'", 7, 7) + } + trace(2, true) { + long testId = testSpan(it, 1, testSessionId, testModuleId, testSuiteId, "[org/example/test_unskippable] test unskippable", "second scenario", null, CIConstants.TEST_PASS, null, null, false, ['foo'], false, false) + karateStepSpan(it, 0, testId, "* print 'second'", 10, 10) + } + }) + + where: + testTags = [(Tags.TEST_ITR_UNSKIPPABLE): true, (Tags.TEST_ITR_FORCED_RUN): true] + } + + private static void karateStepSpan(TraceAssert trace, + int index, + long testId, + String stepName, + int startLine, + int endLine, + String docString = null) { + trace.span(index, { + parentSpanId(BigInteger.valueOf(testId)) + operationName "karate.step" + resourceName stepName + tags { + "$Tags.COMPONENT" "karate" + "step.name" stepName + "step.startLine" startLine + "step.endLine" endLine + + if (docString != null) { + "step.docString" docString + } + + "$Tags.ENV" String + "$DDTags.LIBRARY_VERSION_TAG_KEY" String + + defaultTags() + } + }) + } + + private void runTests(Class<?>... tests) { + TestEventsHandlerHolder.start() + + DiscoverySelector[] selectors = new DiscoverySelector[tests.length] + for (i in 0..<tests.length) { + selectors[i] = selectClass(tests[i]) + } + + def launcherReq = LauncherDiscoveryRequestBuilder.request() + .selectors(selectors) + .build() + + def launcherConfig = LauncherConfig + .builder() + .enableTestEngineAutoRegistration(false) + .addTestEngines(new JupiterTestEngine()) + .build() + + def launcher = LauncherFactory.create(launcherConfig) + launcher.execute(launcherReq) + + TestEventsHandlerHolder.stop() + } + + @Override + String expectedOperationPrefix() { + return "karate" + } + + @Override + String expectedTestFramework() { + return "karate" + } + + @Override + String expectedTestFrameworkVersion() { + return FileUtils.KARATE_VERSION + } + + @Override + String component() { + return "karate" + } + + boolean isSkippingSupported(String frameworkVersion) { + // earlier Karate version contain a bug that does not allow skipping scenarios + frameworkVersion >= "1.2.0" + } +} diff --git a/dd-java-agent/instrumentation/karate/src/test/java/org/example/TestFailedKarate.java b/dd-java-agent/instrumentation/karate/src/test/java/org/example/TestFailedKarate.java new file mode 100644 index 000000000000..06ae4bb6ddbc --- /dev/null +++ b/dd-java-agent/instrumentation/karate/src/test/java/org/example/TestFailedKarate.java @@ -0,0 +1,11 @@ +package org.example; + +import com.intuit.karate.junit5.Karate; + +public class TestFailedKarate { + + @Karate.Test + public Karate testFailed() { + return Karate.run("classpath:org/example/test_failed.feature"); + } +} diff --git a/dd-java-agent/instrumentation/karate/src/test/java/org/example/TestParameterizedKarate.java b/dd-java-agent/instrumentation/karate/src/test/java/org/example/TestParameterizedKarate.java new file mode 100644 index 000000000000..7f2fb53dccc0 --- /dev/null +++ b/dd-java-agent/instrumentation/karate/src/test/java/org/example/TestParameterizedKarate.java @@ -0,0 +1,11 @@ +package org.example; + +import com.intuit.karate.junit5.Karate; + +public class TestParameterizedKarate { + + @Karate.Test + public Karate testSucceed() { + return Karate.run("classpath:org/example/test_parameterized.feature"); + } +} diff --git a/dd-java-agent/instrumentation/karate/src/test/java/org/example/TestSucceedKarate.java b/dd-java-agent/instrumentation/karate/src/test/java/org/example/TestSucceedKarate.java new file mode 100644 index 000000000000..33003ddccf47 --- /dev/null +++ b/dd-java-agent/instrumentation/karate/src/test/java/org/example/TestSucceedKarate.java @@ -0,0 +1,11 @@ +package org.example; + +import com.intuit.karate.junit5.Karate; + +public class TestSucceedKarate { + + @Karate.Test + public Karate testSucceed() { + return Karate.run("classpath:org/example/test_succeed.feature"); + } +} diff --git a/dd-java-agent/instrumentation/karate/src/test/java/org/example/TestUnskippableKarate.java b/dd-java-agent/instrumentation/karate/src/test/java/org/example/TestUnskippableKarate.java new file mode 100644 index 000000000000..9b33ccc40276 --- /dev/null +++ b/dd-java-agent/instrumentation/karate/src/test/java/org/example/TestUnskippableKarate.java @@ -0,0 +1,11 @@ +package org.example; + +import com.intuit.karate.junit5.Karate; + +public class TestUnskippableKarate { + + @Karate.Test + public Karate testSucceed() { + return Karate.run("classpath:org/example/test_unskippable.feature"); + } +} diff --git a/dd-java-agent/instrumentation/karate/src/test/java/org/example/test_failed.feature b/dd-java-agent/instrumentation/karate/src/test/java/org/example/test_failed.feature new file mode 100644 index 000000000000..534144ae715e --- /dev/null +++ b/dd-java-agent/instrumentation/karate/src/test/java/org/example/test_failed.feature @@ -0,0 +1,7 @@ +Feature: test failed + + Scenario: first scenario + * print 'first' + + Scenario: second scenario + * assert false diff --git a/dd-java-agent/instrumentation/karate/src/test/java/org/example/test_parameterized.feature b/dd-java-agent/instrumentation/karate/src/test/java/org/example/test_parameterized.feature new file mode 100644 index 000000000000..62beb7ae1988 --- /dev/null +++ b/dd-java-agent/instrumentation/karate/src/test/java/org/example/test_parameterized.feature @@ -0,0 +1,13 @@ +Feature: test parameterized + + Scenario Outline: first scenario as an outline + (to prevent a particular bug from re-appearing) + + Given def p = <param> + When def response = p + p + Then match response == value + + Examples: + | param | value | + | 'a' | aa | + | 'b' | bb | diff --git a/dd-java-agent/instrumentation/karate/src/test/java/org/example/test_succeed.feature b/dd-java-agent/instrumentation/karate/src/test/java/org/example/test_succeed.feature new file mode 100644 index 000000000000..a82428ed0c6b --- /dev/null +++ b/dd-java-agent/instrumentation/karate/src/test/java/org/example/test_succeed.feature @@ -0,0 +1,9 @@ +@foo +Feature: test succeed + + @bar + Scenario: first scenario + * print 'first' + + Scenario: second scenario + * print 'second' diff --git a/dd-java-agent/instrumentation/karate/src/test/java/org/example/test_unskippable.feature b/dd-java-agent/instrumentation/karate/src/test/java/org/example/test_unskippable.feature new file mode 100644 index 000000000000..25142d2a1616 --- /dev/null +++ b/dd-java-agent/instrumentation/karate/src/test/java/org/example/test_unskippable.feature @@ -0,0 +1,10 @@ +@foo +Feature: test unskippable + + @bar + @datadog_itr_unskippable + Scenario: first scenario + * print 'first' + + Scenario: second scenario + * print 'second' diff --git a/gradle/configure_tests.gradle b/gradle/configure_tests.gradle index 1b889d7b3a08..aaab04a66f76 100644 --- a/gradle/configure_tests.gradle +++ b/gradle/configure_tests.gradle @@ -2,7 +2,7 @@ import java.time.Duration import java.time.temporal.ChronoUnit def isTestingInstrumentation(Project project) { - return ["junit-4.10", "junit-5.3", "testng-6", "testng-7", "cucumber", "munit", "spock"].contains(project.name) + return ["junit-4.10", "junit-5.3", "testng-6", "testng-7", "cucumber", "munit", "spock", "karate"].contains(project.name) } def forkedTestLimit = gradle.sharedServices.registerIfAbsent("forkedTestLimit", BuildService) { diff --git a/internal-api/src/main/java/datadog/trace/api/civisibility/events/TestEventsHandler.java b/internal-api/src/main/java/datadog/trace/api/civisibility/events/TestEventsHandler.java index 903f4d4e6369..81eb3ef86263 100644 --- a/internal-api/src/main/java/datadog/trace/api/civisibility/events/TestEventsHandler.java +++ b/internal-api/src/main/java/datadog/trace/api/civisibility/events/TestEventsHandler.java @@ -5,7 +5,6 @@ import java.lang.reflect.Method; import java.nio.file.Path; import java.util.Collection; -import java.util.List; import javax.annotation.Nullable; public interface TestEventsHandler extends Closeable { @@ -66,7 +65,7 @@ void onTestIgnore( @Nullable String testFramework, @Nullable String testFrameworkVersion, @Nullable String testParameters, - @Nullable List<String> categories, + @Nullable Collection<String> categories, @Nullable Class<?> testClass, @Nullable String testMethodName, @Nullable Method testMethod, diff --git a/internal-api/src/main/java/datadog/trace/util/MethodHandles.java b/internal-api/src/main/java/datadog/trace/util/MethodHandles.java index 9ae8f7c7a97c..ebe8b3e7e667 100644 --- a/internal-api/src/main/java/datadog/trace/util/MethodHandles.java +++ b/internal-api/src/main/java/datadog/trace/util/MethodHandles.java @@ -29,7 +29,7 @@ public MethodHandle privateFieldGetter(String className, String fieldName) { return lookup.unreflectGetter(field); } catch (Throwable t) { - log.error("Could not get private field {} getter from class {}", fieldName, className, t); + log.debug("Could not get private field {} getter from class {}", fieldName, className, t); return null; } } @@ -42,7 +42,7 @@ public MethodHandle constructor(String className, Class<?>... parameterTypes) { return lookup.unreflectConstructor(constructor); } catch (Throwable t) { - log.error( + log.debug( "Could not get constructor accepting {} from class {}", Arrays.toString(parameterTypes), className, 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 042c4495daeb..7ef8b6fd15c2 100644 --- a/internal-api/src/main/java/datadog/trace/util/Strings.java +++ b/internal-api/src/main/java/datadog/trace/util/Strings.java @@ -268,25 +268,25 @@ public static CharSequence truncate(CharSequence input, int limit) { return input.subSequence(0, limit); } - public static String toJson(final Map<String, String> map) { + public static String toJson(final Map<String, ?> map) { return toJson(map, false); } - public static String toJson(final Map<String, String> map, boolean valuesAreJson) { + public static String toJson(final Map<String, ?> map, boolean valuesAreJson) { if (map == null || map.isEmpty()) { return "{}"; } final StringBuilder sb = new StringBuilder("{"); - final Iterator<Entry<String, String>> entriesIter = map.entrySet().iterator(); + final Iterator<? extends Entry<String, ?>> entriesIter = map.entrySet().iterator(); while (entriesIter.hasNext()) { - final Entry<String, String> entry = entriesIter.next(); + final Entry<String, ?> entry = entriesIter.next(); sb.append("\"").append(escapeToJson(entry.getKey())).append("\":"); if (valuesAreJson) { sb.append(entry.getValue()); } else { - sb.append("\"").append(escapeToJson(entry.getValue())).append("\""); + sb.append("\"").append(escapeToJson(String.valueOf(entry.getValue()))).append("\""); } if (entriesIter.hasNext()) { diff --git a/settings.gradle b/settings.gradle index 387fe517ac44..9fec14a4024d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -287,6 +287,7 @@ include ':dd-java-agent:instrumentation:junit-5.3:cucumber' include ':dd-java-agent:instrumentation:junit-5.3:spock' include ':dd-java-agent:instrumentation:kafka-clients-0.11' include ':dd-java-agent:instrumentation:kafka-streams-0.11' +include ':dd-java-agent:instrumentation:karate' include ':dd-java-agent:instrumentation:kotlin-coroutines' include ':dd-java-agent:instrumentation:kotlin-coroutines:coroutines-1.3' include ':dd-java-agent:instrumentation:kotlin-coroutines:coroutines-1.5' @@ -423,4 +424,4 @@ include ':dd-java-agent:instrumentation:zio:zio-2.0' include ':dd-java-agent:benchmark' include ':dd-java-agent:benchmark-integration' include ':dd-java-agent:benchmark-integration:jetty-perftest' -include ':dd-java-agent:benchmark-integration:play-perftest' \ No newline at end of file +include ':dd-java-agent:benchmark-integration:play-perftest' From faa5d00364ef758de93a2836d280b1048cc47af1 Mon Sep 17 00:00:00 2001 From: Paul Laffon <paul.laffon@datadoghq.com> Date: Wed, 18 Oct 2023 05:22:08 -0400 Subject: [PATCH 28/29] Rename spark streaming query attributes (#6029) Rename spark streaming query attributes to make it clear that it is coming from a streaming query - id -> streaming_query.id - name -> streaming_query.name - run_id -> streaming_query. run_id - batch_id -> streaming_query.batch_id The renaming is already done in the backend --- .../spark/DatadogSparkListener.java | 16 ++++++++-------- .../groovy/SparkStructuredStreamingTest.groovy | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/dd-java-agent/instrumentation/spark/src/main/java/datadog/trace/instrumentation/spark/DatadogSparkListener.java b/dd-java-agent/instrumentation/spark/src/main/java/datadog/trace/instrumentation/spark/DatadogSparkListener.java index ee17e5321f54..a2a9961825ac 100644 --- a/dd-java-agent/instrumentation/spark/src/main/java/datadog/trace/instrumentation/spark/DatadogSparkListener.java +++ b/dd-java-agent/instrumentation/spark/src/main/java/datadog/trace/instrumentation/spark/DatadogSparkListener.java @@ -634,11 +634,11 @@ private synchronized void onStreamingQueryTerminatedEvent( metrics.setSpanMetrics(batchSpan); } - batchSpan.setTag("id", event.id()); - batchSpan.setTag("run_id", event.runId()); - batchSpan.setTag("batch_id", getBatchIdFromBatchKey(batchKey)); + batchSpan.setTag("streaming_query.id", event.id()); + batchSpan.setTag("streaming_query.run_id", event.runId()); + batchSpan.setTag("streaming_query.batch_id", getBatchIdFromBatchKey(batchKey)); if (startedEvent != null) { - batchSpan.setTag("name", startedEvent.name()); + batchSpan.setTag("streaming_query.name", startedEvent.name()); batchSpan.setTag(DDTags.RESOURCE_NAME, startedEvent.name()); } @@ -669,10 +669,10 @@ private synchronized void onStreamingQueryProgressEvent( metrics.setSpanMetrics(batchSpan); } - batchSpan.setTag("id", progress.id()); - batchSpan.setTag("run_id", progress.runId()); - batchSpan.setTag("batch_id", progress.batchId()); - batchSpan.setTag("name", progress.name()); + batchSpan.setTag("streaming_query.id", progress.id()); + batchSpan.setTag("streaming_query.run_id", progress.runId()); + batchSpan.setTag("streaming_query.batch_id", progress.batchId()); + batchSpan.setTag("streaming_query.name", progress.name()); batchSpan.setTag(DDTags.RESOURCE_NAME, progress.name()); batchSpan.setMetric("spark.num_input_rows", progress.numInputRows()); diff --git a/dd-java-agent/instrumentation/spark/src/test/groovy/SparkStructuredStreamingTest.groovy b/dd-java-agent/instrumentation/spark/src/test/groovy/SparkStructuredStreamingTest.groovy index dc9a0f1f4887..7e5903c3dd32 100644 --- a/dd-java-agent/instrumentation/spark/src/test/groovy/SparkStructuredStreamingTest.groovy +++ b/dd-java-agent/instrumentation/spark/src/test/groovy/SparkStructuredStreamingTest.groovy @@ -68,11 +68,11 @@ class SparkStructuredStreamingTest extends AgentTestRunner { tags { defaultTags() // Streaming tags - "batch_id" 0 - "name" "test-query" + "streaming_query.batch_id" 0 + "streaming_query.name" "test-query" "app_id" String - "id" UUID - "run_id" UUID + "streaming_query.id" UUID + "streaming_query.run_id" UUID "spark.num_input_rows" 3 "spark.add_batch_duration" Long "spark.get_batch_duration" Long @@ -169,7 +169,7 @@ class SparkStructuredStreamingTest extends AgentTestRunner { span { operationName "spark.streaming_batch" spanType "spark" - assert span.tags["batch_id"] == 1 + assert span.tags["streaming_query.batch_id"] == 1 parent() } span { From 21a62af8a25a997d13ca9e9e9388904238049f8a Mon Sep 17 00:00:00 2001 From: Stuart McCulloch <stuart.mcculloch@datadoghq.com> Date: Wed, 18 Oct 2023 11:22:31 +0200 Subject: [PATCH 29/29] Move dd-trace-core to separate section of shaded dd-java-agent jar (#6052) --- dd-java-agent/build.gradle | 17 ++++++++++++++--- dd-java-agent/instrumentation/build.gradle | 6 ++++++ .../ResourcesFeatureInstrumentation.java | 4 ++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/dd-java-agent/build.gradle b/dd-java-agent/build.gradle index 2b9e8e4f15f3..2ad6be9e0432 100644 --- a/dd-java-agent/build.gradle +++ b/dd-java-agent/build.gradle @@ -14,16 +14,17 @@ apply from: "$rootDir/gradle/publish.gradle" configurations { shadowInclude sharedShadowInclude + traceShadowInclude } sourceCompatibility = JavaVersion.VERSION_1_7 targetCompatibility = JavaVersion.VERSION_1_7 /* - * 7 shadow jars are created + * Several shadow jars are created * - The main "dd-java-agent" jar that also has the bootstrap project - * - 5 jars based on projects (instrumentation, jmxfetch, profiling, appsec, iast) - * - 1 based on the shared dependencies + * - Major feature jars (trace, instrumentation, jmxfetch, profiling, appsec, iast, debugger, ci-visibility) + * - A shared dependencies jar * This general config is shared by all of them */ @@ -124,6 +125,15 @@ def sharedShadowJar = tasks.register('sharedShadowJar', ShadowJar) { } includeShadowJar(sharedShadowJar, 'shared') +// place the tracer in its own shadow jar separate to instrumentation +def traceShadowJar = tasks.register('traceShadowJar', ShadowJar) { + configurations = [project.configurations.traceShadowInclude] + it.destinationDirectory.set(file("${project.buildDir}/trace-lib")) + archiveClassifier = 'trace' + it.dependencies deps.excludeShared +} +includeShadowJar(traceShadowJar, 'trace') + shadowJar generalShadowJarConfig >> { configurations = [project.configurations.shadowInclude] @@ -224,6 +234,7 @@ dependencies { sharedShadowInclude project(':utils:version-utils'), { transitive = false } + traceShadowInclude project(':dd-trace-core') } tasks.withType(Test).configureEach { diff --git a/dd-java-agent/instrumentation/build.gradle b/dd-java-agent/instrumentation/build.gradle index 72f3e80ba15b..3847c8b9dd09 100644 --- a/dd-java-agent/instrumentation/build.gradle +++ b/dd-java-agent/instrumentation/build.gradle @@ -132,6 +132,12 @@ if (project.gradle.startParameter.taskNames.any {it.endsWith("generateMuzzleRepo tasks.named('shadowJar').configure { duplicatesStrategy = DuplicatesStrategy.FAIL + dependencies { + // the tracer is now in a separate shadow jar + exclude(project(":dd-trace-core")) + exclude(dependency('com.datadoghq:sketches-java')) + exclude(dependency('com.google.re2j:re2j')) + } dependencies deps.excludeShared } diff --git a/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/ResourcesFeatureInstrumentation.java b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/ResourcesFeatureInstrumentation.java index 31bbeed8d121..8dee96dd667e 100644 --- a/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/ResourcesFeatureInstrumentation.java +++ b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/ResourcesFeatureInstrumentation.java @@ -29,13 +29,13 @@ public static class InjectResourcesAdvice { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit() { // the tracer jar is not listed on the image classpath, so manually inject its resources - // (drop inst/shared prefixes from embedded resources, so we can find them in native-image + // (drop trace/shared prefixes from embedded resources, so we can find them in native-image // as the final executable won't have our isolating class-loader to map these resources) String[] tracerResources = { "dd-java-agent.version", "dd-trace-api.version", - "inst/dd-trace-core.version", + "trace/dd-trace-core.version", "shared/dogstatsd/version.properties", "shared/okhttp3/internal/publicsuffix/publicsuffixes.gz" };