Skip to content

Add profiler env check command to AgentCLI #8671

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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: <path>/dd_ome_notifier.(sh|bat) %p. OOME tracking is disabled.",
"OOME notifier script value ({}) does not follow the expected format: <path>/dd_oome_notifier.(sh|bat) %p. OOME tracking is disabled.",
onOutOfMemoryVal);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<File> invoker, File origin)
throws IOException {
invoker.accept(origin);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will make it easier to diagnose these issue. Thanks!

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<Path>() {
@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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 <path> ...");
System.out.println(" checkProfilerEnv [temp]");
System.out.println(" [-li | --list-integrations]");
System.out.println(" [-h | --help]");
System.out.println(" [-v | --version]");
Expand Down Expand Up @@ -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);
}
}
96 changes: 96 additions & 0 deletions internal-api/src/main/java/datadog/trace/api/Platform.java
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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")
Expand Down