From bfe5a1fc9433e82a8a5a0071dcfe3c632f2844ed Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Mar 2025 10:37:35 -0400 Subject: [PATCH 1/6] Expose & use jvmstat for JDK 9+ programmatically Squashed 12 commits --- .../java/datadog/trace/bootstrap/Agent.java | 17 ++++++++++ .../profiling-utils/build.gradle | 2 ++ .../JFRBasedProfilingIntegrationTest.java | 2 ++ internal-api/build.gradle | 1 + internal-api/internal-api-9/build.gradle | 4 +++ .../datadog/trace/util/JPMSJPSAccess.java | 26 +++++++++++++++ .../java/datadog/trace/util/JPSUtils.java | 33 +++++++++++++++++++ .../java/datadog/trace/util/PidHelper.java | 5 +++ 8 files changed, 90 insertions(+) create mode 100644 internal-api/internal-api-9/src/main_java11/java/datadog/trace/util/JPMSJPSAccess.java create mode 100644 internal-api/src/main/java/datadog/trace/util/JPSUtils.java diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index e55d6e99ef9..7e2c9969d27 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -42,6 +42,7 @@ import datadog.trace.util.AgentTaskScheduler; import datadog.trace.util.AgentThreadFactory.AgentThread; import datadog.trace.util.throwable.FatalAgentMisconfigurationError; +import de.thetaphi.forbiddenapis.SuppressForbidden; import java.lang.instrument.Instrumentation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -278,6 +279,8 @@ public static void start( codeOriginEnabled = isFeatureEnabled(AgentFeature.CODE_ORIGIN); agentlessLogSubmissionEnabled = isFeatureEnabled(AgentFeature.AGENTLESS_LOG_SUBMISSION); + patchJPSAccess(inst); + if (profilingEnabled) { if (!isOracleJDK8()) { // Profiling agent startup code is written in a way to allow `startProfilingAgent` be called @@ -407,6 +410,20 @@ private static void injectAgentArgsConfig(String agentArgs) { } } + @SuppressForbidden + public static void patchJPSAccess(Instrumentation inst) { + if (Platform.isJavaVersionAtLeast(9)) { + // Unclear if supported for J9, may need to revisit + try { + Class.forName("datadog.trace.util.JPMSJPSAccess") + .getMethod("patchModuleAccess") + .invoke(inst); + } catch (Exception e) { + log.warn("Failed to patch module access for jvmstat"); + } + } + } + public static void shutdown(final boolean sync) { StaticEventLogger.end("Agent"); StaticEventLogger.stop(); diff --git a/dd-java-agent/agent-profiling/profiling-utils/build.gradle b/dd-java-agent/agent-profiling/profiling-utils/build.gradle index e482421df9e..38957666946 100644 --- a/dd-java-agent/agent-profiling/profiling-utils/build.gradle +++ b/dd-java-agent/agent-profiling/profiling-utils/build.gradle @@ -1,5 +1,7 @@ // Set properties before any plugins get loaded ext { + // In order to patch access for jvmstat for JDK >8 + minJavaVersionForTests = JavaVersion.VERSION_11 } apply from: "$rootDir/gradle/java.gradle" diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/JFRBasedProfilingIntegrationTest.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/JFRBasedProfilingIntegrationTest.java index 6402187aa49..e203d11565c 100644 --- a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/JFRBasedProfilingIntegrationTest.java +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/JFRBasedProfilingIntegrationTest.java @@ -507,6 +507,8 @@ void testShutdown(final TestInfo testInfo) throws Exception { assertTrue( targetProcess.waitFor( duration + PROFILING_UPLOAD_TIMEOUT_SECONDS + 1, TimeUnit.SECONDS)); + assertTrue( + checkLogLines(logFilePath, it -> it.contains("Successfully invoked jvmstat"))); } finally { if (targetProcess != null) { targetProcess.destroyForcibly(); diff --git a/internal-api/build.gradle b/internal-api/build.gradle index 9d0107ae991..3adf6961a1a 100644 --- a/internal-api/build.gradle +++ b/internal-api/build.gradle @@ -175,6 +175,7 @@ excludedClassesCoverage += [ "datadog.trace.util.ComparableVersion.LongItem", "datadog.trace.util.ComparableVersion.StringItem", "datadog.trace.util.ConcurrentEnumMap", + "datadog.trace.util.JPSUtils", "datadog.trace.util.MethodHandles", "datadog.trace.util.PidHelper", "datadog.trace.util.PidHelper.Fallback", diff --git a/internal-api/internal-api-9/build.gradle b/internal-api/internal-api-9/build.gradle index 5d1026791f6..0d9ff1869a0 100644 --- a/internal-api/internal-api-9/build.gradle +++ b/internal-api/internal-api-9/build.gradle @@ -41,6 +41,10 @@ forbiddenApisMain { failOnMissingClasses = false } +forbiddenApisMain_java11 { + failOnMissingClasses = false +} + idea { module { jdkName = '11' diff --git a/internal-api/internal-api-9/src/main_java11/java/datadog/trace/util/JPMSJPSAccess.java b/internal-api/internal-api-9/src/main_java11/java/datadog/trace/util/JPMSJPSAccess.java new file mode 100644 index 00000000000..bc93b958ef2 --- /dev/null +++ b/internal-api/internal-api-9/src/main_java11/java/datadog/trace/util/JPMSJPSAccess.java @@ -0,0 +1,26 @@ +package datadog.trace.util; + +import java.lang.instrument.Instrumentation; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +public class JPMSJPSAccess { + public static void patchModuleAccess(Instrumentation inst) { + Module unnamedModule = JPMSJPSAccess.class.getClassLoader().getUnnamedModule(); + Module jvmstatModule = ModuleLayer.boot().findModule("jdk.internal.jvmstat").orElse(null); + + if (jvmstatModule != null) { + Map> extraOpens = Map.of("sun.jvmstat.monitor", Set.of(unnamedModule)); + + // Redefine the module + inst.redefineModule( + jvmstatModule, + Collections.emptySet(), + extraOpens, + extraOpens, + Collections.emptySet(), + Collections.emptyMap()); + } + } +} diff --git a/internal-api/src/main/java/datadog/trace/util/JPSUtils.java b/internal-api/src/main/java/datadog/trace/util/JPSUtils.java new file mode 100644 index 00000000000..eb634ad2420 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/util/JPSUtils.java @@ -0,0 +1,33 @@ +package datadog.trace.util; + +import de.thetaphi.forbiddenapis.SuppressForbidden; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class JPSUtils { + private static final Logger log = LoggerFactory.getLogger(JPSUtils.class); + + @SuppressForbidden + public static Set getVMPids() { + Set vmPids = new HashSet<>(); + try { + Class monitoredHostClass = Class.forName("sun.jvmstat.monitor.MonitoredHost"); + Method getMonitoredHostMethod = + monitoredHostClass.getDeclaredMethod("getMonitoredHost", String.class); + Object vmHost = getMonitoredHostMethod.invoke(null, "localhost"); + for (Integer vmPid : + (List) monitoredHostClass.getDeclaredMethod("activeVms").invoke(vmHost)) { + vmPids.add(vmPid.toString()); + } + log.debug("Successfully invoked jvmstat"); + } catch (Exception e) { + log.debug("Failed to invoke jvmstat with exception ", e); + return null; + } + return vmPids; + } +} diff --git a/internal-api/src/main/java/datadog/trace/util/PidHelper.java b/internal-api/src/main/java/datadog/trace/util/PidHelper.java index 4121d9da68d..dfa6276b879 100644 --- a/internal-api/src/main/java/datadog/trace/util/PidHelper.java +++ b/internal-api/src/main/java/datadog/trace/util/PidHelper.java @@ -65,6 +65,11 @@ private static String findPid() { } public static Set getJavaPids() { + // Attempt to use jvmstat directly, fall through to jps process fork strategy + Set directlyObtainedPids = JPSUtils.getVMPids(); + if (directlyObtainedPids != null) { + return directlyObtainedPids; + } // there is no supported Java API to achieve this // one could use sun.jvmstat.monitor.MonitoredHost but it is an internal API and can go away at // any time - From e5a415fd752e0a61b8893af802854cdedb8dee8b Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 8 Apr 2025 13:52:19 -0400 Subject: [PATCH 2/6] Revert unnecessary dependency on JDK 11 for profiling tests --- dd-java-agent/agent-profiling/profiling-utils/build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/dd-java-agent/agent-profiling/profiling-utils/build.gradle b/dd-java-agent/agent-profiling/profiling-utils/build.gradle index 38957666946..e482421df9e 100644 --- a/dd-java-agent/agent-profiling/profiling-utils/build.gradle +++ b/dd-java-agent/agent-profiling/profiling-utils/build.gradle @@ -1,7 +1,5 @@ // Set properties before any plugins get loaded ext { - // In order to patch access for jvmstat for JDK >8 - minJavaVersionForTests = JavaVersion.VERSION_11 } apply from: "$rootDir/gradle/java.gradle" From 388e64627988a475aa37893ecb274f296a0ad9d2 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 9 Apr 2025 09:04:15 -0400 Subject: [PATCH 3/6] Send telemetry about jvmstat / jps usage --- .../src/main/java/datadog/trace/bootstrap/Agent.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index 7e2c9969d27..c092686ce7e 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -3,6 +3,7 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_STARTUP_LOGS_ENABLED; import static datadog.trace.api.Platform.isJavaVersionAtLeast; import static datadog.trace.api.Platform.isOracleJDK8; +import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; import static datadog.trace.bootstrap.Library.WILDFLY; import static datadog.trace.bootstrap.Library.detectLibraries; import static datadog.trace.util.AgentThreadFactory.AgentThread.JMX_STARTUP; @@ -419,7 +420,10 @@ public static void patchJPSAccess(Instrumentation inst) { .getMethod("patchModuleAccess") .invoke(inst); } catch (Exception e) { - log.warn("Failed to patch module access for jvmstat"); + log.debug( + SEND_TELEMETRY, + "Failed to patch module access for jvmstat and Java version " + + Platform.getRuntimeVersion()); } } } From 62e3ee88155bd5cd127293993e66a5b0ea9835fd Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 9 Apr 2025 11:02:21 -0400 Subject: [PATCH 4/6] Address internal inconsistency & test instability Squashed 3 commits --- .../src/main/java/datadog/trace/bootstrap/Agent.java | 4 ++-- .../smoketest/JFRBasedProfilingIntegrationTest.java | 2 -- internal-api/internal-api-9/build.gradle | 4 ---- .../java/datadog/trace/util/JPMSJPSAccess.java | 0 .../groovy/datadog/trace/util/PidHelperTest.groovy | 10 ++++++++++ .../src/main/java/datadog/trace/util/JPSUtils.java | 10 +--------- 6 files changed, 13 insertions(+), 17 deletions(-) rename internal-api/internal-api-9/src/{main_java11 => main}/java/datadog/trace/util/JPMSJPSAccess.java (100%) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index 04d02c1465b..ded84aa1176 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -430,8 +430,8 @@ public static void patchJPSAccess(Instrumentation inst) { // Unclear if supported for J9, may need to revisit try { Class.forName("datadog.trace.util.JPMSJPSAccess") - .getMethod("patchModuleAccess") - .invoke(inst); + .getMethod("patchModuleAccess", Instrumentation.class) + .invoke(null, inst); } catch (Exception e) { log.debug( SEND_TELEMETRY, diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/JFRBasedProfilingIntegrationTest.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/JFRBasedProfilingIntegrationTest.java index feffe5e5fe4..02d49e29961 100644 --- a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/JFRBasedProfilingIntegrationTest.java +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/JFRBasedProfilingIntegrationTest.java @@ -507,8 +507,6 @@ void testShutdown(final TestInfo testInfo) throws Exception { assertTrue( targetProcess.waitFor( duration + PROFILING_UPLOAD_TIMEOUT_SECONDS + 1, TimeUnit.SECONDS)); - assertTrue( - checkLogLines(logFilePath, it -> it.contains("Successfully invoked jvmstat"))); } finally { if (targetProcess != null) { targetProcess.destroyForcibly(); diff --git a/internal-api/internal-api-9/build.gradle b/internal-api/internal-api-9/build.gradle index 0d9ff1869a0..5d1026791f6 100644 --- a/internal-api/internal-api-9/build.gradle +++ b/internal-api/internal-api-9/build.gradle @@ -41,10 +41,6 @@ forbiddenApisMain { failOnMissingClasses = false } -forbiddenApisMain_java11 { - failOnMissingClasses = false -} - idea { module { jdkName = '11' diff --git a/internal-api/internal-api-9/src/main_java11/java/datadog/trace/util/JPMSJPSAccess.java b/internal-api/internal-api-9/src/main/java/datadog/trace/util/JPMSJPSAccess.java similarity index 100% rename from internal-api/internal-api-9/src/main_java11/java/datadog/trace/util/JPMSJPSAccess.java rename to internal-api/internal-api-9/src/main/java/datadog/trace/util/JPMSJPSAccess.java diff --git a/internal-api/internal-api-9/src/test/groovy/datadog/trace/util/PidHelperTest.groovy b/internal-api/internal-api-9/src/test/groovy/datadog/trace/util/PidHelperTest.groovy index 95f92c3a332..ac271ac29e1 100644 --- a/internal-api/internal-api-9/src/test/groovy/datadog/trace/util/PidHelperTest.groovy +++ b/internal-api/internal-api-9/src/test/groovy/datadog/trace/util/PidHelperTest.groovy @@ -1,6 +1,7 @@ package datadog.trace.util import datadog.trace.test.util.DDSpecification +import net.bytebuddy.agent.ByteBuddyAgent class PidHelperTest extends DDSpecification { @@ -8,4 +9,13 @@ class PidHelperTest extends DDSpecification { expect: !PidHelper.getPid().isEmpty() } + + def "JPS via jvmstat is used when possible"() { + when: + def inst = ByteBuddyAgent.install() + JPMSJPSAccess.patchModuleAccess(inst) + + then: + JPSUtils.VMPids != null + } } diff --git a/internal-api/src/main/java/datadog/trace/util/JPSUtils.java b/internal-api/src/main/java/datadog/trace/util/JPSUtils.java index eb634ad2420..24b6f176beb 100644 --- a/internal-api/src/main/java/datadog/trace/util/JPSUtils.java +++ b/internal-api/src/main/java/datadog/trace/util/JPSUtils.java @@ -2,8 +2,6 @@ import de.thetaphi.forbiddenapis.SuppressForbidden; import java.lang.reflect.Method; -import java.util.HashSet; -import java.util.List; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,21 +11,15 @@ public final class JPSUtils { @SuppressForbidden public static Set getVMPids() { - Set vmPids = new HashSet<>(); try { Class monitoredHostClass = Class.forName("sun.jvmstat.monitor.MonitoredHost"); Method getMonitoredHostMethod = monitoredHostClass.getDeclaredMethod("getMonitoredHost", String.class); Object vmHost = getMonitoredHostMethod.invoke(null, "localhost"); - for (Integer vmPid : - (List) monitoredHostClass.getDeclaredMethod("activeVms").invoke(vmHost)) { - vmPids.add(vmPid.toString()); - } - log.debug("Successfully invoked jvmstat"); + return (Set) monitoredHostClass.getDeclaredMethod("activeVms").invoke(vmHost); } catch (Exception e) { log.debug("Failed to invoke jvmstat with exception ", e); return null; } - return vmPids; } } From 2893834e1adb2f2b553657d5c077bcf632960456 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 9 Apr 2025 15:28:31 -0400 Subject: [PATCH 5/6] Retrieve unnamed module via platform class loader --- .../src/main/java/datadog/trace/util/JPMSJPSAccess.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal-api/internal-api-9/src/main/java/datadog/trace/util/JPMSJPSAccess.java b/internal-api/internal-api-9/src/main/java/datadog/trace/util/JPMSJPSAccess.java index bc93b958ef2..0052445856f 100644 --- a/internal-api/internal-api-9/src/main/java/datadog/trace/util/JPMSJPSAccess.java +++ b/internal-api/internal-api-9/src/main/java/datadog/trace/util/JPMSJPSAccess.java @@ -7,7 +7,7 @@ public class JPMSJPSAccess { public static void patchModuleAccess(Instrumentation inst) { - Module unnamedModule = JPMSJPSAccess.class.getClassLoader().getUnnamedModule(); + Module unnamedModule = ClassLoader.getPlatformClassLoader().getUnnamedModule(); Module jvmstatModule = ModuleLayer.boot().findModule("jdk.internal.jvmstat").orElse(null); if (jvmstatModule != null) { From 2ead3b7ab62505c65adc3c40c14ae86324a5175b Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 9 Apr 2025 16:12:39 -0400 Subject: [PATCH 6/6] Use system class loader for 17+ JDK support --- .../src/main/java/datadog/trace/util/JPMSJPSAccess.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal-api/internal-api-9/src/main/java/datadog/trace/util/JPMSJPSAccess.java b/internal-api/internal-api-9/src/main/java/datadog/trace/util/JPMSJPSAccess.java index 0052445856f..10fe58446b3 100644 --- a/internal-api/internal-api-9/src/main/java/datadog/trace/util/JPMSJPSAccess.java +++ b/internal-api/internal-api-9/src/main/java/datadog/trace/util/JPMSJPSAccess.java @@ -7,7 +7,7 @@ public class JPMSJPSAccess { public static void patchModuleAccess(Instrumentation inst) { - Module unnamedModule = ClassLoader.getPlatformClassLoader().getUnnamedModule(); + Module unnamedModule = ClassLoader.getSystemClassLoader().getUnnamedModule(); Module jvmstatModule = ModuleLayer.boot().findModule("jdk.internal.jvmstat").orElse(null); if (jvmstatModule != null) {