From 9c29d5572f1d12570a0e8d536e9152688ddbf4ee Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Fri, 4 Apr 2025 17:01:50 +0200 Subject: [PATCH] Add profiler env check command to AgentCLI --- .../OOMENotifierScriptInitializer.java | 2 +- .../datadog/trace/agent/tooling/AgentCLI.java | 7 + .../tooling/profiler/EnvironmentChecker.java | 229 ++++++++++++++++++ .../datadog/trace/bootstrap/AgentJar.java | 10 + .../main/java/datadog/trace/api/Platform.java | 96 ++++++++ 5 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/profiler/EnvironmentChecker.java diff --git a/dd-java-agent/agent-crashtracking/src/main/java/com/datadog/crashtracking/OOMENotifierScriptInitializer.java b/dd-java-agent/agent-crashtracking/src/main/java/com/datadog/crashtracking/OOMENotifierScriptInitializer.java index 295eb8eecc6..66fc6fb5274 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/com/datadog/crashtracking/OOMENotifierScriptInitializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/com/datadog/crashtracking/OOMENotifierScriptInitializer.java @@ -43,7 +43,7 @@ static void initialize(String onOutOfMemoryVal) { Path scriptPath = getOOMEScripPath(onOutOfMemoryVal); if (scriptPath == null) { LOG.debug( - "OOME notifier script value ({}) does not follow the expected format: /dd_ome_notifier.(sh|bat) %p. OOME tracking is disabled.", + "OOME notifier script value ({}) does not follow the expected format: /dd_oome_notifier.(sh|bat) %p. OOME tracking is disabled.", onOutOfMemoryVal); return; } diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/AgentCLI.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/AgentCLI.java index 8ab26bb7698..9417d7bb4e8 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/AgentCLI.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/AgentCLI.java @@ -4,6 +4,7 @@ import com.datadog.crashtracking.OOMENotifier; import datadog.trace.agent.tooling.bytebuddy.SharedTypePools; import datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers; +import datadog.trace.agent.tooling.profiler.EnvironmentChecker; import datadog.trace.bootstrap.Agent; import datadog.trace.bootstrap.InitializationTelemetry; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; @@ -126,6 +127,12 @@ public static void scanDependencies(final String[] args) throws Exception { System.out.println("Scan finished"); } + public static void checkProfilerEnv(String temp) { + if (!EnvironmentChecker.checkEnvironment(temp)) { + System.exit(1); + } + } + private static void recursiveDependencySearch(Consumer invoker, File origin) throws IOException { invoker.accept(origin); diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/profiler/EnvironmentChecker.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/profiler/EnvironmentChecker.java new file mode 100644 index 00000000000..13f79ac4ad5 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/profiler/EnvironmentChecker.java @@ -0,0 +1,229 @@ +package datadog.trace.agent.tooling.profiler; + +import datadog.trace.api.Platform; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Set; +import java.util.jar.JarFile; + +public final class EnvironmentChecker { + public static boolean checkEnvironment(String temp) { + if (!Platform.isJavaVersionAtLeast(8)) { + System.out.println("Profiler requires Java 8 or newer"); + return false; + } + System.out.println( + "Using Java version: " + + Platform.getRuntimeVersion() + + " (" + + System.getProperty("java.home") + + ")"); + System.out.println("Running as user: " + System.getProperty("user.name")); + boolean result = false; + result |= checkJFR(); + result |= checkDdprof(); + if (!result) {; + System.out.println("Profiler is not supported on this JVM."); + return false; + } else { + System.out.println("Profiler is supported on this JVM."); + } + System.out.println(); + if (!checkTempLocation(temp)) { + System.out.println( + "Profiler will not work properly due to issues with temp directory location."); + return false; + } else { + if (!temp.equals(System.getProperty("java.io.tmpdir"))) { + System.out.println( + "! Make sure to add '-Ddd.profiling.tempdir=" + temp + "' to your JVM command line !"); + } + } + System.out.println("Profiler is ready to be used."); + return true; + } + + private static boolean checkJFR() { + if (Platform.isOracleJDK8()) { + System.out.println( + "JFR is commercial feature in Oracle JDK 8. Make sure you have the right license."); + return true; + } else if (Platform.isJ9()) { + System.out.println("JFR is not supported on J9 JVM."); + return false; + } else { + System.out.println("JFR is supported on " + Platform.getRuntimeVersion()); + return true; + } + } + + private static boolean checkDdprof() { + if (!Platform.isLinux()) { + System.out.println("Datadog profiler is only supported on Linux."); + return false; + } else { + System.out.println("Datadog profiler is supported on " + Platform.getRuntimeVersion()); + return true; + } + } + + private static boolean checkTempLocation(String temp) { + // Check if the temp directory is writable + if (temp == null || temp.isEmpty()) { + System.out.println("Temp directory is not specified."); + return false; + } + + System.out.println("Checking temporary directory: " + temp); + + Path base = Paths.get(temp); + if (!Files.exists(base)) { + System.out.println("Temporary directory does not exist: " + base); + return false; + } + Path target = base.resolve("dd-profiler").normalize(); + boolean rslt = true; + Set supportedViews = FileSystems.getDefault().supportedFileAttributeViews(); + boolean isPosix = supportedViews.contains("posix"); + try { + if (isPosix) { + Files.createDirectories( + target, + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); + } else { + // non-posix, eg. Windows - let's rely on the created folders being world-writable + Files.createDirectories(target); + } + System.out.println("Temporary directory is writable: " + target); + rslt &= checkCreateTempFile(target); + rslt &= checkLoadLibrary(target); + } catch (Exception e) { + System.out.println("Unable to create temp directory in location " + temp); + if (isPosix) { + try { + System.out.println( + "Base dir: " + + base + + " [" + + PosixFilePermissions.toString(Files.getPosixFilePermissions(base)) + + "]"); + } catch (IOException ignored) { + // never happens + } + } + System.out.println("Error: " + e); + } finally { + if (Files.exists(target)) { + try { + Files.walkFileTree( + target, + new FileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) + throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) + throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException ignored) { + // should never happen + } + } + } + return rslt; + } + + private static boolean checkCreateTempFile(Path target) { + // create a file to check if the directory is writable + try { + System.out.println("Attempting to create a test file in: " + target); + Path testFile = target.resolve("testfile"); + Files.createFile(testFile); + System.out.println("Test file created: " + testFile); + return true; + } catch (Exception e) { + System.out.println("Unable to create test file in temp directory " + target); + System.out.println("Error: " + e); + } + return false; + } + + private static boolean checkLoadLibrary(Path target) { + if (!Platform.isLinux()) { + // we are loading the native library only on linux + System.out.println("Skipping native library check on non-linux platform"); + return true; + } + boolean rslt = true; + try { + rslt &= extractSoFromJar(target); + if (rslt) { + Path libFile = target.resolve("libjavaProfiler.so"); + System.out.println("Attempting to load native library from: " + libFile); + System.load(libFile.toString()); + System.out.println("Native library loaded successfully"); + } + return true; + } catch (Throwable t) { + System.out.println("Unable to load native library in temp directory " + target); + System.out.println("Error: " + t); + return false; + } + } + + private static boolean extractSoFromJar(Path target) throws Exception { + URL jarUrl = EnvironmentChecker.class.getProtectionDomain().getCodeSource().getLocation(); + try (JarFile jarFile = new JarFile(new File(jarUrl.toURI()))) { + return jarFile.stream() + .filter(e -> e.getName().contains("libjavaProfiler.so")) + .filter( + e -> + e.getName().contains(Platform.isAarch64() ? "/linux-arm64/" : "/linux-x64/") + && (!Platform.isMusl() || e.getName().contains("-musl"))) + .findFirst() + .map( + e -> { + try { + Path soFile = target.resolve("libjavaProfiler.so"); + Files.createDirectories(soFile.getParent()); + Files.copy(jarFile.getInputStream(e), soFile); + System.out.println("Native library extracted to: " + soFile); + return true; + } catch (Throwable t) { + System.out.println("Failed to extract or load native library"); + System.out.println("Error: " + t); + } + return false; + }) + .orElse(Boolean.FALSE); + } + } +} diff --git a/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentJar.java b/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentJar.java index 95b3d757bc3..87da58f8426 100644 --- a/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentJar.java +++ b/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentJar.java @@ -33,6 +33,9 @@ public static void main(final String[] args) { case "scanDependencies": scanDependencies(args); break; + case "checkProfilerEnv": + checkProfilerEnv(args); + break; case "--list-integrations": case "-li": printIntegrationNames(); @@ -63,6 +66,7 @@ private static void printUsage() { System.out.println(" sampleTrace [-c count] [-i interval]"); System.out.println(" uploadCrash file ..."); System.out.println(" scanDependencies ..."); + System.out.println(" checkProfilerEnv [temp]"); System.out.println(" [-li | --list-integrations]"); System.out.println(" [-h | --help]"); System.out.println(" [-v | --version]"); @@ -164,4 +168,10 @@ public static String getAgentVersion() throws IOException { return sb.toString().trim(); } + + private static void checkProfilerEnv(final String[] args) throws Exception { + String tmpDir = args.length == 2 ? args[1] : System.getProperty("java.io.tmpdir"); + + installAgentCLI().getMethod("checkProfilerEnv", String.class).invoke(null, tmpDir); + } } diff --git a/internal-api/src/main/java/datadog/trace/api/Platform.java b/internal-api/src/main/java/datadog/trace/api/Platform.java index c1ed2b539d0..8d01b60b861 100644 --- a/internal-api/src/main/java/datadog/trace/api/Platform.java +++ b/internal-api/src/main/java/datadog/trace/api/Platform.java @@ -1,6 +1,14 @@ package datadog.trace.api; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +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.List; import java.util.Locale; @@ -276,6 +284,94 @@ public static boolean isMac() { return os.contains("mac"); } + public static boolean isAarch64() { + return System.getProperty("os.arch").toLowerCase().contains("aarch64"); + } + + public static boolean isMusl() { + if (!isLinux()) { + return false; + } + // check the Java exe then fall back to proc/self maps + try { + return isMuslJavaExecutable(); + } catch (IOException e) { + try { + return isMuslProcSelfMaps(); + } catch (IOException ignore) { + return false; + } + } + } + + static boolean isMuslProcSelfMaps() throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader("/proc/self/maps"))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains("-musl-")) { + return true; + } + if (line.contains("/libc.")) { + return false; + } + } + } + return false; + } + + /** + * There is information about the linking in the ELF file. Since properly parsing ELF is not + * trivial this code will attempt a brute-force approach and will scan the first 4096 bytes of the + * 'java' program image for anything prefixed with `/ld-` - in practice this will contain + * `/ld-musl` for musl systems and probably something else for non-musl systems (eg. + * `/ld-linux-...`). However, if such string is missing should indicate that the system is not a + * musl one. + */ + static boolean isMuslJavaExecutable() throws IOException { + + byte[] magic = new byte[] {(byte) 0x7f, (byte) 'E', (byte) 'L', (byte) 'F'}; + byte[] prefix = new byte[] {(byte) '/', (byte) 'l', (byte) 'd', (byte) '-'}; // '/ld-*' + byte[] musl = new byte[] {(byte) 'm', (byte) 'u', (byte) 's', (byte) 'l'}; // 'musl' + + Path binary = Paths.get(System.getProperty("java.home"), "bin", "java"); + byte[] buffer = new byte[4096]; + + try (InputStream is = Files.newInputStream(binary)) { + int read = is.read(buffer, 0, 4); + if (read != 4 || !containsArray(buffer, 0, magic)) { + throw new IOException(Arrays.toString(buffer)); + } + read = is.read(buffer); + if (read <= 0) { + throw new IOException(); + } + int prefixPos = 0; + for (int i = 0; i < read; i++) { + if (buffer[i] == prefix[prefixPos]) { + if (++prefixPos == prefix.length) { + return containsArray(buffer, i + 1, musl); + } + } else { + prefixPos = 0; + } + } + } + return false; + } + + private static boolean containsArray(byte[] container, int offset, byte[] contained) { + for (int i = 0; i < contained.length; i++) { + int leftPos = offset + i; + if (leftPos >= container.length) { + return false; + } + if (container[leftPos] != contained[i]) { + return false; + } + } + return true; + } + public static boolean isOracleJDK8() { return isJavaVersion(8) && RUNTIME.vendor.contains("Oracle")