Skip to content

Commit a51f61a

Browse files
authored
Add profiler env check command to AgentCLI (#8671)
1 parent d1abd07 commit a51f61a

File tree

5 files changed

+343
-1
lines changed

5 files changed

+343
-1
lines changed

dd-java-agent/agent-crashtracking/src/main/java/com/datadog/crashtracking/OOMENotifierScriptInitializer.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ static void initialize(String onOutOfMemoryVal) {
4343
Path scriptPath = getOOMEScripPath(onOutOfMemoryVal);
4444
if (scriptPath == null) {
4545
LOG.debug(
46-
"OOME notifier script value ({}) does not follow the expected format: <path>/dd_ome_notifier.(sh|bat) %p. OOME tracking is disabled.",
46+
"OOME notifier script value ({}) does not follow the expected format: <path>/dd_oome_notifier.(sh|bat) %p. OOME tracking is disabled.",
4747
onOutOfMemoryVal);
4848
return;
4949
}

dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/AgentCLI.java

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.datadog.crashtracking.OOMENotifier;
55
import datadog.trace.agent.tooling.bytebuddy.SharedTypePools;
66
import datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers;
7+
import datadog.trace.agent.tooling.profiler.EnvironmentChecker;
78
import datadog.trace.bootstrap.Agent;
89
import datadog.trace.bootstrap.InitializationTelemetry;
910
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
@@ -126,6 +127,12 @@ public static void scanDependencies(final String[] args) throws Exception {
126127
System.out.println("Scan finished");
127128
}
128129

130+
public static void checkProfilerEnv(String temp) {
131+
if (!EnvironmentChecker.checkEnvironment(temp)) {
132+
System.exit(1);
133+
}
134+
}
135+
129136
private static void recursiveDependencySearch(Consumer<File> invoker, File origin)
130137
throws IOException {
131138
invoker.accept(origin);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package datadog.trace.agent.tooling.profiler;
2+
3+
import datadog.trace.api.Platform;
4+
import java.io.File;
5+
import java.io.IOException;
6+
import java.net.URL;
7+
import java.nio.file.FileSystems;
8+
import java.nio.file.FileVisitResult;
9+
import java.nio.file.FileVisitor;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.nio.file.Paths;
13+
import java.nio.file.attribute.BasicFileAttributes;
14+
import java.nio.file.attribute.PosixFilePermissions;
15+
import java.util.Set;
16+
import java.util.jar.JarFile;
17+
18+
public final class EnvironmentChecker {
19+
public static boolean checkEnvironment(String temp) {
20+
if (!Platform.isJavaVersionAtLeast(8)) {
21+
System.out.println("Profiler requires Java 8 or newer");
22+
return false;
23+
}
24+
System.out.println(
25+
"Using Java version: "
26+
+ Platform.getRuntimeVersion()
27+
+ " ("
28+
+ System.getProperty("java.home")
29+
+ ")");
30+
System.out.println("Running as user: " + System.getProperty("user.name"));
31+
boolean result = false;
32+
result |= checkJFR();
33+
result |= checkDdprof();
34+
if (!result) {;
35+
System.out.println("Profiler is not supported on this JVM.");
36+
return false;
37+
} else {
38+
System.out.println("Profiler is supported on this JVM.");
39+
}
40+
System.out.println();
41+
if (!checkTempLocation(temp)) {
42+
System.out.println(
43+
"Profiler will not work properly due to issues with temp directory location.");
44+
return false;
45+
} else {
46+
if (!temp.equals(System.getProperty("java.io.tmpdir"))) {
47+
System.out.println(
48+
"! Make sure to add '-Ddd.profiling.tempdir=" + temp + "' to your JVM command line !");
49+
}
50+
}
51+
System.out.println("Profiler is ready to be used.");
52+
return true;
53+
}
54+
55+
private static boolean checkJFR() {
56+
if (Platform.isOracleJDK8()) {
57+
System.out.println(
58+
"JFR is commercial feature in Oracle JDK 8. Make sure you have the right license.");
59+
return true;
60+
} else if (Platform.isJ9()) {
61+
System.out.println("JFR is not supported on J9 JVM.");
62+
return false;
63+
} else {
64+
System.out.println("JFR is supported on " + Platform.getRuntimeVersion());
65+
return true;
66+
}
67+
}
68+
69+
private static boolean checkDdprof() {
70+
if (!Platform.isLinux()) {
71+
System.out.println("Datadog profiler is only supported on Linux.");
72+
return false;
73+
} else {
74+
System.out.println("Datadog profiler is supported on " + Platform.getRuntimeVersion());
75+
return true;
76+
}
77+
}
78+
79+
private static boolean checkTempLocation(String temp) {
80+
// Check if the temp directory is writable
81+
if (temp == null || temp.isEmpty()) {
82+
System.out.println("Temp directory is not specified.");
83+
return false;
84+
}
85+
86+
System.out.println("Checking temporary directory: " + temp);
87+
88+
Path base = Paths.get(temp);
89+
if (!Files.exists(base)) {
90+
System.out.println("Temporary directory does not exist: " + base);
91+
return false;
92+
}
93+
Path target = base.resolve("dd-profiler").normalize();
94+
boolean rslt = true;
95+
Set<String> supportedViews = FileSystems.getDefault().supportedFileAttributeViews();
96+
boolean isPosix = supportedViews.contains("posix");
97+
try {
98+
if (isPosix) {
99+
Files.createDirectories(
100+
target,
101+
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
102+
} else {
103+
// non-posix, eg. Windows - let's rely on the created folders being world-writable
104+
Files.createDirectories(target);
105+
}
106+
System.out.println("Temporary directory is writable: " + target);
107+
rslt &= checkCreateTempFile(target);
108+
rslt &= checkLoadLibrary(target);
109+
} catch (Exception e) {
110+
System.out.println("Unable to create temp directory in location " + temp);
111+
if (isPosix) {
112+
try {
113+
System.out.println(
114+
"Base dir: "
115+
+ base
116+
+ " ["
117+
+ PosixFilePermissions.toString(Files.getPosixFilePermissions(base))
118+
+ "]");
119+
} catch (IOException ignored) {
120+
// never happens
121+
}
122+
}
123+
System.out.println("Error: " + e);
124+
} finally {
125+
if (Files.exists(target)) {
126+
try {
127+
Files.walkFileTree(
128+
target,
129+
new FileVisitor<Path>() {
130+
@Override
131+
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
132+
throws IOException {
133+
return FileVisitResult.CONTINUE;
134+
}
135+
136+
@Override
137+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
138+
throws IOException {
139+
Files.delete(file);
140+
return FileVisitResult.CONTINUE;
141+
}
142+
143+
@Override
144+
public FileVisitResult visitFileFailed(Path file, IOException exc)
145+
throws IOException {
146+
return FileVisitResult.CONTINUE;
147+
}
148+
149+
@Override
150+
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
151+
throws IOException {
152+
Files.delete(dir);
153+
return FileVisitResult.CONTINUE;
154+
}
155+
});
156+
} catch (IOException ignored) {
157+
// should never happen
158+
}
159+
}
160+
}
161+
return rslt;
162+
}
163+
164+
private static boolean checkCreateTempFile(Path target) {
165+
// create a file to check if the directory is writable
166+
try {
167+
System.out.println("Attempting to create a test file in: " + target);
168+
Path testFile = target.resolve("testfile");
169+
Files.createFile(testFile);
170+
System.out.println("Test file created: " + testFile);
171+
return true;
172+
} catch (Exception e) {
173+
System.out.println("Unable to create test file in temp directory " + target);
174+
System.out.println("Error: " + e);
175+
}
176+
return false;
177+
}
178+
179+
private static boolean checkLoadLibrary(Path target) {
180+
if (!Platform.isLinux()) {
181+
// we are loading the native library only on linux
182+
System.out.println("Skipping native library check on non-linux platform");
183+
return true;
184+
}
185+
boolean rslt = true;
186+
try {
187+
rslt &= extractSoFromJar(target);
188+
if (rslt) {
189+
Path libFile = target.resolve("libjavaProfiler.so");
190+
System.out.println("Attempting to load native library from: " + libFile);
191+
System.load(libFile.toString());
192+
System.out.println("Native library loaded successfully");
193+
}
194+
return true;
195+
} catch (Throwable t) {
196+
System.out.println("Unable to load native library in temp directory " + target);
197+
System.out.println("Error: " + t);
198+
return false;
199+
}
200+
}
201+
202+
private static boolean extractSoFromJar(Path target) throws Exception {
203+
URL jarUrl = EnvironmentChecker.class.getProtectionDomain().getCodeSource().getLocation();
204+
try (JarFile jarFile = new JarFile(new File(jarUrl.toURI()))) {
205+
return jarFile.stream()
206+
.filter(e -> e.getName().contains("libjavaProfiler.so"))
207+
.filter(
208+
e ->
209+
e.getName().contains(Platform.isAarch64() ? "/linux-arm64/" : "/linux-x64/")
210+
&& (!Platform.isMusl() || e.getName().contains("-musl")))
211+
.findFirst()
212+
.map(
213+
e -> {
214+
try {
215+
Path soFile = target.resolve("libjavaProfiler.so");
216+
Files.createDirectories(soFile.getParent());
217+
Files.copy(jarFile.getInputStream(e), soFile);
218+
System.out.println("Native library extracted to: " + soFile);
219+
return true;
220+
} catch (Throwable t) {
221+
System.out.println("Failed to extract or load native library");
222+
System.out.println("Error: " + t);
223+
}
224+
return false;
225+
})
226+
.orElse(Boolean.FALSE);
227+
}
228+
}
229+
}

dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentJar.java

+10
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public static void main(final String[] args) {
3333
case "scanDependencies":
3434
scanDependencies(args);
3535
break;
36+
case "checkProfilerEnv":
37+
checkProfilerEnv(args);
38+
break;
3639
case "--list-integrations":
3740
case "-li":
3841
printIntegrationNames();
@@ -63,6 +66,7 @@ private static void printUsage() {
6366
System.out.println(" sampleTrace [-c count] [-i interval]");
6467
System.out.println(" uploadCrash file ...");
6568
System.out.println(" scanDependencies <path> ...");
69+
System.out.println(" checkProfilerEnv [temp]");
6670
System.out.println(" [-li | --list-integrations]");
6771
System.out.println(" [-h | --help]");
6872
System.out.println(" [-v | --version]");
@@ -164,4 +168,10 @@ public static String getAgentVersion() throws IOException {
164168

165169
return sb.toString().trim();
166170
}
171+
172+
private static void checkProfilerEnv(final String[] args) throws Exception {
173+
String tmpDir = args.length == 2 ? args[1] : System.getProperty("java.io.tmpdir");
174+
175+
installAgentCLI().getMethod("checkProfilerEnv", String.class).invoke(null, tmpDir);
176+
}
167177
}

internal-api/src/main/java/datadog/trace/api/Platform.java

+96
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
package datadog.trace.api;
22

3+
import java.io.BufferedReader;
4+
import java.io.FileReader;
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.nio.file.Paths;
310
import java.util.ArrayList;
11+
import java.util.Arrays;
412
import java.util.List;
513
import java.util.Locale;
614

@@ -276,6 +284,94 @@ public static boolean isMac() {
276284
return os.contains("mac");
277285
}
278286

287+
public static boolean isAarch64() {
288+
return System.getProperty("os.arch").toLowerCase().contains("aarch64");
289+
}
290+
291+
public static boolean isMusl() {
292+
if (!isLinux()) {
293+
return false;
294+
}
295+
// check the Java exe then fall back to proc/self maps
296+
try {
297+
return isMuslJavaExecutable();
298+
} catch (IOException e) {
299+
try {
300+
return isMuslProcSelfMaps();
301+
} catch (IOException ignore) {
302+
return false;
303+
}
304+
}
305+
}
306+
307+
static boolean isMuslProcSelfMaps() throws IOException {
308+
try (BufferedReader reader = new BufferedReader(new FileReader("/proc/self/maps"))) {
309+
String line;
310+
while ((line = reader.readLine()) != null) {
311+
if (line.contains("-musl-")) {
312+
return true;
313+
}
314+
if (line.contains("/libc.")) {
315+
return false;
316+
}
317+
}
318+
}
319+
return false;
320+
}
321+
322+
/**
323+
* There is information about the linking in the ELF file. Since properly parsing ELF is not
324+
* trivial this code will attempt a brute-force approach and will scan the first 4096 bytes of the
325+
* 'java' program image for anything prefixed with `/ld-` - in practice this will contain
326+
* `/ld-musl` for musl systems and probably something else for non-musl systems (eg.
327+
* `/ld-linux-...`). However, if such string is missing should indicate that the system is not a
328+
* musl one.
329+
*/
330+
static boolean isMuslJavaExecutable() throws IOException {
331+
332+
byte[] magic = new byte[] {(byte) 0x7f, (byte) 'E', (byte) 'L', (byte) 'F'};
333+
byte[] prefix = new byte[] {(byte) '/', (byte) 'l', (byte) 'd', (byte) '-'}; // '/ld-*'
334+
byte[] musl = new byte[] {(byte) 'm', (byte) 'u', (byte) 's', (byte) 'l'}; // 'musl'
335+
336+
Path binary = Paths.get(System.getProperty("java.home"), "bin", "java");
337+
byte[] buffer = new byte[4096];
338+
339+
try (InputStream is = Files.newInputStream(binary)) {
340+
int read = is.read(buffer, 0, 4);
341+
if (read != 4 || !containsArray(buffer, 0, magic)) {
342+
throw new IOException(Arrays.toString(buffer));
343+
}
344+
read = is.read(buffer);
345+
if (read <= 0) {
346+
throw new IOException();
347+
}
348+
int prefixPos = 0;
349+
for (int i = 0; i < read; i++) {
350+
if (buffer[i] == prefix[prefixPos]) {
351+
if (++prefixPos == prefix.length) {
352+
return containsArray(buffer, i + 1, musl);
353+
}
354+
} else {
355+
prefixPos = 0;
356+
}
357+
}
358+
}
359+
return false;
360+
}
361+
362+
private static boolean containsArray(byte[] container, int offset, byte[] contained) {
363+
for (int i = 0; i < contained.length; i++) {
364+
int leftPos = offset + i;
365+
if (leftPos >= container.length) {
366+
return false;
367+
}
368+
if (container[leftPos] != contained[i]) {
369+
return false;
370+
}
371+
}
372+
return true;
373+
}
374+
279375
public static boolean isOracleJDK8() {
280376
return isJavaVersion(8)
281377
&& RUNTIME.vendor.contains("Oracle")

0 commit comments

Comments
 (0)