Skip to content

Commit 6b9c1c9

Browse files
committed
Add profiler env check command to AgentCLI
1 parent 8adba3f commit 6b9c1c9

File tree

5 files changed

+337
-1
lines changed

5 files changed

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

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)