diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java index a247c8b67ec..f7e0be0c48c 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java @@ -8,6 +8,7 @@ import com.sun.management.HotSpotDiagnosticMXBean; import datadog.environment.JavaVirtualMachine; import datadog.environment.OperatingSystem; +import datadog.environment.SystemProperties; import datadog.libs.ddprof.DdprofLibraryLoader; import datadog.trace.api.Platform; import datadog.trace.util.TempLocationManager; @@ -127,15 +128,23 @@ public static boolean initialize(boolean forceJmx) { */ private static boolean initializeJ9() { try { - String scriptPath = getJ9CrashUploaderScriptPath(); - // Check if -Xdump:tool is already configured via JVM arguments boolean xdumpConfigured = isXdumpToolConfigured(); // Get custom javacore path if configured String javacorePath = getJ9JavacorePath(); + if (javacorePath == null || javacorePath.isEmpty()) { + // OpenJ9 defaults javacore output to the JVM working directory. Persist that location in + // the uploader config so the crash script does not need to guess from its own cwd. + javacorePath = SystemProperties.get("user.dir"); + } if (xdumpConfigured) { LOG.debug("J9 crash tracking: -Xdump:tool already configured, crash uploads enabled"); + // Use the path from the -Xdump:tool arg when available (allows callers to specify a known + // path via -Xdump:tool:events=gpf+abort,exec=\ %pid), falling back to the default + // TempLocationManager path when the path cannot be extracted. + String extractedPath = extractJ9ScriptPathFromXdumpArg(); + String scriptPath = extractedPath != null ? extractedPath : getJ9CrashUploaderScriptPath(); // Initialize the crash uploader script and config manager CrashUploaderScriptInitializer.initialize(scriptPath, null, javacorePath); // Also set up OOME notifier script @@ -143,6 +152,7 @@ private static boolean initializeJ9() { OOMENotifierScriptInitializer.initialize(oomeScript); return true; } else { + String scriptPath = getJ9CrashUploaderScriptPath(); // Log instructions for manual configuration LOG.info("J9 JVM detected. To enable crash tracking, add this JVM argument at startup:"); LOG.info(" -Xdump:tool:events=gpf+abort,exec={}\\ %pid", scriptPath); @@ -158,6 +168,40 @@ private static boolean initializeJ9() { return false; } + /** + * Extract the crash uploader script path from the {@code -Xdump:tool} JVM argument. + * + *

Looks for a JVM argument of the form {@code + * -Xdump:tool:events=...,exec=/path/to/dd_crash_uploader.sh\ %pid} and returns the script path + * portion (before the {@code \ %pid} argument separator). + * + * @return the script path, or {@code null} if not found or not extractable + */ + private static String extractJ9ScriptPathFromXdumpArg() { + List vmArgs = JavaVirtualMachine.getVmOptions(); + for (String arg : vmArgs) { + if (arg.startsWith("-Xdump:tool") && arg.contains("dd_crash_uploader")) { + int execIdx = arg.indexOf("exec="); + if (execIdx >= 0) { + String execVal = arg.substring(execIdx + 5); + // Separator between command and args: plain space, or "\ " (backslash + space) as + // suggested by the Initializer's log hint. Check plain space first since that is the + // form that actually works when the shell splits the exec string into tokens. + int spaceIdx = execVal.indexOf(' '); + if (spaceIdx >= 0) { + String candidate = execVal.substring(0, spaceIdx); + // Strip a trailing backslash left over from the "\ %pid" notation + return candidate.endsWith("\\") + ? candidate.substring(0, candidate.length() - 1) + : candidate; + } + return execVal; + } + } + } + return null; + } + /** * Get the custom javacore file path from -Xdump:java:file=... JVM argument. * diff --git a/dd-smoke-tests/crashtracking/build.gradle b/dd-smoke-tests/crashtracking/build.gradle index 9ce1f54afa2..c0b11b56fa8 100644 --- a/dd-smoke-tests/crashtracking/build.gradle +++ b/dd-smoke-tests/crashtracking/build.gradle @@ -42,4 +42,3 @@ tasks.withType(Test).configureEach { showStandardStreams = true } } - diff --git a/dd-smoke-tests/crashtracking/src/main/java/datadog/smoketest/crashtracking/OpenJ9CrashtrackingTestApplication.java b/dd-smoke-tests/crashtracking/src/main/java/datadog/smoketest/crashtracking/OpenJ9CrashtrackingTestApplication.java new file mode 100644 index 00000000000..eb3bcda2595 --- /dev/null +++ b/dd-smoke-tests/crashtracking/src/main/java/datadog/smoketest/crashtracking/OpenJ9CrashtrackingTestApplication.java @@ -0,0 +1,56 @@ +package datadog.smoketest.crashtracking; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.concurrent.TimeUnit; + +/** + * Test application for OpenJ9 crash tracking smoke tests. + * + *

Waits for the agent to write the crash-uploader script, then crashes the JVM via a null + * pointer write using {@code sun.misc.Unsafe.putAddress(0L, 0L)} (accessed via reflection so the + * class compiles against any Java version). This triggers a GPF (general protection fault) on + * OpenJ9, which the {@code -Xdump:tool:events=gpf+abort,...} handler detects. + * + *

Note: {@code sun.misc.Unsafe.getLong(0L)} is converted to a Java-level {@link + * NullPointerException} on Semeru/OpenJ9 25, so it does not exercise crash tracking. {@code + * sun.misc.Unsafe.putAddress(0L, 0L)} goes directly to {@code unsafePut64} in the JVM native + * library and produces a native SIGSEGV at address 0. + * + *

System properties consumed: + * + *

+ */ +public class OpenJ9CrashtrackingTestApplication { + public static void main(String[] args) throws Exception { + // Wait for the agent to write the crash-uploader script (proves initialization is done) + String scriptPath = System.getProperty("dd.test.crash_script"); + if (scriptPath != null) { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30); + while (!Files.exists(Paths.get(scriptPath)) && System.nanoTime() < deadline) { + Thread.sleep(200); + } + if (!Files.exists(Paths.get(scriptPath))) { + System.err.println("Timeout: crash script not created at " + scriptPath); + System.exit(-1); + } + } + + System.out.println("===> Crash script ready, crashing JVM via Unsafe.putAddress(0L, 0L)..."); + System.out.flush(); + + // Write to address 0 via sun.misc.Unsafe to trigger a SIGSEGV (GPF event). + // Unsafe.getLong(0L) was not enough on OpenJ9 here; it threw a NullPointerException instead. + Class unsafeClass = Class.forName("sun.misc.Unsafe"); + Field f = unsafeClass.getDeclaredField("theUnsafe"); + f.setAccessible(true); + Object theUnsafe = f.get(null); + Method putAddress = unsafeClass.getDeclaredMethod("putAddress", long.class, long.class); + putAddress.invoke(theUnsafe, 0L, 0L); + } +} diff --git a/dd-smoke-tests/crashtracking/src/test/java/datadog/smoketest/AbstractCrashtrackingSmokeTest.java b/dd-smoke-tests/crashtracking/src/test/java/datadog/smoketest/AbstractCrashtrackingSmokeTest.java new file mode 100644 index 00000000000..8fd62bc020a --- /dev/null +++ b/dd-smoke-tests/crashtracking/src/test/java/datadog/smoketest/AbstractCrashtrackingSmokeTest.java @@ -0,0 +1,120 @@ +package datadog.smoketest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.squareup.moshi.Moshi; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +abstract class AbstractCrashtrackingSmokeTest { + static final OutputThreads OUTPUT = new OutputThreads(); + static final Path LOG_FILE_DIR = + Paths.get(System.getProperty("datadog.smoketest.builddir"), "reports"); + + MockWebServer tracingServer; + final BlockingQueue crashEvents = new LinkedBlockingQueue<>(); + final Moshi moshi = new Moshi.Builder().build(); + Path tempDir; + + @BeforeEach + void setUpTracingServer() throws Exception { + tempDir = Files.createTempDirectory("dd-smoketest-"); + crashEvents.clear(); + tracingServer = new MockWebServer(); + tracingServer.setDispatcher( + new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + String data = request.getBody().readString(StandardCharsets.UTF_8); + System.out.println("URL ====== " + request.getPath()); + if ("/telemetry/proxy/api/v2/apmtelemetry".equals(request.getPath())) { + try { + MinimalTelemetryData minimal = + moshi.adapter(MinimalTelemetryData.class).fromJson(data); + if ("logs".equals(minimal.request_type)) { + crashEvents.add(moshi.adapter(CrashTelemetryData.class).fromJson(data)); + } + } catch (IOException e) { + System.out.println("Unable to parse: " + e); + } + } + System.out.println(data); + return new MockResponse().setResponseCode(200); + } + }); + OUTPUT.clearMessages(); + } + + @AfterEach + void tearDownTracingServer() throws Exception { + tracingServer.shutdown(); + try (Stream files = Files.walk(tempDir)) { + files.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + Files.deleteIfExists(tempDir); + } + + @AfterAll + static void shutdownOutputThreads() { + OUTPUT.close(); + } + + protected long crashDataTimeoutMs() { + return 10 * 1000; + } + + protected String assertCrashPing() throws InterruptedException, IOException { + CrashTelemetryData crashData = crashEvents.poll(crashDataTimeoutMs(), TimeUnit.MILLISECONDS); + assertNotNull(crashData, "Crash ping not sent"); + assertTrue(crashData.payload.get(0).tags.contains("is_crash_ping:true"), "Not a crash ping"); + final Object uuid = + moshi.adapter(Map.class).fromJson(crashData.payload.get(0).message).get("crash_uuid"); + assertNotNull(uuid, "crash uuid not found"); + return uuid.toString(); + } + + protected CrashTelemetryData assertCrashData(String uuid) + throws InterruptedException, IOException { + CrashTelemetryData crashData = crashEvents.poll(crashDataTimeoutMs(), TimeUnit.MILLISECONDS); + assertNotNull(crashData, "Crash data not uploaded"); + assertTrue( + crashData.payload.get(0).tags.contains("severity:crash"), "Expected severity:crash tag"); + final Object receivedUuid = + moshi.adapter(Map.class).fromJson(crashData.payload.get(0).message).get("uuid"); + assertEquals(uuid, receivedUuid, "crash uuid should match the one sent with the ping"); + return crashData; + } + + static String javaPath() { + String sep = FileSystems.getDefault().getSeparator(); + return System.getProperty("java.home") + sep + "bin" + sep + "java"; + } + + static String appShadowJar() { + return System.getProperty("datadog.smoketest.app.shadowJar.path"); + } + + static String agentShadowJar() { + return System.getProperty("datadog.smoketest.agent.shadowJar.path"); + } +} diff --git a/dd-smoke-tests/crashtracking/src/test/java/datadog/smoketest/CrashtrackingSmokeTest.java b/dd-smoke-tests/crashtracking/src/test/java/datadog/smoketest/CrashtrackingSmokeTest.java index bbf554523e6..67f1edb9e0e 100644 --- a/dd-smoke-tests/crashtracking/src/test/java/datadog/smoketest/CrashtrackingSmokeTest.java +++ b/dd-smoke-tests/crashtracking/src/test/java/datadog/smoketest/CrashtrackingSmokeTest.java @@ -1,38 +1,19 @@ package datadog.smoketest; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeFalse; import static org.junit.jupiter.api.Assumptions.assumeTrue; import com.datadoghq.profiler.Platform; -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; import datadog.environment.JavaVirtualMachine; import datadog.environment.OperatingSystem; -import java.io.File; import java.io.IOException; import java.net.ServerSocket; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; -import java.util.Comparator; import java.util.List; -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; -import okhttp3.mockwebserver.Dispatcher; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -42,16 +23,8 @@ * NOTE: The current implementation of crash tracking doesn't work with ancient version of bash * that ships with OS X by default. */ -public class CrashtrackingSmokeTest { - private static final long DATA_TIMEOUT_MS = 10 * 1000; - private static final OutputThreads OUTPUT = new OutputThreads(); - private static final Path LOG_FILE_DIR = - Paths.get(System.getProperty("datadog.smoketest.builddir"), "reports"); - - private MockWebServer tracingServer; +public class CrashtrackingSmokeTest extends AbstractCrashtrackingSmokeTest { private TestUDPServer udpServer; - private final BlockingQueue crashEvents = new LinkedBlockingQueue<>(); - private final Moshi moshi = new Moshi.Builder().build(); @BeforeAll static void setupAll() { @@ -59,77 +32,15 @@ static void setupAll() { assumeFalse(JavaVirtualMachine.isJ9()); } - private Path tempDir; - @BeforeEach - void setup() throws Exception { - tempDir = Files.createTempDirectory("dd-smoketest-"); - - crashEvents.clear(); - - tracingServer = new MockWebServer(); - tracingServer.setDispatcher( - new Dispatcher() { - @Override - public MockResponse dispatch(final RecordedRequest request) { - System.out.println("URL ====== " + request.getPath()); - - String data = request.getBody().readString(StandardCharsets.UTF_8); - - if ("/telemetry/proxy/api/v2/apmtelemetry".equals(request.getPath())) { - try { - JsonAdapter adapter = - moshi.adapter(MinimalTelemetryData.class); - MinimalTelemetryData minimal = adapter.fromJson(data); - if ("logs".equals(minimal.request_type)) { - JsonAdapter crashAdapter = - moshi.adapter(CrashTelemetryData.class); - crashEvents.add(crashAdapter.fromJson(data)); - } - } catch (IOException e) { - System.out.println("Unable to parse " + e); - } - } - - System.out.println(data); - - return new MockResponse().setResponseCode(200); - } - }); - + void setUpUdpServer() throws Exception { udpServer = new TestUDPServer(); udpServer.start(); - - OUTPUT.clearMessages(); } @AfterEach - void teardown() throws Exception { - tracingServer.shutdown(); + void tearDownUdpServer() throws Exception { udpServer.close(); - - try (Stream fileStream = Files.walk(tempDir)) { - fileStream.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); - } - Files.deleteIfExists(tempDir); - } - - @AfterAll - static void shutdown() { - OUTPUT.close(); - } - - private static String javaPath() { - final String separator = FileSystems.getDefault().getSeparator(); - return System.getProperty("java.home") + separator + "bin" + separator + "java"; - } - - private static String appShadowJar() { - return System.getProperty("datadog.smoketest.app.shadowJar.path"); - } - - private static String agentShadowJar() { - return System.getProperty("datadog.smoketest.agent.shadowJar.path"); } private static String getExtension() { @@ -169,7 +80,7 @@ void testAutoInjection() throws Exception { */ @Test void testCrashTracking() throws Exception { - Path script = tempDir.resolve("dd_crash_uploader." + getExtension()); + String script = tempDir.resolve("dd_crash_uploader." + getExtension()).toString(); String onErrorValue = script + " %p"; String errorFile = tempDir.resolve("hs_err.log").toString(); @@ -210,8 +121,7 @@ void testCrashTracking() throws Exception { */ @Test void testCrashTrackingLegacy() throws Exception { - Path script = tempDir.resolve("dd_crash_uploader." + getExtension()); - String onErrorValue = script.toString(); + String script = tempDir.resolve("dd_crash_uploader." + getExtension()).toString(); String errorFile = tempDir.resolve("hs_err.log").toString(); ProcessBuilder pb = @@ -221,7 +131,7 @@ void testCrashTrackingLegacy() throws Exception { "-javaagent:" + agentShadowJar(), "-Xmx96m", "-Xms96m", - "-XX:OnError=" + onErrorValue, + "-XX:OnError=" + script, "-XX:ErrorFile=" + errorFile, "-XX:+CrashOnOutOfMemoryError", // Use OOME to trigger crash "-Ddd.dogstatsd.start-delay=0", // Minimize the delay to initialize JMX and create @@ -246,7 +156,7 @@ void testCrashTrackingLegacy() throws Exception { */ @Test void testOomeTracking() throws Exception { - Path script = tempDir.resolve("dd_oome_notifier." + getExtension()); + String script = tempDir.resolve("dd_oome_notifier." + getExtension()).toString(); String onErrorValue = script + " %p"; String errorFile = tempDir.resolve("hs_err_pid%p.log").toString(); @@ -287,8 +197,8 @@ void testOomeTracking() throws Exception { @Test void testCombineTracking() throws Exception { - Path errorScript = tempDir.resolve("dd_crash_uploader." + getExtension()); - Path oomeScript = tempDir.resolve("dd_oome_notifier." + getExtension()); + String errorScript = tempDir.resolve("dd_crash_uploader." + getExtension()).toString(); + String oomeScript = tempDir.resolve("dd_oome_notifier." + getExtension()).toString(); String onErrorValue = errorScript + " %p"; String onOomeValue = oomeScript + " %p"; String errorFile = tempDir.resolve("hs_err.log").toString(); @@ -331,7 +241,7 @@ void testCombineTracking() throws Exception { void testOomeTrackingWithInheritedEnvVars() throws Exception { int jmxPort = findFreePort(); - Path script = tempDir.resolve("dd_oome_notifier." + getExtension()); + String script = tempDir.resolve("dd_oome_notifier." + getExtension()).toString(); String onErrorValue = script + " %p"; String errorFile = tempDir.resolve("hs_err_pid%p.log").toString(); @@ -391,7 +301,7 @@ void testOomeTrackingWithInheritedEnvVars() throws Exception { void testCrashTrackingWithInheritedEnvVars() throws Exception { int jmxPort = findFreePort(); - Path script = tempDir.resolve("dd_crash_uploader." + getExtension()); + String script = tempDir.resolve("dd_crash_uploader." + getExtension()).toString(); String onErrorValue = script + " %p"; String errorFile = tempDir.resolve("hs_err.log").toString(); @@ -452,30 +362,18 @@ private static void assertExpectedCrash(Process p) throws InterruptedException { assertTrue(p.waitFor() > 0, "Application should have crashed"); } - private String assertCrashPing() throws InterruptedException, IOException { - CrashTelemetryData crashData = crashEvents.poll(DATA_TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertNotNull(crashData, "Crash ping not sent"); - assertTrue(crashData.payload.get(0).tags.contains("is_crash_ping:true"), "Not a crash ping"); - final Map map = moshi.adapter(Map.class).fromJson(crashData.payload.get(0).message); - final Object uuid = map.get("crash_uuid"); - assertNotNull(uuid, "crash uuid not found"); - return uuid.toString(); - } - - private void assertCrashData(String uuid) throws InterruptedException, IOException { - CrashTelemetryData crashData = crashEvents.poll(DATA_TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertNotNull(crashData, "Crash data not uploaded"); + @Override + protected CrashTelemetryData assertCrashData(String uuid) + throws InterruptedException, IOException { + CrashTelemetryData crashData = super.assertCrashData(uuid); assertTrue(crashData.payload.get(0).message.contains("Java heap space")); - assertTrue(crashData.payload.get(0).tags.contains("severity:crash")); - final Map map = moshi.adapter(Map.class).fromJson(crashData.payload.get(0).message); - final Object receivedUuid = map.get("uuid"); - assertEquals(uuid, receivedUuid, "crash uuid should match the one sent with the ping"); + return crashData; } private void assertOOMEvent() throws InterruptedException { String event; do { - event = udpServer.getMessages().poll(DATA_TIMEOUT_MS, TimeUnit.MILLISECONDS); + event = udpServer.getMessages().poll(crashDataTimeoutMs(), TimeUnit.MILLISECONDS); } while (event != null && !event.startsWith("_e")); assertNotNull(event, "OOM Event not received"); diff --git a/dd-smoke-tests/crashtracking/src/test/java/datadog/smoketest/OpenJ9CrashtrackingSmokeTest.java b/dd-smoke-tests/crashtracking/src/test/java/datadog/smoketest/OpenJ9CrashtrackingSmokeTest.java new file mode 100644 index 00000000000..a319b53b2b5 --- /dev/null +++ b/dd-smoke-tests/crashtracking/src/test/java/datadog/smoketest/OpenJ9CrashtrackingSmokeTest.java @@ -0,0 +1,90 @@ +package datadog.smoketest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import datadog.environment.JavaVirtualMachine; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class OpenJ9CrashtrackingSmokeTest extends AbstractCrashtrackingSmokeTest { + @BeforeAll + static void setupAll() { + assumeTrue(JavaVirtualMachine.isJ9(), "OpenJ9 launcher required"); + } + + @Override + protected long crashDataTimeoutMs() { + return 30_000; + } + + /** + * Verifies end-to-end OpenJ9 crash tracking: + * + *
    + *
  1. The agent detects the {@code -Xdump:tool} argument and writes the crash-uploader script + * to the path specified in {@code exec=}. + *
  2. The application crashes itself via {@code sun.misc.Unsafe.putAddress(0L, 0L)}, which + * triggers the dump tool on the {@code gpf} event. + *
  3. The crash-uploader uploads the javacore file and the telemetry (ping + data) arrives. + *
+ */ + @Test + void testCrashTracking() throws Exception { + String script = tempDir.resolve("dd_crash_uploader.sh").toString(); + String javacorePattern = tempDir.resolve("javacore.%Y%m%d.%H%M%S.%pid.%seq.txt").toString(); + + List jvmArgs = new ArrayList<>(); + jvmArgs.add(javaPath()); + jvmArgs.add("-javaagent:" + agentShadowJar()); + jvmArgs.add("-Xdump:tool:events=gpf+abort,exec=" + script + " %pid"); + jvmArgs.add("-Xdump:java:file=" + javacorePattern); + jvmArgs.add("-Xdump:system:none"); + jvmArgs.add("-Xdump:snap:none"); + jvmArgs.add("-Ddd.dogstatsd.start-delay=0"); + jvmArgs.add("-Ddd.trace.enabled=false"); + jvmArgs.add("-Ddd.test.crash_script=" + script); + jvmArgs.add("-cp"); + jvmArgs.add(appShadowJar()); + jvmArgs.add("datadog.smoketest.crashtracking.OpenJ9CrashtrackingTestApplication"); + + ProcessBuilder pb = new ProcessBuilder(jvmArgs); + pb.directory(tempDir.toFile()); + pb.environment().put("DD_TRACE_AGENT_PORT", String.valueOf(tracingServer.getPort())); + + Process p = pb.start(); + OUTPUT.captureOutput(p, LOG_FILE_DIR.resolve("testProcess.openj9CrashTracking.log").toFile()); + + // OpenJ9 runs the dump tool synchronously on crash, so the upload completes before JVM exits + assertTrue(p.waitFor(60, TimeUnit.SECONDS), "JVM did not exit within 60s after crash"); + assertTrue(p.exitValue() != 0, "JVM should have crashed (non-zero exit code)"); + + CrashTelemetryData crashData = assertCrashData(assertCrashPing()); + assertAdditionalData(crashData); + } + + @SuppressWarnings("unchecked") + private void assertAdditionalData(CrashTelemetryData crashData) throws IOException { + Map crashDataMap = + moshi.adapter(Map.class).fromJson(crashData.payload.get(0).message); + Map error = (Map) crashDataMap.get("error"); + assertEquals("SIGSEGV", error.get("kind")); + + Map stack = (Map) error.get("stack"); + List frames = (List) stack.get("frames"); + assertTrue( + frames.stream() + .filter(Map.class::isInstance) + .map(Map.class::cast) + .map(frame -> frame.get("function")) + .anyMatch( + "datadog/smoketest/crashtracking/OpenJ9CrashtrackingTestApplication.main"::equals), + "Expected application main frame"); + } +}