diff --git a/components/environment/build.gradle.kts b/components/environment/build.gradle.kts
new file mode 100644
index 00000000000..e06feeb39fd
--- /dev/null
+++ b/components/environment/build.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+ `java-library`
+}
+
+apply(from = "$rootDir/gradle/java.gradle")
+
+extra.set("minimumInstructionCoverage", 0.7)
+val excludedClassesCoverage by extra {
+ listOf(
+ "datadog.environment.JavaVirtualMachine", // depends on OS and JVM vendor
+ "datadog.environment.JavaVirtualMachine.JvmOptionsHolder", // depends on OS and JVM vendor
+ "datadog.environment.JvmOptions", // depends on OS and JVM vendor
+ "datadog.environment.OperatingSystem", // depends on OS
+ )
+}
+val excludedClassesBranchCoverage by extra {
+ listOf("datadog.environment.CommandLine") // tested using forked process
+}
diff --git a/components/environment/src/main/java/datadog/environment/CommandLine.java b/components/environment/src/main/java/datadog/environment/CommandLine.java
new file mode 100644
index 00000000000..669f2fb94f1
--- /dev/null
+++ b/components/environment/src/main/java/datadog/environment/CommandLine.java
@@ -0,0 +1,45 @@
+package datadog.environment;
+
+import static java.util.Collections.emptyList;
+
+import de.thetaphi.forbiddenapis.SuppressForbidden;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Fetches and captures the command line, both command and its arguments. It relies on a
+ * non-standard {@code sun.java.command} system property and was tested on:
+ *
+ *
+ * - OracleJDK,
+ *
- OpenJDK,
+ *
- Temurin based JDK,
+ *
- IMB JDK,
+ *
- Azul Zulu,
+ *
- Amazon Coretto,
+ *
+ */
+class CommandLine {
+ private static final String SUN_JAVA_COMMAND_PROPERTY = "sun.java.command";
+ final List fullCommand = findFullCommand();
+ final String command = getCommand();
+ final List arguments = getCommandArguments();
+
+ @SuppressForbidden // split on single-character uses fast path
+ private List findFullCommand() {
+ String command = SystemProperties.getOrDefault(SUN_JAVA_COMMAND_PROPERTY, "").trim();
+ return command.isEmpty() ? emptyList() : Arrays.asList(command.split(" "));
+ }
+
+ private String getCommand() {
+ return fullCommand.isEmpty() ? null : fullCommand.get(0);
+ }
+
+ private List getCommandArguments() {
+ if (fullCommand.isEmpty()) {
+ return fullCommand;
+ } else {
+ return fullCommand.subList(1, fullCommand.size());
+ }
+ }
+}
diff --git a/components/environment/src/main/java/datadog/environment/EnvironmentVariables.java b/components/environment/src/main/java/datadog/environment/EnvironmentVariables.java
new file mode 100644
index 00000000000..96f60bd6c4e
--- /dev/null
+++ b/components/environment/src/main/java/datadog/environment/EnvironmentVariables.java
@@ -0,0 +1,41 @@
+package datadog.environment;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Safely queries environment variables against security manager.
+ *
+ * @see Security
+ * Manager
+ */
+public final class EnvironmentVariables {
+ private EnvironmentVariables() {}
+
+ /**
+ * Gets an environment variable value.
+ *
+ * @param name The environment variable name.
+ * @return The environment variable value, {@code null} if missing or can't be retrieved.
+ */
+ public static @Nullable String get(String name) {
+ return getOrDefault(name, null);
+ }
+
+ /**
+ * Gets an environment variable value, or default value if missing or can't be retrieved.
+ *
+ * @param name The environment variable name.
+ * @param defaultValue The default value to return if the environment variable is missing or can't
+ * be retrieved.
+ * @return The environment variable value, {@code defaultValue} if missing or can't be retrieved.
+ */
+ public static String getOrDefault(@Nonnull String name, String defaultValue) {
+ try {
+ String value = System.getenv(name);
+ return value == null ? defaultValue : value;
+ } catch (SecurityException e) {
+ return defaultValue;
+ }
+ }
+}
diff --git a/components/environment/src/main/java/datadog/environment/JavaVersion.java b/components/environment/src/main/java/datadog/environment/JavaVersion.java
new file mode 100644
index 00000000000..48aaed0c66e
--- /dev/null
+++ b/components/environment/src/main/java/datadog/environment/JavaVersion.java
@@ -0,0 +1,107 @@
+package datadog.environment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class represents a Java version according the String Naming Convention.
+ *
+ * @see String
+ * Naming Convention
+ */
+final class JavaVersion {
+ final int major;
+ final int minor;
+ final int update;
+
+ JavaVersion(int major, int minor, int update) {
+ this.major = major;
+ this.minor = minor;
+ this.update = update;
+ }
+
+ static JavaVersion getRuntimeVersion() {
+ return parseJavaVersion(SystemProperties.getOrDefault("java.version", ""));
+ }
+
+ static JavaVersion parseJavaVersion(String javaVersion) {
+ // Remove pre-release part, usually -ea
+ final int indexOfDash = javaVersion.indexOf('-');
+ if (indexOfDash >= 0) {
+ javaVersion = javaVersion.substring(0, indexOfDash);
+ }
+
+ int major = 0;
+ int minor = 0;
+ int update = 0;
+
+ try {
+ List nums = splitDigits(javaVersion);
+ major = nums.get(0);
+
+ // for java 1.6/1.7/1.8
+ if (major == 1) {
+ major = nums.get(1);
+ minor = nums.get(2);
+ update = nums.get(3);
+ } else {
+ minor = nums.get(1);
+ update = nums.get(2);
+ }
+ } catch (NumberFormatException | IndexOutOfBoundsException e) {
+ // unable to parse version string - do nothing
+ }
+ return new JavaVersion(major, minor, update);
+ }
+
+ /* The method splits java version string by digits. Delimiters are: dot, underscore and plus */
+ private static List splitDigits(String str) {
+ List results = new ArrayList<>();
+
+ int len = str.length();
+ int value = 0;
+ for (int i = 0; i < len; i++) {
+ char ch = str.charAt(i);
+ if (ch >= '0' && ch <= '9') {
+ value = value * 10 + (ch - '0');
+ } else if (ch == '.' || ch == '_' || ch == '+') {
+ results.add(value);
+ value = 0;
+ } else {
+ throw new NumberFormatException();
+ }
+ }
+ results.add(value);
+ return results;
+ }
+
+ public boolean is(int major) {
+ return this.major == major;
+ }
+
+ public boolean is(int major, int minor) {
+ return this.major == major && this.minor == minor;
+ }
+
+ public boolean is(int major, int minor, int update) {
+ return this.major == major && this.minor == minor && this.update == update;
+ }
+
+ public boolean isAtLeast(int major, int minor, int update) {
+ return isAtLeast(this.major, this.minor, this.update, major, minor, update);
+ }
+
+ public boolean isBetween(
+ int fromMajor, int fromMinor, int fromUpdate, int toMajor, int toMinor, int toUpdate) {
+ return isAtLeast(toMajor, toMinor, toUpdate, fromMajor, fromMinor, fromUpdate)
+ && isAtLeast(fromMajor, fromMinor, fromUpdate)
+ && !isAtLeast(toMajor, toMinor, toUpdate);
+ }
+
+ private static boolean isAtLeast(
+ int major, int minor, int update, int atLeastMajor, int atLeastMinor, int atLeastUpdate) {
+ return (major > atLeastMajor)
+ || (major == atLeastMajor && minor > atLeastMinor)
+ || (major == atLeastMajor && minor == atLeastMinor && update >= atLeastUpdate);
+ }
+}
diff --git a/components/environment/src/main/java/datadog/environment/JavaVirtualMachine.java b/components/environment/src/main/java/datadog/environment/JavaVirtualMachine.java
new file mode 100644
index 00000000000..fe884a81eb0
--- /dev/null
+++ b/components/environment/src/main/java/datadog/environment/JavaVirtualMachine.java
@@ -0,0 +1,212 @@
+package datadog.environment;
+
+import java.util.List;
+import java.util.Locale;
+import javax.annotation.Nullable;
+
+public final class JavaVirtualMachine {
+ private static final CommandLine commandLine = new CommandLine();
+ private static final JavaVersion javaVersion = JavaVersion.getRuntimeVersion();
+ private static final Runtime runtime = new Runtime();
+
+ private JavaVirtualMachine() {}
+
+ public static boolean isJavaVersion(int major) {
+ return javaVersion.is(major);
+ }
+
+ public static boolean isJavaVersion(int major, int minor) {
+ return javaVersion.is(major, minor);
+ }
+
+ public static boolean isJavaVersion(int major, int minor, int update) {
+ return javaVersion.is(major, minor, update);
+ }
+
+ public static boolean isJavaVersionAtLeast(int major) {
+ return isJavaVersionAtLeast(major, 0, 0);
+ }
+
+ public static boolean isJavaVersionAtLeast(int major, int minor) {
+ return isJavaVersionAtLeast(major, minor, 0);
+ }
+
+ public static boolean isJavaVersionAtLeast(int major, int minor, int update) {
+ return javaVersion.isAtLeast(major, minor, update);
+ }
+
+ /**
+ * Checks if the Java version is between {@code fromMajor} (inclusive) and {@code toMajor}
+ * (exclusive).
+ *
+ * @param fromMajor major from version (inclusive)
+ * @param toMajor major to version (exclusive)
+ * @return if the current java version is between the from version (inclusive) and the to version
+ * exclusive
+ */
+ public static boolean isJavaVersionBetween(int fromMajor, int toMajor) {
+ return isJavaVersionBetween(fromMajor, 0, toMajor, 0);
+ }
+
+ /**
+ * Checks if the Java version is between {@code fromMajor.fromMinor} (inclusive) and {@code
+ * toMajor.toMinor} (exclusive).
+ *
+ * @param fromMajor major from version (inclusive)
+ * @param fromMinor minor from version (inclusive)
+ * @param toMajor major to version (exclusive)
+ * @param toMinor minor to version (exclusive)
+ * @return if the current java version is between the from version (inclusive) and the to version
+ * exclusive
+ */
+ public static boolean isJavaVersionBetween(
+ int fromMajor, int fromMinor, int toMajor, int toMinor) {
+ return isJavaVersionBetween(fromMajor, fromMinor, 0, toMajor, toMinor, 0);
+ }
+
+ /**
+ * Checks if the Java version is between {@code fromMajor.fromMinor.fromUpdate} (inclusive) and
+ * {@code toMajor.toMinor.toUpdate} (exclusive).
+ *
+ * @param fromMajor major from version (inclusive)
+ * @param fromMinor minor from version (inclusive)
+ * @param fromUpdate update from version (inclusive)
+ * @param toMajor major to version (exclusive)
+ * @param toMinor minor to version (exclusive)
+ * @param toUpdate update to version (exclusive)
+ * @return if the current java version is between the from version (inclusive) and the to version
+ * exclusive
+ */
+ public static boolean isJavaVersionBetween(
+ int fromMajor, int fromMinor, int fromUpdate, int toMajor, int toMinor, int toUpdate) {
+ return javaVersion.isBetween(fromMajor, fromMinor, fromUpdate, toMajor, toMinor, toUpdate);
+ }
+
+ /**
+ * Checks whether the current JVM is an Oracle JDK 8.
+ *
+ * @return {@code true} if the current JVM is an Oracle JDK 8, {@code false} otherwise.
+ */
+ public static boolean isOracleJDK8() {
+ return isJavaVersion(8)
+ && runtime.vendor.contains("Oracle")
+ && !runtime.name.contains("OpenJDK");
+ }
+
+ public static boolean isJ9() {
+ return SystemProperties.getOrDefault("java.vm.name", "").contains("J9");
+ }
+
+ public static boolean isIbm8() {
+ return isJavaVersion(8) && runtime.vendor.contains("IBM");
+ }
+
+ public static boolean isGraalVM() {
+ return runtime.vendorVersion.toLowerCase().contains("graalvm");
+ }
+
+ public static String getLangVersion() {
+ return String.valueOf(javaVersion.major);
+ }
+
+ public static String getRuntimeVendor() {
+ return runtime.vendor;
+ }
+
+ public static String getRuntimeVersion() {
+ return runtime.version;
+ }
+
+ public static String getRuntimePatches() {
+ return runtime.patches;
+ }
+
+ /**
+ * Gets the JVM options.
+ *
+ * @return The JVM options, an empty collection if they can't be retrieved.
+ */
+ public static List getVmOptions() {
+ return JvmOptionsHolder.JVM_OPTIONS.VM_OPTIONS;
+ }
+
+ /**
+ * Gets the command arguments.
+ *
+ * @return The command arguments, an empty collection if missing or can't be retrieved.
+ */
+ public static List getCommandArguments() {
+ return commandLine.arguments;
+ }
+
+ /**
+ * Gets the JVM runtime main class name.
+ *
+ * @return The JVM runtime main class name, {@code null} if using JAR file instead or can't be
+ * retrieved.
+ */
+ public static @Nullable String getMainClass() {
+ return commandLine.command != null && !isJarName(commandLine.command)
+ ? commandLine.command
+ : null;
+ }
+
+ /**
+ * Gets the JVM runtime jar file.
+ *
+ * @return The JVM runtime jar file, {@code null} if using main class instead or can't be
+ * retrieved.
+ */
+ public static @Nullable String getJarFile() {
+ return commandLine.command != null && isJarName(commandLine.command)
+ ? commandLine.command
+ : null;
+ }
+
+ private static boolean isJarName(String argument) {
+ return argument.toLowerCase(Locale.ROOT).endsWith(".jar");
+ }
+
+ private static class JvmOptionsHolder {
+ private static final JvmOptions JVM_OPTIONS = new JvmOptions();
+ }
+
+ static final class Runtime {
+ /*
+ * Example:
+ * jvm -> "AdoptOpenJDK 1.8.0_265-b01"
+ *
+ * name -> "OpenJDK"
+ * vendor -> "AdoptOpenJDK"
+ * version -> "1.8.0_265"
+ * patches -> "b01"
+ */
+ public final String name;
+
+ public final String vendor;
+ public final String version;
+ public final String vendorVersion;
+ public final String patches;
+
+ public Runtime() {
+ this(
+ SystemProperties.get("java.version"),
+ SystemProperties.get("java.runtime.version"),
+ SystemProperties.get("java.runtime.name"),
+ SystemProperties.get("java.vm.vendor"),
+ SystemProperties.get("java.vendor.version"));
+ }
+
+ // Only visible for testing
+ Runtime(String javaVer, String rtVer, String name, String vendor, String vendorVersion) {
+ this.name = name == null ? "" : name;
+ this.vendor = vendor == null ? "" : vendor;
+ javaVer = javaVer == null ? "" : javaVer;
+ this.version = javaVer;
+ this.vendorVersion = vendorVersion == null ? "" : vendorVersion;
+ rtVer = javaVer.isEmpty() || rtVer == null ? javaVer : rtVer;
+ int patchStart = javaVer.length() + 1;
+ this.patches = (patchStart >= rtVer.length()) ? "" : rtVer.substring(javaVer.length() + 1);
+ }
+ }
+}
diff --git a/components/environment/src/main/java/datadog/environment/JvmOptions.java b/components/environment/src/main/java/datadog/environment/JvmOptions.java
new file mode 100644
index 00000000000..90250d4b323
--- /dev/null
+++ b/components/environment/src/main/java/datadog/environment/JvmOptions.java
@@ -0,0 +1,211 @@
+package datadog.environment;
+
+import static datadog.environment.OperatingSystem.isLinux;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+
+import de.thetaphi.forbiddenapis.SuppressForbidden;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.Field;
+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.ListIterator;
+import java.util.StringTokenizer;
+
+/** Fetches and captures the JVM options. */
+class JvmOptions {
+ final String[] PROCFS_CMDLINE = readProcFsCmdLine();
+ final List VM_OPTIONS = findVmOptions();
+
+ // static {
+ // RuntimeException runtimeException = new RuntimeException();
+ // runtimeException.printStackTrace(System.out);
+ // }
+
+ @SuppressForbidden // split on single-character uses fast path
+ private String[] readProcFsCmdLine() {
+ if (!isLinux()) {
+ try {
+ Path cmdlinePath = Paths.get("/proc/self/cmdline");
+ if (Files.exists(cmdlinePath) && Files.isReadable(cmdlinePath)) {
+ try (BufferedReader in = Files.newBufferedReader(cmdlinePath)) {
+ return in.readLine().split("\0");
+ }
+ }
+ } catch (Throwable ignored) {
+ }
+ }
+ return null;
+ }
+
+ @SuppressForbidden // Class.forName() as backup
+ private List findVmOptions() {
+ // Try ProcFS on Linux
+ if (PROCFS_CMDLINE != null) {
+ // Start at 1 to skip "java" command itself
+ int index = 1;
+ // Look for main class or "-jar", end of VM options
+ for (; index < PROCFS_CMDLINE.length; index++) {
+ if (!PROCFS_CMDLINE[index].startsWith("-") || "-jar".equals(PROCFS_CMDLINE[index])) {
+ break;
+ }
+ }
+ // Create list of VM options
+ String[] vmOptionsArray = new String[index - 1];
+ System.arraycopy(PROCFS_CMDLINE, 1, vmOptionsArray, 0, vmOptionsArray.length);
+ List vmOptions = Arrays.asList(vmOptionsArray);
+ // Substitute @argfile by their content
+ ListIterator iterator = vmOptions.listIterator();
+ while (iterator.hasNext()) {
+ String vmOption = iterator.next();
+ if (vmOption.startsWith("@")) {
+ iterator.remove();
+ for (String argument : getArgumentsFromFile(vmOption)) {
+ iterator.add(argument);
+ }
+ }
+ }
+ // Insert JAVA_TOOL_OPTIONS at the start if present
+ List toolOptions = getToolOptions();
+ if (!toolOptions.isEmpty()) {
+ vmOptions.addAll(0, toolOptions);
+ }
+ return vmOptions;
+ }
+
+ // Try Oracle-based
+ // IBM Semeru Runtime 1.8.0_345-b01 will throw UnsatisfiedLinkError here.
+ try {
+ final Class> managementFactoryHelperClass =
+ Class.forName("sun.management.ManagementFactoryHelper");
+ final Class> vmManagementClass = Class.forName("sun.management.VMManagement");
+
+ Object vmManagement;
+ try {
+ vmManagement =
+ managementFactoryHelperClass.getDeclaredMethod("getVMManagement").invoke(null);
+ } catch (final NoSuchMethodException e) {
+ // Older vm before getVMManagement() existed
+ final Field field = managementFactoryHelperClass.getDeclaredField("jvm");
+ field.setAccessible(true);
+ vmManagement = field.get(null);
+ field.setAccessible(false);
+ }
+ //noinspection unchecked
+ return (List) vmManagementClass.getMethod("getVmArguments").invoke(vmManagement);
+ } catch (final ReflectiveOperationException | UnsatisfiedLinkError ignored) {
+ // Ignored exception
+ }
+
+ // Try IBM-based.
+ try {
+ final Class> VMClass = Class.forName("com.ibm.oti.vm.VM");
+ final String[] argArray = (String[]) VMClass.getMethod("getVMArgs").invoke(null);
+ return Arrays.asList(argArray);
+ } catch (final ReflectiveOperationException ignored) {
+ // Ignored exception
+ }
+
+ // Fallback to default
+ try {
+ return ManagementFactory.getRuntimeMXBean().getInputArguments();
+ } catch (final Throwable t) {
+ // Throws InvocationTargetException on modularized applications
+ // with non-opened java.management module
+ System.err.println("WARNING: Unable to get VM args using managed beans");
+ }
+ return emptyList();
+ }
+
+ @SuppressForbidden // split on single-character uses fast path
+ private List findFullCommand() {
+ // Besides "sun.java.command" property is not an standard, all main JDKs has set this
+ // property.
+ // Tested on:
+ // - OracleJDK, OpenJDK, AdoptOpenJDK, IBM JDK, Azul Zulu JDK, Amazon Coretto JDK
+ String command = SystemProperties.getOrDefault("sun.java.command", "").trim();
+ return command.isEmpty() ? emptyList() : Arrays.asList(command.split(" "));
+ }
+
+ private static List getArgumentsFromFile(String argFile) {
+ String filename = argFile.substring(1);
+ Path path = Paths.get(filename);
+ if (!Files.exists(path) || !Files.isReadable(path)) {
+ return singletonList(argFile);
+ }
+ List args = new ArrayList<>();
+ try {
+ for (String line : Files.readAllLines(path)) {
+ // Use default delimiters that matches argfiles separator specification
+ StringTokenizer tokenizer = new StringTokenizer(line);
+ while (tokenizer.hasMoreTokens()) {
+ args.add(tokenizer.nextToken());
+ }
+ }
+ return args;
+ } catch (IOException e) {
+ return singletonList(argFile);
+ }
+ }
+
+ private static List getToolOptions() {
+ String javaToolOptions = EnvironmentVariables.getOrDefault("JAVA_TOOL_OPTIONS", "");
+ return javaToolOptions.isEmpty() ? emptyList() : parseToolOptions(javaToolOptions);
+ }
+
+ /**
+ * Parse the JAVA_TOOL_OPTIONS environment variable according the JVMTI specifications
+ *
+ * @param javaToolOptions The JAVA_TOOL_OPTIONS environment variable.
+ * @return The parsed JVM options.
+ * @see JVMTI
+ * specifications
+ */
+ static List parseToolOptions(String javaToolOptions) {
+ List options = new ArrayList<>();
+ StringBuilder option = new StringBuilder();
+ boolean inQuotes = false;
+ char quoteChar = 0;
+
+ for (int i = 0; i < javaToolOptions.length(); i++) {
+ char c = javaToolOptions.charAt(i);
+ if (inQuotes) {
+ if (quoteChar == c) {
+ inQuotes = false;
+ } else {
+ option.append(c);
+ }
+ } else if (c == '"' || c == '\'') {
+ inQuotes = true;
+ quoteChar = c;
+ } else if (Character.isWhitespace(c)) {
+ if (option.length() > 0) {
+ options.add(option.toString());
+ option.setLength(0);
+ }
+ } else {
+ option.append(c);
+ }
+ }
+ if (option.length() > 0) {
+ options.add(option.toString());
+ }
+ return options;
+ }
+
+ private static List split(String str, String delimiter) {
+ List parts = new ArrayList<>();
+ StringTokenizer tokenizer = new StringTokenizer(str, delimiter);
+ while (tokenizer.hasMoreTokens()) {
+ parts.add(tokenizer.nextToken());
+ }
+ return parts;
+ }
+}
diff --git a/components/environment/src/main/java/datadog/environment/OperatingSystem.java b/components/environment/src/main/java/datadog/environment/OperatingSystem.java
new file mode 100644
index 00000000000..06c72bad553
--- /dev/null
+++ b/components/environment/src/main/java/datadog/environment/OperatingSystem.java
@@ -0,0 +1,149 @@
+package datadog.environment;
+
+import static java.util.Locale.ROOT;
+
+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.Arrays;
+
+/** Detects operating systems and libc library. */
+public final class OperatingSystem {
+ private static final String OS_NAME_PROPERTY = "os.name";
+ private static final String OS_ARCH_PROPERTY = "os.arch";
+
+ private OperatingSystem() {}
+
+ /**
+ * Checks whether the operating system is Linux based.
+ *
+ * @return @{@code true} if operating system is Linux based, {@code false} otherwise.
+ */
+ public static boolean isLinux() {
+ return propertyContains(OS_NAME_PROPERTY, "linux");
+ }
+
+ /**
+ * Checks whether the operating system is Windows.
+ *
+ * @return @{@code true} if operating system is Windows, {@code false} otherwise.
+ */
+ public static boolean isWindows() {
+ // https://mkyong.com/java/how-to-detect-os-in-java-systemgetpropertyosname/
+ return propertyContains(OS_NAME_PROPERTY, "win");
+ }
+
+ /**
+ * Checks whether the operating system is macOS.
+ *
+ * @return @{@code true} if operating system is macOS, {@code false} otherwise.
+ */
+ public static boolean isMacOs() {
+ return propertyContains(OS_NAME_PROPERTY, "mac");
+ }
+
+ /**
+ * Checks whether the architecture is AArch64.
+ *
+ * @return {@code true} if the architecture is AArch64, {@code false} otherwise.
+ */
+ public static boolean isAarch64() {
+ return propertyContains(OS_ARCH_PROPERTY, "aarch64");
+ }
+
+ private static boolean propertyContains(String property, String content) {
+ return SystemProperties.getOrDefault(property, "").toLowerCase(ROOT).contains(content);
+ }
+
+ /**
+ * Checks whether the libc is MUSL.
+ *
+ * @return {@code true} if the libc is MUSL, {@code false} otherwise.
+ */
+ 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;
+ }
+ }
+ }
+
+ private 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 (e.g.
+ * `/ld-linux-...`). However, if such string is missing should indicate that the system is not a
+ * musl one.
+ */
+ private 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(SystemProperties.getOrDefault("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;
+ }
+}
diff --git a/components/environment/src/main/java/datadog/environment/SystemProperties.java b/components/environment/src/main/java/datadog/environment/SystemProperties.java
new file mode 100644
index 00000000000..29b8e25804c
--- /dev/null
+++ b/components/environment/src/main/java/datadog/environment/SystemProperties.java
@@ -0,0 +1,53 @@
+package datadog.environment;
+
+/**
+ * Safely queries system properties against security manager.
+ *
+ * @see Security
+ * Manager
+ */
+public final class SystemProperties {
+ private SystemProperties() {}
+
+ /**
+ * Gets a system property value.
+ *
+ * @param property The system property name.
+ * @return The system property value, {@code null} if missing or can't be retrieved.
+ */
+ public static String get(String property) {
+ return getOrDefault(property, null);
+ }
+
+ /**
+ * Gets a system property value, or default value if missing or can't be retrieved.
+ *
+ * @param property The system property name.
+ * @param defaultValue The default value to return if the system property is missing or can't be
+ * retrieved.
+ * @return The system property value, {@code defaultValue} if missing or can't be retrieved.
+ */
+ public static String getOrDefault(String property, String defaultValue) {
+ try {
+ return System.getProperty(property, defaultValue);
+ } catch (SecurityException ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Sets a system property value.
+ *
+ * @param property The system property name.
+ * @param value The system property value to set.
+ * @return {@code true} if the system property was successfully set, {@code} false otherwise.
+ */
+ public static boolean set(String property, String value) {
+ try {
+ System.setProperty(property, value);
+ return true;
+ } catch (SecurityException ignored) {
+ return false;
+ }
+ }
+}
diff --git a/components/environment/src/test/java/datadog/environment/CommandLineTest.java b/components/environment/src/test/java/datadog/environment/CommandLineTest.java
new file mode 100644
index 00000000000..c362a02d86c
--- /dev/null
+++ b/components/environment/src/test/java/datadog/environment/CommandLineTest.java
@@ -0,0 +1,266 @@
+package datadog.environment;
+
+import static datadog.environment.CommandLineTest.RunArguments.of;
+import static java.io.File.separator;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Objects.requireNonNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class CommandLineTest {
+ private static final String JVM_OPTIONS_MARKER = "-- JVM OPTIONS --";
+ private static final String CMD_ARGUMENTS_MARKER = "-- CMD ARGUMENTS --";
+ private static final String REAL_CMD_ARGUMENTS_MARKER = "-- REAL CMD ARGUMENTS --";
+
+ static Stream data() {
+ // spotless:off
+ return Stream.of(
+ arguments(
+ "No JVM options nor command argument",
+ of(emptyList(), emptyList()),
+ of(emptyList(), emptyList())
+ ),
+ arguments(
+ "JVM options only",
+ of(asList("-Xmx128m", "-XX:+UseG1GC", "-Dtest.property=value"), emptyList()),
+ of(asList("-Xmx128m", "-XX:+UseG1GC", "-Dtest.property=value"), emptyList())),
+ arguments(
+ "Command arguments only",
+ of(emptyList(), asList("arg1", "arg2")),
+ of(emptyList(), asList("arg1", "arg2"))),
+ arguments(
+ "Both JVM options and command arguments",
+ of(asList("-Xmx128m", "-XX:+UseG1GC", "-Dtest.property=value"), asList("arg1", "arg2")),
+ of(asList("-Xmx128m", "-XX:+UseG1GC", "-Dtest.property=value"), asList("arg1", "arg2"))),
+ arguments(
+ "JVM options from argfile",
+ of(asList("-Dtest.property=value", argFile("space-separated")), asList("arg1", "arg2")),
+ of(flatten("-Dtest.property=value", expectedArsFromArgFile("space-separated")), asList("arg1", "arg2")))
+ );
+ // spotless:on
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}")
+ @MethodSource("data")
+ void testGetVmArguments(String useCase, RunArguments arguments, RunArguments expectedArguments)
+ throws Exception {
+ if (useCase.contains("argfile")) {
+ System.out.println(">>> java.home" + System.getProperty("java.home"));
+ System.err.println(">>> java.home" + System.getProperty("java.home"));
+ }
+ // Skip unsupported test cases
+ skipArgFileTestOnJava8(arguments);
+ // keepDisabledArgFileOnLinuxOnly(arguments);
+ // Run test process
+ Result result = forkAndRunWithArgs(CommandLineTestProcess.class, arguments);
+ // Check results
+ assertEquals(expectedArguments.jvmOptions, result.jvmOptions, "Failed to get JVM options");
+ assertEquals(result.realCmdArgs, result.cmdArgs, "Failed to get command arguments");
+ assertEquals(result.realCmdArgs, expectedArguments.cmdArgs, "Unexpected command arguments");
+ }
+
+ // @Test
+ // // Disable the test for Java 8. Using -PtestJvm will set Java HOME to the JVM to use to run
+ // this
+ // // test.
+ // @DisabledIfSystemProperty(named = "java.home", matches = ".*[-/]8\\..*")
+ // public void testGetVmArgumentsFromArgFile() throws Exception {
+ // List jvmOptions = asList("-Dproperty1=value1", argFile("space-separated"));
+ // List expectedJvmOptions = flatten("-Dproperty1=value1",
+ // expectedArsFromArgFile("space-separated"));
+ // Result result = forkAndRunWithArgs(CommandLineTestProcess.class, of(jvmOptions,
+ // emptyList()));
+ // assertEquals(expectedJvmOptions, result.jvmOptions, "Failed to get JVM options");
+ // // TODO CMD ARGS
+ // }
+ //
+ // @Test
+ // // Enable only for Java 20+: https://bugs.openjdk.org/browse/JDK-8297258
+ // // Using -PtestJvm will set Java HOME to the JVM to use to run this test.
+ // @EnabledIfSystemProperty(named = "java.home", matches = ".*[-/]21\\..*")
+ // public void testGetVmArgumentsFromDisabledArgFile() throws Exception {
+ // List jvmArgs = asList("-Dproperty1=value1", "--disable-@files");
+ // List cmdArgs = asList("arg1", argFile("space-separated"), "arg2");
+ // // --disable-@files won't be reported
+ // List expectedJvmOptions = singletonList("-Dproperty1=value1");
+ // List expectedCmdArgs = flatten(CommandLineTestProcess.class.getName(), cmdArgs);
+ // Result result = forkAndRunWithArgs(CommandLineTestProcess.class, of(jvmArgs, cmdArgs));
+ // assertEquals(expectedJvmOptions, result.jvmOptions, "Failed to get JVM options");
+ // assertEquals(expectedCmdArgs, result.cmdArgs, "Failed to get command arguments");
+ // }
+
+ private static void skipArgFileTestOnJava8(RunArguments arguments) {
+ boolean useArgFile = false;
+ for (String jvmOption : arguments.jvmOptions) {
+ if (jvmOption.startsWith("@")) {
+ useArgFile = true;
+ break;
+ }
+ }
+ if (!useArgFile) {
+ for (String cmdArg : arguments.cmdArgs) {
+ if (cmdArg.startsWith("@")) {
+ useArgFile = true;
+ break;
+ }
+ }
+ }
+ if (useArgFile) {
+ assumeFalse(System.getProperty("java.home").matches(".*[-/]8[./].*"));
+ }
+ }
+
+ private static void keepDisabledArgFileOnLinuxOnly(RunArguments arguments) {
+ boolean disableArgFile = false;
+ for (String jvmOptions : arguments.jvmOptions) {
+ if (jvmOptions.startsWith("--disable-@files")) {
+ disableArgFile = true;
+ break;
+ }
+ }
+ if (disableArgFile) {
+ assumeTrue(OperatingSystem.isLinux());
+ }
+ }
+
+ private static String argFile(String name) {
+ return "@src/test/resources/argfiles/" + name + ".txt";
+ }
+
+ private static List expectedArsFromArgFile(String name) {
+ List arguments = new ArrayList<>();
+ try (InputStream stream =
+ requireNonNull(
+ CommandLineTest.class.getResourceAsStream("/argfiles/" + name + "-expected.txt"));
+ BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ arguments.add(line);
+ }
+ } catch (IOException e) {
+ Assertions.fail("Failed to read expected args from " + name + "argfile", e);
+ }
+ return arguments;
+ }
+
+ private Result forkAndRunWithArgs(Class> clazz, RunArguments arguments)
+ throws IOException, InterruptedException {
+ // Build the command to run a new Java process
+ List command = new ArrayList<>();
+ command.add(System.getProperty("java.home") + separator + "bin" + separator + "java");
+ command.addAll(arguments.jvmOptions);
+ command.add("-cp");
+ command.add(System.getProperty("java.class.path"));
+ command.add(clazz.getName());
+ command.addAll(arguments.cmdArgs);
+ // Start the process
+ ProcessBuilder processBuilder = new ProcessBuilder(command);
+ Process process = processBuilder.start();
+ // Read and parse output and error streams
+ Result result = new Result();
+ List current = null;
+ String output = "";
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (JVM_OPTIONS_MARKER.equals(line)) {
+ current = result.jvmOptions;
+ } else if (CMD_ARGUMENTS_MARKER.equals(line)) {
+ current = result.cmdArgs;
+ } else if (REAL_CMD_ARGUMENTS_MARKER.equals(line)) {
+ current = result.realCmdArgs;
+ } else if (current != null) {
+ current.add(line);
+ }
+ output += line + "\n";
+ }
+ }
+ String error = "";
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ error += line + "\n";
+ }
+ }
+ // Wait for the process to complete
+ int exitCode = process.waitFor();
+ // Dumping state on error
+ if (exitCode != 0) {
+ System.err.println("Error running command: " + String.join(" ", command));
+ System.err.println("Exit code " + exitCode + " with output:");
+ System.err.println(output);
+ System.err.println("and error:");
+ System.err.println(error);
+ }
+ assertEquals(0, exitCode, "Process should exit normally");
+ return result;
+ }
+
+ private static List flatten(Object... values) {
+ List result = new ArrayList<>();
+ for (Object value : values) {
+ if (value instanceof Collection) {
+ result.addAll((Collection extends String>) value);
+ } else {
+ result.add(value.toString());
+ }
+ }
+ return result;
+ }
+
+ static class RunArguments {
+ List jvmOptions = new ArrayList<>();
+ List cmdArgs = new ArrayList<>();
+
+ static RunArguments of(List jvmArgs, List cmdArgs) {
+ RunArguments arguments = new RunArguments();
+ arguments.jvmOptions = jvmArgs;
+ arguments.cmdArgs = cmdArgs;
+ return arguments;
+ }
+ }
+
+ static class Result extends CommandLineTest.RunArguments {
+ List realCmdArgs = new ArrayList<>();
+ String command; // TODO
+ }
+
+ // This class will be executed in the subprocess
+ public static class CommandLineTestProcess {
+ public static void main(String[] args) {
+ // Print each VM argument on a new line
+ System.out.println(JVM_OPTIONS_MARKER);
+ for (String option : JavaVirtualMachine.getVmOptions()) {
+ System.out.println(option);
+ }
+ // Print each command argument on a new line
+ System.out.println(CMD_ARGUMENTS_MARKER);
+ for (String arg : JavaVirtualMachine.getCommandArguments()) {
+ System.out.println(arg);
+ }
+ // Print each real command argument on a new line
+ System.out.println(REAL_CMD_ARGUMENTS_MARKER);
+ for (String arg : args) {
+ System.out.println(arg);
+ }
+ }
+ }
+}
diff --git a/components/environment/src/test/java/datadog/environment/EnvironmentVariablesTest.java b/components/environment/src/test/java/datadog/environment/EnvironmentVariablesTest.java
new file mode 100644
index 00000000000..80cb4f601b6
--- /dev/null
+++ b/components/environment/src/test/java/datadog/environment/EnvironmentVariablesTest.java
@@ -0,0 +1,27 @@
+package datadog.environment;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+class EnvironmentVariablesTest {
+ private static final String EXISTING_ENV_VAR = "JAVA_8_HOME";
+ private static final String MISSING_ENV_VAR = "UNDEFINED_ENV_VAR";
+
+ @Test
+ void testGet() {
+ assertNotNull(EnvironmentVariables.get(EXISTING_ENV_VAR));
+ assertNull(EnvironmentVariables.get(MISSING_ENV_VAR));
+ assertThrows(NullPointerException.class, () -> EnvironmentVariables.get(null));
+ }
+
+ @Test
+ void testGetOrDefault() {
+ assertNotNull(EnvironmentVariables.getOrDefault(EXISTING_ENV_VAR, null));
+
+ assertEquals("", EnvironmentVariables.getOrDefault(MISSING_ENV_VAR, ""));
+ assertNull(EnvironmentVariables.getOrDefault(MISSING_ENV_VAR, null));
+
+ assertThrows(NullPointerException.class, () -> EnvironmentVariables.getOrDefault(null, ""));
+ }
+}
diff --git a/components/environment/src/test/java/datadog/environment/JavaVersionTest.java b/components/environment/src/test/java/datadog/environment/JavaVersionTest.java
new file mode 100644
index 00000000000..9fcb3fc3a13
--- /dev/null
+++ b/components/environment/src/test/java/datadog/environment/JavaVersionTest.java
@@ -0,0 +1,103 @@
+package datadog.environment;
+
+import static java.lang.Integer.MAX_VALUE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.params.provider.Arguments.of;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class JavaVersionTest {
+ static Stream argumentsParsing() {
+ return Stream.of(
+ of("", 0, 0, 0),
+ of("a.0.0", 0, 0, 0),
+ of("0.a.0", 0, 0, 0),
+ of("0.0.a", 0, 0, 0),
+ of("1.a.0_0", 0, 0, 0),
+ of("1.8.a_0", 0, 0, 0),
+ of("1.8.0_a", 0, 0, 0),
+ of("1.7", 7, 0, 0),
+ of("1.7.0", 7, 0, 0),
+ of("1.7.0_221", 7, 0, 221),
+ of("1.8", 8, 0, 0),
+ of("1.8.0", 8, 0, 0),
+ of("1.8.0_212", 8, 0, 212),
+ of("1.8.0_292", 8, 0, 292),
+ of("9-ea", 9, 0, 0),
+ of("9.0.4", 9, 0, 4),
+ of("9.1.2", 9, 1, 2),
+ of("10.0.2", 10, 0, 2),
+ of("11", 11, 0, 0),
+ of("11.0.6", 11, 0, 6),
+ of("11.0.11", 11, 0, 11),
+ of("12.0.2", 12, 0, 2),
+ of("13.0.2", 13, 0, 2),
+ of("14", 14, 0, 0),
+ of("14.0.2", 14, 0, 2),
+ of("15", 15, 0, 0),
+ of("15.0.2", 15, 0, 2),
+ of("16.0.1", 16, 0, 1),
+ of("11.0.9.1+1", 11, 0, 9),
+ of("11.0.6+10", 11, 0, 6),
+ of("17.0.4-x", 17, 0, 4));
+ }
+
+ @ParameterizedTest(name = "[{index}] {0} parsed as {1}.{2}.{3}")
+ @MethodSource("argumentsParsing")
+ void testParsing(String version, int major, int minor, int update) {
+ JavaVersion javaVersion = JavaVersion.parseJavaVersion(version);
+
+ assertEquals(major, javaVersion.major);
+ assertEquals(minor, javaVersion.minor);
+ assertEquals(update, javaVersion.update);
+
+ assertFalse(javaVersion.is(major - 1));
+ assertTrue(javaVersion.is(major));
+ assertFalse(javaVersion.is(major + 1));
+
+ assertFalse(javaVersion.is(major, minor - 1));
+ assertTrue(javaVersion.is(major, minor));
+ assertFalse(javaVersion.is(major, minor + 1));
+
+ assertFalse(javaVersion.is(major, minor, update - 1));
+ assertTrue(javaVersion.is(major, minor, update));
+ assertFalse(javaVersion.is(major, minor, update + 1));
+
+ assertTrue(javaVersion.isAtLeast(major, minor, update));
+
+ assertFalse(javaVersion.isBetween(major, minor, update, major, minor, update));
+
+ assertTrue(javaVersion.isBetween(major, minor, update, MAX_VALUE, MAX_VALUE, MAX_VALUE));
+ assertTrue(javaVersion.isBetween(major, minor, update, major + 1, 0, 0));
+ assertTrue(javaVersion.isBetween(major, minor, update, major, minor + 1, 0));
+ assertTrue(javaVersion.isBetween(major, minor, update, major, minor, update + 1));
+
+ assertFalse(javaVersion.isBetween(major, minor, update, major - 1, 0, 0));
+ assertFalse(javaVersion.isBetween(major, minor, update, major, minor - 1, 0));
+ assertFalse(javaVersion.isBetween(major, minor, update, major, minor, update - 1));
+ }
+
+ static Stream argumentsAtLeast() {
+ return Stream.of(
+ of("17.0.5+8", 17, 0, 5),
+ of("17.0.5", 17, 0, 5),
+ of("17.0.6+8", 17, 0, 5),
+ of("11.0.17+8", 11, 0, 17),
+ of("11.0.18+8", 11, 0, 17),
+ of("11.0.17", 11, 0, 17),
+ of("1.8.0_352", 8, 0, 352),
+ of("1.8.0_362", 8, 0, 352));
+ }
+
+ @ParameterizedTest(name = "[{index}] {0} is at least {1}.{2}.{3}")
+ @MethodSource("argumentsAtLeast")
+ void testIsAtLeast(String version, int major, int minor, int update) {
+ JavaVersion javaVersion = JavaVersion.parseJavaVersion(version);
+ assertTrue(javaVersion.isAtLeast(major, minor, update));
+ }
+}
diff --git a/components/environment/src/test/java/datadog/environment/JavaVirtualMachineTest.java b/components/environment/src/test/java/datadog/environment/JavaVirtualMachineTest.java
new file mode 100644
index 00000000000..62b0ad3307f
--- /dev/null
+++ b/components/environment/src/test/java/datadog/environment/JavaVirtualMachineTest.java
@@ -0,0 +1,169 @@
+package datadog.environment;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.condition.JRE.JAVA_11;
+import static org.junit.jupiter.api.condition.JRE.JAVA_17;
+import static org.junit.jupiter.api.condition.JRE.JAVA_21;
+import static org.junit.jupiter.api.condition.JRE.JAVA_8;
+import static org.junit.jupiter.api.condition.JRE.JAVA_9;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+import org.junit.jupiter.api.condition.EnabledOnJre;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+class JavaVirtualMachineTest {
+ @Test
+ @EnabledOnJre(JAVA_8)
+ void onJava8Only() {
+ assertTrue(JavaVirtualMachine.isJavaVersion(8));
+ assertFalse(JavaVirtualMachine.isJavaVersion(11));
+ }
+
+ @Test
+ @EnabledOnJre(JAVA_11)
+ void onJava11Only() {
+ assertFalse(JavaVirtualMachine.isJavaVersion(8));
+ assertTrue(JavaVirtualMachine.isJavaVersion(11));
+ assertFalse(JavaVirtualMachine.isJavaVersion(17));
+ }
+
+ @Test
+ @EnabledOnJre(JAVA_17)
+ void onJava17Only() {
+ assertFalse(JavaVirtualMachine.isJavaVersion(11));
+ assertTrue(JavaVirtualMachine.isJavaVersion(17));
+ assertFalse(JavaVirtualMachine.isJavaVersion(21));
+ }
+
+ @Test
+ @EnabledOnJre(JAVA_21)
+ void onJava21Only() {
+ assertFalse(JavaVirtualMachine.isJavaVersion(17));
+ assertTrue(JavaVirtualMachine.isJavaVersion(21));
+ assertFalse(JavaVirtualMachine.isJavaVersion(25));
+ }
+
+ @Test
+ void onJava7andHigher() {
+ assertTrue(JavaVirtualMachine.isJavaVersionAtLeast(7));
+ }
+
+ @Test
+ void onJava8AndHigher() {
+ for (int version = 7; version <= 8; version++) {
+ assertTrue(JavaVirtualMachine.isJavaVersionAtLeast(version));
+ }
+ }
+
+ @Test
+ @EnabledForJreRange(min = JAVA_11)
+ void onJava11AndHigher() {
+ for (int version = 7; version <= 11; version++) {
+ assertTrue(JavaVirtualMachine.isJavaVersionAtLeast(version));
+ }
+ }
+
+ @Test
+ @EnabledForJreRange(min = JAVA_17)
+ void onJava17AndHigher() {
+ for (int version = 7; version <= 17; version++) {
+ assertTrue(JavaVirtualMachine.isJavaVersionAtLeast(version));
+ }
+ }
+
+ @Test
+ @EnabledForJreRange(min = JAVA_21)
+ void onJava21AndHigher() {
+ for (int version = 7; version <= 21; version++) {
+ assertTrue(JavaVirtualMachine.isJavaVersionAtLeast(version));
+ }
+ }
+
+ @Test
+ @EnabledForJreRange(min = JAVA_8, max = JAVA_9)
+ void fromJava8to9() {
+ assertFalse(JavaVirtualMachine.isJavaVersionBetween(7, 8));
+ assertTrue(JavaVirtualMachine.isJavaVersionBetween(8, 10));
+ assertFalse(JavaVirtualMachine.isJavaVersionBetween(10, 11));
+ }
+
+ @Test
+ @EnabledForJreRange(min = JAVA_11, max = JAVA_11)
+ void fromJava11to17() {
+ assertFalse(JavaVirtualMachine.isJavaVersionBetween(8, 11));
+ assertTrue(JavaVirtualMachine.isJavaVersionBetween(11, 18));
+ assertFalse(JavaVirtualMachine.isJavaVersionBetween(18, 21));
+ }
+
+ @Test
+ @EnabledIfSystemProperty(named = "java.vendor.version", matches = ".*graalvm.*")
+ void onlyOnGraalVm() {
+ assertTrue(JavaVirtualMachine.isGraalVM());
+ assertFalse(JavaVirtualMachine.isIbm8());
+ assertFalse(JavaVirtualMachine.isJ9());
+ assertFalse(JavaVirtualMachine.isOracleJDK8());
+ }
+
+ @Test
+ @EnabledIfSystemProperty(named = "java.vm.vendor", matches = ".*IBM.*")
+ @EnabledOnJre(JAVA_8)
+ void onlyOnIbm8() {
+ assertFalse(JavaVirtualMachine.isGraalVM());
+ assertTrue(JavaVirtualMachine.isIbm8());
+ assertFalse(JavaVirtualMachine.isJ9());
+ assertFalse(JavaVirtualMachine.isOracleJDK8());
+ }
+
+ @Test
+ @EnabledIfSystemProperty(named = "java.vm.name", matches = ".*J9.*")
+ void onlyOnJ9() {
+ assertFalse(JavaVirtualMachine.isGraalVM());
+ assertFalse(JavaVirtualMachine.isIbm8());
+ assertTrue(JavaVirtualMachine.isJ9());
+ assertFalse(JavaVirtualMachine.isOracleJDK8());
+ }
+
+ @Test
+ @EnabledIfSystemProperty(named = "java.vm.vendor", matches = ".*Oracle.*")
+ @DisabledIfSystemProperty(named = "java.runtime.name", matches = ".*OpenJDK.*")
+ @EnabledOnJre(JAVA_8)
+ void onlyOnOracleJDK8() {
+ assertFalse(JavaVirtualMachine.isGraalVM());
+ assertFalse(JavaVirtualMachine.isIbm8());
+ assertFalse(JavaVirtualMachine.isJ9());
+ assertTrue(JavaVirtualMachine.isOracleJDK8());
+ }
+
+ @ParameterizedTest
+ @CsvSource(
+ value = {
+ "1.8.0_265 | 1.8.0_265-b01 | OpenJDK | AdoptOpenJDK | 1.8.0_265 | b01 | OpenJDK | AdoptOpenJDK",
+ "1.8.0_265 | 1.8-b01 | OpenJDK | AdoptOpenJDK | 1.8.0_265 | '' | OpenJDK | AdoptOpenJDK",
+ "19 | 19 | OpenJDK 64-Bit | Homebrew | 19 | '' | OpenJDK 64-Bit | Homebrew",
+ "17 | null | null | null | 17 | '' | '' | ''",
+ "null | 17 | null | null | '' | '' | '' | ''",
+ },
+ nullValues = "null",
+ delimiter = '|')
+ void testRuntimeParsing(
+ String javaVersion,
+ String javaRuntimeVersion,
+ String javaRuntimeName,
+ String javaVmVendor,
+ String expectedVersion,
+ String expectedPatches,
+ String expectedName,
+ String expectedVendor) {
+ JavaVirtualMachine.Runtime runtime =
+ new JavaVirtualMachine.Runtime(
+ javaVersion, javaRuntimeVersion, javaRuntimeName, javaVmVendor, null);
+ assertEquals(expectedVersion, runtime.version);
+ assertEquals(expectedPatches, runtime.patches);
+ assertEquals(expectedName, runtime.name);
+ assertEquals(expectedVendor, runtime.vendor);
+ }
+}
diff --git a/components/environment/src/test/java/datadog/environment/JvmOptionsTest.java b/components/environment/src/test/java/datadog/environment/JvmOptionsTest.java
new file mode 100644
index 00000000000..52ff039bc9a
--- /dev/null
+++ b/components/environment/src/test/java/datadog/environment/JvmOptionsTest.java
@@ -0,0 +1,38 @@
+package datadog.environment;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class JvmOptionsTest {
+ static Stream data() {
+ return Stream.of(
+ arguments("", emptyList()),
+ arguments("-Xmx512m", singletonList("-Xmx512m")),
+ arguments("-Xms256m -Xmx512m", asList("-Xms256m", "-Xmx512m")),
+ arguments(" -Xms256m -Xmx512m ", asList("-Xms256m", "-Xmx512m")),
+ arguments("-Xms256m\t-Xmx512m", asList("-Xms256m", "-Xmx512m")),
+ arguments("\t -Xms256m \t -Xmx512m \t", asList("-Xms256m", "-Xmx512m")),
+ arguments(
+ "-Xmx512m -Dprop=\"value with space\"", asList("-Xmx512m", "-Dprop=value with space")),
+ arguments(
+ "-Xmx512m -Dprop='value with space'", asList("-Xmx512m", "-Dprop=value with space")),
+ arguments("-Xmx512m -Dprop='mixing\"quotes'", asList("-Xmx512m", "-Dprop=mixing\"quotes")),
+ arguments("-Xmx512m -Dprop=\"mixing'quotes\"", asList("-Xmx512m", "-Dprop=mixing'quotes")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ void testParseToolOptions(String javaToolOptions, List expectedVmOptions) {
+ List vmOptions = JvmOptions.parseToolOptions(javaToolOptions);
+ assertEquals(expectedVmOptions, vmOptions);
+ }
+}
diff --git a/components/environment/src/test/java/datadog/environment/OperatingSystemTest.java b/components/environment/src/test/java/datadog/environment/OperatingSystemTest.java
new file mode 100644
index 00000000000..5dc4a355546
--- /dev/null
+++ b/components/environment/src/test/java/datadog/environment/OperatingSystemTest.java
@@ -0,0 +1,36 @@
+package datadog.environment;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.condition.OS.LINUX;
+import static org.junit.jupiter.api.condition.OS.MAC;
+import static org.junit.jupiter.api.condition.OS.WINDOWS;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+
+class OperatingSystemTest {
+ @Test
+ @EnabledOnOs(LINUX)
+ void onLinuxOnly() {
+ assertTrue(OperatingSystem.isLinux());
+ assertFalse(OperatingSystem.isMacOs());
+ assertFalse(OperatingSystem.isWindows());
+ }
+
+ @Test
+ @EnabledOnOs(MAC)
+ void onMacOsOnly() {
+ assertFalse(OperatingSystem.isLinux());
+ assertTrue(OperatingSystem.isMacOs());
+ assertFalse(OperatingSystem.isWindows());
+ }
+
+ @Test
+ @EnabledOnOs(WINDOWS)
+ void onWindowsOnly() {
+ assertFalse(OperatingSystem.isLinux());
+ assertFalse(OperatingSystem.isMacOs());
+ assertTrue(OperatingSystem.isWindows());
+ }
+}
diff --git a/components/environment/src/test/java/datadog/environment/SystemPropertiesTest.java b/components/environment/src/test/java/datadog/environment/SystemPropertiesTest.java
new file mode 100644
index 00000000000..a145071c442
--- /dev/null
+++ b/components/environment/src/test/java/datadog/environment/SystemPropertiesTest.java
@@ -0,0 +1,38 @@
+package datadog.environment;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import org.junit.jupiter.api.Test;
+
+class SystemPropertiesTest {
+ private static final String EXISTING_SYSTEM_PROPERTY = "java.home";
+ private static final String MISSING_SYSTEM_PROPERTY = "undefined.system.property";
+
+ @Test
+ void testGet() {
+ assertNotNull(SystemProperties.get(EXISTING_SYSTEM_PROPERTY));
+ assertNull(SystemProperties.get(MISSING_SYSTEM_PROPERTY));
+ assertThrows(NullPointerException.class, () -> SystemProperties.get(null));
+ }
+
+ @Test
+ void testGetOrDefault() {
+ assertNotNull(SystemProperties.getOrDefault(EXISTING_SYSTEM_PROPERTY, null));
+
+ assertEquals("", SystemProperties.getOrDefault(MISSING_SYSTEM_PROPERTY, ""));
+ assertNull(SystemProperties.getOrDefault(MISSING_SYSTEM_PROPERTY, null));
+
+ assertThrows(NullPointerException.class, () -> SystemProperties.getOrDefault(null, ""));
+ }
+
+ @Test
+ void testSet() {
+ String testProperty = "test.property";
+ String testValue = "test-value";
+ assumeTrue(SystemProperties.get(testProperty) == null);
+
+ assertTrue(SystemProperties.set(testProperty, testValue));
+ assertEquals(testValue, SystemProperties.get(testProperty));
+ }
+}
diff --git a/components/environment/src/test/resources/argfiles/space-separated-expected.txt b/components/environment/src/test/resources/argfiles/space-separated-expected.txt
new file mode 100644
index 00000000000..9f6e3601e79
--- /dev/null
+++ b/components/environment/src/test/resources/argfiles/space-separated-expected.txt
@@ -0,0 +1,3 @@
+-Xms256m
+-Xmx512m
+-Dargfile.prop=test
diff --git a/components/environment/src/test/resources/argfiles/space-separated.txt b/components/environment/src/test/resources/argfiles/space-separated.txt
new file mode 100644
index 00000000000..40b1252d911
--- /dev/null
+++ b/components/environment/src/test/resources/argfiles/space-separated.txt
@@ -0,0 +1 @@
+-Xms256m -Xmx512m -Dargfile.prop=test
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Constants.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Constants.java
index d6ddee8410d..bec6aa6f148 100644
--- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Constants.java
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Constants.java
@@ -15,10 +15,11 @@ public final class Constants {
*/
public static final String[] BOOTSTRAP_PACKAGE_PREFIXES = {
"datadog.slf4j",
+ "datadog.cli",
+ "datadog.context",
+ "datadog.environment",
"datadog.json",
"datadog.yaml",
- "datadog.context",
- "datadog.cli",
"datadog.appsec.api",
"datadog.trace.api",
"datadog.trace.bootstrap",
diff --git a/dd-java-agent/build.gradle b/dd-java-agent/build.gradle
index 8592f244c5f..cf35af97164 100644
--- a/dd-java-agent/build.gradle
+++ b/dd-java-agent/build.gradle
@@ -241,6 +241,7 @@ tasks.withType(GenerateMavenPom).configureEach { task ->
dependencies {
implementation project(path: ':components:json')
implementation project(path: ':components:cli')
+ implementation project(path: ':components:environment')
modules {
module("com.squareup.okio:okio") {
replacedBy("com.datadoghq.okio:okio") // embed our patched fork
diff --git a/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java b/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java
index ea784033ebb..fe64b90360d 100644
--- a/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java
+++ b/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java
@@ -4,6 +4,8 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import datadog.cli.CLIHelper;
+import datadog.environment.EnvironmentVariables;
+import datadog.environment.SystemProperties;
import de.thetaphi.forbiddenapis.SuppressForbidden;
import java.io.BufferedReader;
import java.io.File;
@@ -63,6 +65,10 @@ public static void premain(final String agentArgs, final Instrumentation inst) {
public static void agentmain(final String agentArgs, final Instrumentation inst) {
BootstrapInitializationTelemetry initTelemetry;
+ // TODO Example calls that break Gradle smoke tests
+ String javaVersion = SystemProperties.get("java.version");
+// String forwarderPath = EnvironmentVariables.get("DD_TELEMETRY_FORWARDER_PATH");
+
try {
initTelemetry = createInitializationTelemetry();
} catch (Throwable t) {
diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/SpockRunner.java b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/SpockRunner.java
index 83bf7732644..988b710616e 100644
--- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/SpockRunner.java
+++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/SpockRunner.java
@@ -38,10 +38,11 @@ public class SpockRunner extends JUnitPlatform {
*/
public static final String[] BOOTSTRAP_PACKAGE_PREFIXES_COPY = {
"datadog.slf4j",
+ "datadog.cli",
+ "datadog.context",
+ "datadog.environment",
"datadog.json",
"datadog.yaml",
- "datadog.context",
- "datadog.cli",
"datadog.appsec.api",
"datadog.trace.api",
"datadog.trace.bootstrap",
diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle
index cb46e079d9a..9398720f21f 100644
--- a/gradle/dependencies.gradle
+++ b/gradle/dependencies.gradle
@@ -17,6 +17,7 @@ final class CachedData {
exclude(project(':internal-api:internal-api-9'))
exclude(project(':communication'))
exclude(project(':components:context'))
+ exclude(project(':components:environment'))
exclude(project(':components:json'))
exclude(project(':components:yaml'))
exclude(project(':components:cli'))
diff --git a/internal-api/build.gradle b/internal-api/build.gradle
index fbf1916a4af..e9ba384aea2 100644
--- a/internal-api/build.gradle
+++ b/internal-api/build.gradle
@@ -243,9 +243,10 @@ dependencies {
// references TraceScope and Continuation from public api
api project(':dd-trace-api')
api libs.slf4j
+ api project(':components:cli')
api project(':components:context')
+ api project(':components:environment')
api project(':components:yaml')
- api project(':components:cli')
api project(":utils:time-utils")
// has to be loaded by system classloader:
diff --git a/settings.gradle b/settings.gradle
index 1c20bcacfa1..517619b434e 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -88,6 +88,7 @@ include ':dd-java-agent:agent-otel:otel-tooling'
include ':communication'
include ':components:cli'
include ':components:context'
+include ':components:environment'
include ':components:json'
include ':components:yaml'
include ':telemetry'