diff --git a/dd-java-agent/agent-bootstrap/src/main/resources/META-INF/native-image/com.datadoghq/dd-java-agent/reflect-config.json b/dd-java-agent/agent-bootstrap/src/main/resources/META-INF/native-image/com.datadoghq/dd-java-agent/reflect-config.json index 92c08c7d8db..3225d6dd59f 100644 --- a/dd-java-agent/agent-bootstrap/src/main/resources/META-INF/native-image/com.datadoghq/dd-java-agent/reflect-config.json +++ b/dd-java-agent/agent-bootstrap/src/main/resources/META-INF/native-image/com.datadoghq/dd-java-agent/reflect-config.json @@ -206,5 +206,17 @@ "fields": [ {"name": "consumerIndex", "allowUnsafeAccess": true} ] + }, + { + "name": "com.datadoghq.profiler.BufferWriter8", + "methods": [ + {"name": "", "parameterTypes": []} + ] + }, + { + "name": "com.datadoghq.profiler.BufferWriter9", + "methods": [ + {"name": "", "parameterTypes": []} + ] } ] 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 e72dfe63f56..73db82e962c 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.OperatingSystem; import datadog.libs.ddprof.DdprofLibraryLoader; +import datadog.trace.api.Platform; import datadog.trace.util.TempLocationManager; import java.io.IOException; import java.io.InputStream; @@ -76,7 +77,8 @@ public boolean setValue(String flagName, String value) { public static boolean initialize(boolean forceJmx) { try { FlagAccess access = null; - if (forceJmx) { + // Native images don't support the native ddprof library, use JMX instead + if (forceJmx || Platform.isNativeImage()) { access = new JMXFlagAccess(ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class)); } else { diff --git a/dd-java-agent/instrumentation/graal/graal-native-image-20.0/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java b/dd-java-agent/instrumentation/graal/graal-native-image-20.0/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java index 86eec9c6960..de9422d24d5 100644 --- a/dd-java-agent/instrumentation/graal/graal-native-image-20.0/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java +++ b/dd-java-agent/instrumentation/graal/graal-native-image-20.0/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java @@ -67,6 +67,10 @@ public static void onEnter(@Advice.Argument(value = 0, readOnly = false) String[ + "com.datadog.profiling.controller.openjdk.events.SmapEntryEvent:build_time," + "com.datadog.profiling.controller.openjdk.events.SmapEntryFactory$SmapParseErrorEvent:build_time," + "com.datadog.profiling.ddprof.JavaProfilerLoader:run_time," + + "com.datadoghq.profiler.ThreadContext:run_time," + + "com.datadoghq.profiler.BufferWriter:run_time," + + "com.datadoghq.profiler.BufferWriter8:run_time," + + "com.datadoghq.profiler.BufferWriter9:run_time," + "datadog.environment.JavaVirtualMachine:rerun," + "datadog.environment.OperatingSystem:rerun," + "datadog.environment.OperatingSystem$Architecture:rerun," diff --git a/dd-smoke-tests/quarkus-native/build.gradle b/dd-smoke-tests/quarkus-native/build.gradle index ee1f1e38dc0..89d43864d70 100644 --- a/dd-smoke-tests/quarkus-native/build.gradle +++ b/dd-smoke-tests/quarkus-native/build.gradle @@ -4,6 +4,7 @@ apply from: "$rootDir/gradle/java.gradle" dependencies { testImplementation project(':dd-smoke-tests') + testImplementation libs.bundles.jmc } // Check 'testJvm' gradle command parameter is GraalVM (e.g., -PtestJvm=graalvm21), // if not nothing is done @@ -40,7 +41,8 @@ if (testGraalvmVersion >= 17) { "-PappBuildDir=$appBuildDir", "-PapiJar=${project(':dd-trace-api').tasks.jar.archiveFile.get()}", "-Dquarkus.native.additional-build-args=-J-javaagent:${project(':dd-java-agent').tasks.shadowJar.archiveFile.get()}," + - "-J-Ddatadog.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd'T'HH:mm:ss.SSS'Z [dd.trace]',-march=native" + "-J-Ddatadog.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd'T'HH:mm:ss.SSS'Z [dd.trace]'," + + "-J-Ddd.profiling.enabled=true,-march=native" ) outputs.cacheIf { true } outputs.dir(appBuildDir) @@ -68,6 +70,7 @@ if (testGraalvmVersion >= 17) { tasks.withType(Test).configureEach { jvmArgs "-Ddatadog.smoketest.quarkus.native.executable=$appBuildDir/quarkus-native-smoketest--runner" + jvmArgs "-Ddd.profiling.enabled=true" } } else { tasks.withType(Test).configureEach { diff --git a/dd-smoke-tests/quarkus-native/src/test/groovy/datadog/smoketest/QuarkusNativeProfilingSmokeTest.groovy b/dd-smoke-tests/quarkus-native/src/test/groovy/datadog/smoketest/QuarkusNativeProfilingSmokeTest.groovy new file mode 100644 index 00000000000..3c471486f9a --- /dev/null +++ b/dd-smoke-tests/quarkus-native/src/test/groovy/datadog/smoketest/QuarkusNativeProfilingSmokeTest.groovy @@ -0,0 +1,89 @@ +package datadog.smoketest + +import okhttp3.Request +import org.openjdk.jmc.common.item.ItemFilters +import org.openjdk.jmc.flightrecorder.JfrLoaderToolkit +import org.openjdk.jmc.flightrecorder.internal.InvalidJfrFileException +import spock.lang.Shared +import spock.lang.TempDir +import spock.util.concurrent.PollingConditions + +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import java.util.concurrent.atomic.AtomicInteger + +/** + * Smoke test for profiling in Quarkus native images. + * + * This test validates that profiling works correctly when the Datadog agent is attached + * during GraalVM native-image compilation. The build will fail if any profiler classes + * have incorrect native-image configuration (e.g., missing reflection metadata or + * improper class initialization timing). + * + * Test validates: + * 1. Native image builds successfully with profiling enabled + * 2. Application starts without agent initialization errors + * 3. Profiler produces JFR files at runtime + */ +class QuarkusNativeProfilingSmokeTest extends QuarkusSlf4jSmokeTest { + + @Shared + @TempDir + Path testJfrDir + + @Override + protected List additionalArguments() { + return [ + "-Ddd.profiling.upload.period=1", + "-Ddd.profiling.start-force-first=true", + "-Ddd.profiling.debug.dump_path=${testJfrDir}".toString(), + // TODO: Remove this arg after JFR initialization is fixed on GraalVM 25. + // https://datadoghq.atlassian.net/browse/PROF-12742 + "-XX:StartFlightRecording=filename=${testJfrDir}/recording.jfr".toString(), + ] + } + + def "profiling works in native image"() { + setup: + String url = "http://localhost:${httpPort}/${endpointName}?id=1" + def conditions = new PollingConditions(initialDelay: 2, timeout: 10) + + when: + // Make a request to generate some activity for the profiler + def response = client.newCall(new Request.Builder().url(url).get().build()).execute() + + then: + response.code() == 200 + response.body().string() == "Hello 1!" + + // Wait for JFR files with execution samples to be generated + conditions.eventually { + assert countJfrsWithExecutionSamples() > 0 + } + } + + int countJfrsWithExecutionSamples() { + AtomicInteger jfrCount = new AtomicInteger(0) + Files.walkFileTree(testJfrDir, new SimpleFileVisitor() { + @Override + FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (file.toString().endsWith(".jfr")) { + try { + def events = JfrLoaderToolkit.loadEvents(file.toFile()) + if (events.apply(ItemFilters.type("jdk.ExecutionSample")).hasItems()) { + jfrCount.incrementAndGet() + return FileVisitResult.SKIP_SIBLINGS + } + } catch (InvalidJfrFileException ignored) { + // The recording captured at process exit might be incomplete + } + } + return FileVisitResult.CONTINUE + } + }) + return jfrCount.get() + } +} diff --git a/dd-smoke-tests/quarkus-native/src/test/groovy/datadog/smoketest/QuarkusNativeSmokeTest.groovy b/dd-smoke-tests/quarkus-native/src/test/groovy/datadog/smoketest/QuarkusNativeSmokeTest.groovy index 529e4e6869d..665f1f51e01 100644 --- a/dd-smoke-tests/quarkus-native/src/test/groovy/datadog/smoketest/QuarkusNativeSmokeTest.groovy +++ b/dd-smoke-tests/quarkus-native/src/test/groovy/datadog/smoketest/QuarkusNativeSmokeTest.groovy @@ -21,10 +21,15 @@ abstract class QuarkusNativeSmokeTest extends AbstractServerSmokeTest { "-Ddd.app.customlogmanager=true", "-Dquarkus.http.port=${httpPort}" ]) + command.addAll(additionalArguments()) ProcessBuilder processBuilder = new ProcessBuilder(command) processBuilder.directory(new File(buildDirectory)) } + protected List additionalArguments() { + return Collections.emptyList() + } + @Override File createTemporaryFile() { return new File("${buildDirectory}/tmp/trace-structure-quarkus-native.out")