diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploader.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploader.java index 6b0a9b388a5..7a838523993 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploader.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploader.java @@ -581,7 +581,8 @@ private RequestBody makeErrorTrackingRequestBody(@Nonnull CrashLog payload, bool if (payload.experimental != null && (payload.experimental.ucontext != null || payload.experimental.registerToMemoryMapping != null - || payload.experimental.runtimeArgs != null)) { + || payload.experimental.runtimeArgs != null + || payload.experimental.runtimeInfo != null)) { writer.name("experimental"); writer.beginObject(); if (payload.experimental.ucontext != null) { @@ -611,6 +612,20 @@ private RequestBody makeErrorTrackingRequestBody(@Nonnull CrashLog payload, bool } writer.endArray(); } + if (payload.experimental.runtimeInfo != null) { + writer.name("runtime_info"); + writer.beginObject(); + if (payload.experimental.runtimeInfo.jreVersion != null) { + writer.name("jre_version").value(payload.experimental.runtimeInfo.jreVersion); + } + if (payload.experimental.runtimeInfo.javaVm != null) { + writer.name("java_vm").value(payload.experimental.runtimeInfo.javaVm); + } + if (payload.experimental.runtimeInfo.vmInfo != null) { + writer.name("vm_info").value(payload.experimental.runtimeInfo.vmInfo); + } + writer.endObject(); + } writer.endObject(); } // files (e.g. /proc/self/maps or dynamic_libraries) diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/dto/Experimental.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/dto/Experimental.java index 30d04bc3f76..449bdd93156 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/dto/Experimental.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/dto/Experimental.java @@ -14,21 +14,33 @@ public final class Experimental { @Json(name = "runtime_args") public final List runtimeArgs; + @Json(name = "runtime_info") + public final RuntimeInfo runtimeInfo; + public Experimental(Map ucontext) { - this(ucontext, null, null); + this(ucontext, null, null, null); } public Experimental(Map ucontext, List runtimeArgs) { - this(ucontext, null, runtimeArgs); + this(ucontext, null, runtimeArgs, null); } public Experimental( Map ucontext, Map registerToMemoryMapping, List runtimeArgs) { + this(ucontext, registerToMemoryMapping, runtimeArgs, null); + } + + public Experimental( + Map ucontext, + Map registerToMemoryMapping, + List runtimeArgs, + RuntimeInfo runtimeInfo) { this.ucontext = ucontext; this.registerToMemoryMapping = registerToMemoryMapping; this.runtimeArgs = runtimeArgs; + this.runtimeInfo = runtimeInfo; } @Override @@ -37,11 +49,12 @@ public boolean equals(Object o) { Experimental that = (Experimental) o; return Objects.equals(ucontext, that.ucontext) && Objects.equals(registerToMemoryMapping, that.registerToMemoryMapping) - && Objects.equals(runtimeArgs, that.runtimeArgs); + && Objects.equals(runtimeArgs, that.runtimeArgs) + && Objects.equals(runtimeInfo, that.runtimeInfo); } @Override public int hashCode() { - return Objects.hash(ucontext, registerToMemoryMapping, runtimeArgs); + return Objects.hash(ucontext, registerToMemoryMapping, runtimeArgs, runtimeInfo); } } diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/dto/RuntimeInfo.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/dto/RuntimeInfo.java new file mode 100644 index 00000000000..b103960158c --- /dev/null +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/dto/RuntimeInfo.java @@ -0,0 +1,45 @@ +package datadog.crashtracking.dto; + +import com.squareup.moshi.Json; +import java.util.Objects; + +/** + * JDK runtime information extracted from the hs_err crash log header and vm_info line. This + * captures the exact JDK vendor and build so crash reports can be correlated with the specific + * binaries in use. + */ +public final class RuntimeInfo { + @Json(name = "jre_version") + public final String jreVersion; + + @Json(name = "java_vm") + public final String javaVm; + + @Json(name = "vm_info") + public final String vmInfo; + + public RuntimeInfo(String jreVersion, String javaVm, String vmInfo) { + this.jreVersion = jreVersion; + this.javaVm = javaVm; + this.vmInfo = vmInfo; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RuntimeInfo that = (RuntimeInfo) o; + return Objects.equals(jreVersion, that.jreVersion) + && Objects.equals(javaVm, that.javaVm) + && Objects.equals(vmInfo, that.vmInfo); + } + + @Override + public int hashCode() { + return Objects.hash(jreVersion, javaVm, vmInfo); + } +} diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/HotspotCrashLogParser.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/HotspotCrashLogParser.java index 0c01f6b57a9..f4cc8e6874f 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/HotspotCrashLogParser.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/HotspotCrashLogParser.java @@ -12,6 +12,7 @@ import datadog.crashtracking.dto.Metadata; import datadog.crashtracking.dto.OSInfo; import datadog.crashtracking.dto.ProcInfo; +import datadog.crashtracking.dto.RuntimeInfo; import datadog.crashtracking.dto.SigInfo; import datadog.crashtracking.dto.StackFrame; import datadog.crashtracking.dto.StackTrace; @@ -44,6 +45,9 @@ */ public final class HotspotCrashLogParser { private static final String HOTSPOT_JVM_ARGS_PREFIX = "jvm_args:"; + private static final String JRE_VERSION_PREFIX = "# JRE version: "; + private static final String JAVA_VM_PREFIX = "# Java VM: "; + private static final String VM_INFO_PREFIX = "vm_info: "; private static final DateTimeFormatter ZONED_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("EEE MMM ppd HH:mm:ss yyyy zzz", Locale.getDefault()); private static final DateTimeFormatter OFFSET_DATE_TIME_FORMATTER = @@ -362,6 +366,9 @@ public CrashLog parse(String uuid, String crashLog) { String dynamicLibraryKey = null; boolean previousLineBlank = false; State nextThreadSectionState = null; + String jreVersion = null; + String javaVm = null; + String vmInfo = null; String[] lines = NEWLINE_SPLITTER.split(crashLog); outer: @@ -392,6 +399,11 @@ public CrashLog parse(String uuid, String crashLog) { } } } + if (jreVersion == null && line.startsWith(JRE_VERSION_PREFIX)) { + jreVersion = line.substring(JRE_VERSION_PREFIX.length()).trim(); + } else if (javaVm == null && line.startsWith(JAVA_VM_PREFIX)) { + javaVm = line.substring(JAVA_VM_PREFIX.length()).trim(); + } break; case HEADER: if (line.contains("S U M M A R Y")) { @@ -486,6 +498,8 @@ public CrashLog parse(String uuid, String crashLog) { state = State.DYNAMIC_LIBRARIES; } else if (line.contains("S Y S T E M")) { state = State.SYSTEM; + } else if (vmInfo == null && line.startsWith(VM_INFO_PREFIX)) { + vmInfo = line.substring(VM_INFO_PREFIX.length()).trim(); } else if (line.equals("END.")) { state = State.DONE; } @@ -527,6 +541,8 @@ public CrashLog parse(String uuid, String crashLog) { datetimeRaw = line.substring(6).trim(); } else if (datetime == null && datetimeRaw != null && line.startsWith("timezone: ")) { datetime = dateTimeToISO(datetimeRaw + " " + line.substring(10).trim()); + } else if (vmInfo == null && line.startsWith(VM_INFO_PREFIX)) { + vmInfo = line.substring(VM_INFO_PREFIX.length()).trim(); } break; case DONE: @@ -609,11 +625,16 @@ public CrashLog parse(String uuid, String crashLog) { registerToMemoryMapping.replaceAll((k, v) -> RedactUtils.redactRegisterToMemoryMapping(v)); resolvedMapping = registerToMemoryMapping; } + RuntimeInfo runtimeInfo = + (jreVersion != null || javaVm != null || vmInfo != null) + ? new RuntimeInfo(jreVersion, javaVm, vmInfo) + : null; Experimental experimental = !registers.isEmpty() || resolvedMapping != null || (runtimeArgs != null && !runtimeArgs.isEmpty()) - ? new Experimental(registers, resolvedMapping, runtimeArgs) + || runtimeInfo != null + ? new Experimental(registers, resolvedMapping, runtimeArgs, runtimeInfo) : null; DynamicLibs files = (dynamicLibraryLines != null && !dynamicLibraryLines.isEmpty()) diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/J9JavacoreParser.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/J9JavacoreParser.java index f13896b0dca..cb5dc5ed8f5 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/J9JavacoreParser.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/J9JavacoreParser.java @@ -11,6 +11,7 @@ import datadog.crashtracking.dto.Metadata; import datadog.crashtracking.dto.OSInfo; import datadog.crashtracking.dto.ProcInfo; +import datadog.crashtracking.dto.RuntimeInfo; import datadog.crashtracking.dto.SigInfo; import datadog.crashtracking.dto.StackFrame; import datadog.crashtracking.dto.StackTrace; @@ -46,6 +47,8 @@ */ public final class J9JavacoreParser { private static final String J9_USER_ARG_PREFIX = "2CIUSERARG"; + private static final String J9_JAVA_VERSION_PREFIX = "1CIJAVAVERSION "; + private static final String J9_VM_VERSION_PREFIX = "1CIVMVERSION"; private final BuildIdCollector buildIdCollector; @@ -119,6 +122,8 @@ public CrashLog parse(String uuid, String javacoreContent) { Map registers = null; RuntimeArgs j9UserArgs = new RuntimeArgs(); + String j9JavaVersion = null; + String j9VmVersion = null; String[] lines = NEWLINE_SPLITTER.split(javacoreContent); @@ -179,6 +184,11 @@ public CrashLog parse(String uuid, String javacoreContent) { if (pidMatcher.matches()) { pid = pidMatcher.group(1); } + if (j9JavaVersion == null && line.startsWith(J9_JAVA_VERSION_PREFIX)) { + j9JavaVersion = line.substring(J9_JAVA_VERSION_PREFIX.length()).trim(); + } else if (j9VmVersion == null && line.startsWith(J9_VM_VERSION_PREFIX)) { + j9VmVersion = line.substring(J9_VM_VERSION_PREFIX.length()).trim(); + } break; case THREADS: @@ -299,10 +309,15 @@ public CrashLog parse(String uuid, String javacoreContent) { Integer parsedPid = safelyParseInt(pid); ProcInfo procInfo = parsedPid != null ? new ProcInfo(parsedPid) : null; List runtimeArgs = j9UserArgs.build(); + RuntimeInfo runtimeInfo = + (j9JavaVersion != null || j9VmVersion != null) + ? new RuntimeInfo(j9JavaVersion, null, j9VmVersion) + : null; Experimental experimental = (registers != null && !registers.isEmpty()) || (runtimeArgs != null && !runtimeArgs.isEmpty()) - ? new Experimental(registers, runtimeArgs) + || runtimeInfo != null + ? new Experimental(registers, null, runtimeArgs, runtimeInfo) : null; return new CrashLog( diff --git a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/HotspotCrashLogParserTest.java b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/HotspotCrashLogParserTest.java index 2008ac78e05..28bcda39e7f 100644 --- a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/HotspotCrashLogParserTest.java +++ b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/HotspotCrashLogParserTest.java @@ -337,6 +337,28 @@ public void testParseCurrentThreadName(String line, String expected) { HotspotCrashLogParser.parseCurrentThreadName(line)); } + @TableTest({ + "scenario | filename | expectedJreVersion | expectedVmInfo ", + "Zulu 17 | sample-crash-for-telemetry.txt | OpenJDK Runtime Environment Zulu17.42+20-SA (17.0.7+7) (build 17.0.7+7-LTS) | OpenJDK 64-Bit Server VM (17.0.7+7-LTS) for linux-amd64 JRE (17.0.7+7-LTS) (Zulu17.42+20-SA), built on Apr 11 2023 11:39:51 by \"zulu_re\" with gcc 8.3.0 ", + "Temurin 22 | sample-crash-for-telemetry-2.txt | OpenJDK Runtime Environment Temurin-22.0.1+8 (22.0.1+8) (build 22.0.1+8) | OpenJDK 64-Bit Server VM (22.0.1+8) for linux-amd64 JRE (22.0.1+8), built on 2024-04-16T00:00:00Z by \"admin\" with gcc 11.3.0 ", + "Zulu 8 | sample-crash-for-telemetry-3.txt | OpenJDK Runtime Environment (Zulu 8.70.0.23-CA-macos-aarch64) (8.0_372-b07) (build 1.8.0_372-b07) | OpenJDK 64-Bit Server VM (25.372-b07) for bsd-aarch64 JRE (Zulu 8.70.0.23-CA-macos-aarch64) (1.8.0_372-b07), built on Apr 18 2023 01:36:20 by \"zulu_re\" with gcc Apple LLVM 12.0.0 (clang-1200.0.32.28)", + "Corretto 21 | sample-crash-linux-aarch64.txt | OpenJDK Runtime Environment Corretto-21.0.7.6.1 (21.0.7+6) (build 21.0.7+6-LTS) | OpenJDK 64-Bit Server VM (21.0.7+6-LTS) for linux-aarch64-musl JRE (21.0.7+6-LTS), built on 2025-04-09T23:34:45Z by \"jenkins\" with gcc 12.2.1 20220924 ", + "OpenJDK 25 | sample-crash-macos-aarch64.txt | OpenJDK Runtime Environment (25.0.2+10) (build 25.0.2+10-69) | OpenJDK 64-Bit Server VM (25.0.2+10-69) for bsd-aarch64 JRE (25.0.2+10-69), built on 2025-12-18T11:36:35Z with clang Apple LLVM 15.0.0 (clang-1500.3.9.4) " + }) + public void testRuntimeInfoParsing( + String filename, String expectedJreVersion, String expectedVmInfo) throws Exception { + CrashLog crashLog = + new HotspotCrashLogParser().parse(UUID.randomUUID().toString(), readFileAsString(filename)); + + assertNotNull(crashLog.experimental, "experimental should be populated"); + assertNotNull(crashLog.experimental.runtimeInfo, "runtimeInfo should be populated"); + assertNotNull(crashLog.experimental.runtimeInfo.jreVersion, "jreVersion should be populated"); + assertNotNull(crashLog.experimental.runtimeInfo.javaVm, "javaVm should be populated"); + assertNotNull(crashLog.experimental.runtimeInfo.vmInfo, "vmInfo should be populated"); + assertEquals(expectedJreVersion, crashLog.experimental.runtimeInfo.jreVersion); + assertEquals(expectedVmInfo, crashLog.experimental.runtimeInfo.vmInfo); + } + @Test public void testNoSignalProducesInternalError() throws Exception { // A crash log that reaches the PROCESS section but has no siginfo line diff --git a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/J9JavacoreParserTest.java b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/J9JavacoreParserTest.java index cab2a9a457e..bfa122b0f2e 100644 --- a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/J9JavacoreParserTest.java +++ b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/J9JavacoreParserTest.java @@ -212,6 +212,23 @@ public void testDateTimeParsing() throws Exception { "Expected ISO-8601 format, got: " + crashLog.timestamp); } + @TableTest({ + "scenario | filename | expectedJreVersion ", + "OpenJ9 11 GPF | sample-j9-javacore-gpf.txt | JRE 11.0.12 Linux amd64-64 ", + "OpenJ9 17 OOM | sample-j9-javacore-oom.txt | JRE 17.0.6 Linux amd64-64 ", + "OpenJ9 11 aarch | sample-openj9-11-javacore-gpf.txt | JRE 11 Linux aarch64-64 (build 11.0.28+6) ", + "IBM J9 8 | sample-ibmj9-8-javacore-gpf.txt | JRE 1.8.0 Linux amd64-64 (build 8.0.8.51 - pxa6480sr8fp51-20250819_01(SR8 FP51))" + }) + public void testRuntimeInfoParsing(String filename, String expectedJreVersion) throws Exception { + CrashLog crashLog = + new J9JavacoreParser().parse(UUID.randomUUID().toString(), readFileAsString(filename)); + + assertNotNull(crashLog.experimental, "experimental should be populated"); + assertNotNull(crashLog.experimental.runtimeInfo, "runtimeInfo should be populated"); + assertNotNull(crashLog.experimental.runtimeInfo.jreVersion, "jreVersion should be populated"); + assertEquals(expectedJreVersion, crashLog.experimental.runtimeInfo.jreVersion); + } + @Test public void testNoSignalProducesInternalError() throws Exception { // A javacore with a THREADS section but no 1TISIGINFO line