diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostIdResource.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostIdResource.java
index 4ba015eabee3..eb434daf6f2a 100644
--- a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostIdResource.java
+++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostIdResource.java
@@ -6,6 +6,7 @@
package io.opentelemetry.instrumentation.resources;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.logging.Level.FINE;
@@ -25,11 +26,22 @@
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Logger;
+import javax.annotation.Nullable;
/**
* {@link ResourceProvider} for automatically configuring host.id according to the
- * semantic conventions
+ * semantic conventions.
+ *
+ *
The lookup is non-privileged and OS specific:
+ *
+ *
+ * - Linux: {@code /etc/machine-id}, falling back to {@code /var/lib/dbus/machine-id}
+ *
- BSD: {@code /etc/hostid}, falling back to {@code /bin/kenv -q smbios.system.uuid}
+ *
- macOS: the {@code IOPlatformUUID} from {@code /usr/sbin/ioreg -rd1 -c
+ * IOPlatformExpertDevice}
+ *
- Windows: the {@code MachineGuid} registry value
+ *
*/
public final class HostIdResource {
@@ -38,27 +50,46 @@ public final class HostIdResource {
// copied from HostIncubatingAttributes
static final AttributeKey HOST_ID = AttributeKey.stringKey("host.id");
- public static final String REGISTRY_QUERY =
- "reg query HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid";
+ // Non-privileged machine-id sources per the semantic conventions. Commands are invoked with
+ // absolute paths to avoid resolving them through a potentially attacker controlled PATH, see
+ // https://github.com/open-telemetry/semantic-conventions/pull/3896
+ private static final List LINUX_MACHINE_ID_PATHS =
+ asList("/etc/machine-id", "/var/lib/dbus/machine-id");
+ private static final String BSD_HOSTID_PATH = "/etc/hostid";
+ private static final List BSD_KENV_COMMAND =
+ asList("/bin/kenv", "-q", "smbios.system.uuid");
+ private static final List MACOS_IOREG_COMMAND =
+ asList("/usr/sbin/ioreg", "-rd1", "-c", "IOPlatformExpertDevice");
+
+ // Prefer the SystemRoot/windir environment variables to locate the Windows directory, falling
+ // back to the conventional install path only if neither is set.
+ private static final String[] WINDOWS_ROOT_ENV_VARS = {"SystemRoot", "windir"};
+ private static final String WINDOWS_ROOT_FALLBACK = "C:\\Windows";
+ private static final List WINDOWS_REGISTRY_QUERY_ARGS =
+ asList("query", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid");
private static final HostIdResource INSTANCE =
new HostIdResource(
HostIdResource::getOsTypeSystemProperty,
- HostIdResource::readMachineIdFile,
- HostIdResource::queryWindowsRegistry);
+ HostIdResource::readFileLines,
+ HostIdResource::queryWindowsRegistry,
+ HostIdResource::runCommand);
private final Supplier getOsType;
- private final Function> machineIdReader;
+ private final Function> fileReader;
private final Supplier> queryWindowsRegistry;
+ private final Function, List> commandExecutor;
// Visible for testing
HostIdResource(
Supplier getOsType,
- Function> machineIdReader,
- Supplier> queryWindowsRegistry) {
+ Function> fileReader,
+ Supplier> queryWindowsRegistry,
+ Function, List> commandExecutor) {
this.getOsType = getOsType;
- this.machineIdReader = machineIdReader;
+ this.fileReader = fileReader;
this.queryWindowsRegistry = queryWindowsRegistry;
+ this.commandExecutor = commandExecutor;
}
/** Returns a {@link Resource} containing the {@code host.id} resource attribute. */
@@ -74,6 +105,12 @@ Resource createResource() {
if (runningLinux()) {
return readLinuxMachineId();
}
+ if (runningMacOs()) {
+ return readMacOsUuid();
+ }
+ if (runningBsd()) {
+ return readBsdHostId();
+ }
logger.log(FINE, "Unsupported OS type: {0}", getOsType.get());
return Resource.empty();
}
@@ -86,6 +123,16 @@ private boolean runningWindows() {
return getOsType.get().startsWith("Windows");
}
+ private boolean runningMacOs() {
+ // os.name is "Mac OS X" on the JVM (not "Darwin" as reported by uname)
+ String osType = getOsType.get().toLowerCase(Locale.ROOT);
+ return osType.contains("mac") || osType.contains("darwin");
+ }
+
+ private boolean runningBsd() {
+ return getOsType.get().endsWith("BSD");
+ }
+
// see
// https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/SystemUtils.java
// for values
@@ -94,25 +141,49 @@ private static String getOsTypeSystemProperty() {
}
private Resource readLinuxMachineId() {
- Path path = FileSystems.getDefault().getPath("/etc/machine-id");
- List lines = machineIdReader.apply(path);
- if (lines.isEmpty()) {
- return Resource.empty();
+ // /etc/machine-id is the primary source, /var/lib/dbus/machine-id is the fallback
+ for (String path : LINUX_MACHINE_ID_PATHS) {
+ String machineId =
+ firstNonBlankLine(fileReader.apply(FileSystems.getDefault().getPath(path)));
+ if (machineId != null) {
+ return Resource.create(Attributes.of(HOST_ID, machineId));
+ }
}
- return Resource.create(Attributes.of(HOST_ID, lines.get(0)));
+ return Resource.empty();
}
- private static List readMachineIdFile(Path path) {
- try {
- List lines = Files.readAllLines(path);
- if (lines.isEmpty()) {
- logger.fine("Failed to read /etc/machine-id: empty file");
+ private Resource readBsdHostId() {
+ // /etc/hostid is the primary source, kenv is the fallback
+ String hostId =
+ firstNonBlankLine(fileReader.apply(FileSystems.getDefault().getPath(BSD_HOSTID_PATH)));
+ if (hostId == null) {
+ hostId = firstNonBlankLine(commandExecutor.apply(BSD_KENV_COMMAND));
+ }
+ if (hostId != null) {
+ return Resource.create(Attributes.of(HOST_ID, hostId));
+ }
+ logger.fine("Failed to read host id on BSD: no value found in /etc/hostid or kenv output");
+ return Resource.empty();
+ }
+
+ private Resource readMacOsUuid() {
+ List lines = commandExecutor.apply(MACOS_IOREG_COMMAND);
+
+ for (String line : lines) {
+ if (line.contains("IOPlatformUUID")) {
+ // line looks like: "IOPlatformUUID" = "0123456789ABCDEF"
+ int equalsIndex = line.indexOf('=');
+ if (equalsIndex >= 0) {
+ String uuid = line.substring(equalsIndex + 1).trim().replace("\"", "").trim();
+ if (!uuid.isEmpty()) {
+ return Resource.create(Attributes.of(HOST_ID, uuid));
+ }
+ }
+ break;
}
- return lines;
- } catch (IOException e) {
- logger.log(FINE, "Failed to read /etc/machine-id", e);
- return emptyList();
}
+ logger.fine("Failed to read macOS host id: no IOPlatformUUID found in ioreg output");
+ return Resource.empty();
}
private Resource readWindowsGuid() {
@@ -120,19 +191,65 @@ private Resource readWindowsGuid() {
for (String line : lines) {
if (line.contains("MachineGuid")) {
+ // line looks like: MachineGuid REG_SZ 0123456789ABCDEF
String[] parts = line.trim().split("\\s+");
if (parts.length == 3) {
return Resource.create(Attributes.of(HOST_ID, parts[2]));
}
}
}
- logger.fine("Failed to read Windows registry: No MachineGuid found in output: " + lines);
+ logger.fine("Failed to read Windows registry: No MachineGuid found in output");
return Resource.empty();
}
private static List queryWindowsRegistry() {
+ List command = new ArrayList<>();
+ command.add(windowsRegPath());
+ command.addAll(WINDOWS_REGISTRY_QUERY_ARGS);
+ return runCommand(command);
+ }
+
+ private static String windowsRegPath() {
+ String root = null;
+ for (String envVar : WINDOWS_ROOT_ENV_VARS) {
+ root = System.getenv(envVar);
+ if (root != null && !root.isEmpty()) {
+ break;
+ }
+ }
+ if (root == null || root.isEmpty()) {
+ root = WINDOWS_ROOT_FALLBACK;
+ }
+ return root + "\\System32\\reg.exe";
+ }
+
+ @Nullable
+ private static String firstNonBlankLine(List lines) {
+ for (String line : lines) {
+ String trimmed = line.trim();
+ if (!trimmed.isEmpty()) {
+ return trimmed;
+ }
+ }
+ return null;
+ }
+
+ private static List readFileLines(Path path) {
+ try {
+ List lines = Files.readAllLines(path);
+ if (lines.isEmpty()) {
+ logger.log(FINE, "Failed to read {0}: empty file", path);
+ }
+ return lines;
+ } catch (IOException e) {
+ logger.log(FINE, "Failed to read " + path, e);
+ return emptyList();
+ }
+ }
+
+ private static List runCommand(List command) {
try {
- ProcessBuilder processBuilder = new ProcessBuilder("cmd", "/c", REGISTRY_QUERY);
+ ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
@@ -140,7 +257,9 @@ private static List queryWindowsRegistry() {
int exitedValue = process.waitFor();
if (exitedValue != 0) {
logger.fine(
- "Failed to read Windows registry. Exit code: "
+ "Failed to run command "
+ + command
+ + ". Exit code: "
+ exitedValue
+ " Output: "
+ String.join("\n", output));
@@ -153,7 +272,7 @@ private static List queryWindowsRegistry() {
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
- logger.log(FINE, "Failed to read Windows registry", e);
+ logger.log(FINE, "Failed to run command " + command, e);
return emptyList();
}
}
diff --git a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostIdResourceTest.java b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostIdResourceTest.java
index e30b17280ee3..aa8300de7628 100644
--- a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostIdResourceTest.java
+++ b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostIdResourceTest.java
@@ -33,8 +33,8 @@ class HostIdResourceTest {
@ParameterizedTest
@MethodSource("createResourceLinuxCases")
- void createResourceLinux(String expectedValue, Function> pathReader) {
- HostIdResource hostIdResource = new HostIdResource(() -> "linux", pathReader, null);
+ void createResourceLinux(String expectedValue, Function> fileReader) {
+ HostIdResource hostIdResource = new HostIdResource(() -> "linux", fileReader, null, null);
assertHostId(expectedValue, hostIdResource);
}
@@ -42,6 +42,14 @@ private static Stream createResourceLinuxCases() {
return Stream.of(
argumentSet(
"default", "test", (Function>) path -> singletonList("test")),
+ argumentSet(
+ "dbus fallback",
+ "dbus-id",
+ (Function>)
+ path ->
+ path.endsWith("machine-id") && path.toString().contains("dbus")
+ ? singletonList("dbus-id")
+ : emptyList()),
argumentSet(
"empty file or error reading",
null,
@@ -52,7 +60,7 @@ private static Stream createResourceLinuxCases() {
@MethodSource("createResourceWindowsCases")
void createResourceWindows(String expectedValue, Supplier> queryWindowsRegistry) {
HostIdResource hostIdResource =
- new HostIdResource(() -> "Windows 95", null, queryWindowsRegistry);
+ new HostIdResource(() -> "Windows 95", null, queryWindowsRegistry, null);
assertHostId(expectedValue, hostIdResource);
}
@@ -69,6 +77,61 @@ private static Stream createResourceWindowsCases() {
argumentSet("short output", null, (Supplier>) Collections::emptyList));
}
+ @ParameterizedTest
+ @MethodSource("createResourceMacOsCases")
+ void createResourceMacOs(String expectedValue, Function, List> command) {
+ HostIdResource hostIdResource = new HostIdResource(() -> "Mac OS X", null, null, command);
+ assertHostId(expectedValue, hostIdResource);
+ }
+
+ private static Stream createResourceMacOsCases() {
+ return Stream.of(
+ argumentSet(
+ "default",
+ "0123456789ABCDEF",
+ (Function, List>)
+ command ->
+ asList(
+ "+-o IOPlatformExpertDevice ",
+ " \"IOPlatformUUID\" = \"0123456789ABCDEF\"")),
+ argumentSet(
+ "no uuid",
+ null,
+ (Function, List>)
+ command -> singletonList("+-o IOPlatformExpertDevice")),
+ argumentSet(
+ "empty output", null, (Function, List>) command -> emptyList()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("createResourceBsdCases")
+ void createResourceBsd(
+ String expectedValue,
+ Function> fileReader,
+ Function, List> command) {
+ HostIdResource hostIdResource = new HostIdResource(() -> "FreeBSD", fileReader, null, command);
+ assertHostId(expectedValue, hostIdResource);
+ }
+
+ private static Stream createResourceBsdCases() {
+ return Stream.of(
+ argumentSet(
+ "hostid file",
+ "hostid-value",
+ (Function>) path -> singletonList("hostid-value"),
+ (Function, List>) command -> emptyList()),
+ argumentSet(
+ "kenv fallback",
+ "kenv-uuid",
+ (Function>) path -> emptyList(),
+ (Function, List>) command -> singletonList("kenv-uuid")),
+ argumentSet(
+ "nothing found",
+ null,
+ (Function>) path -> emptyList(),
+ (Function, List>) command -> emptyList()));
+ }
+
private static void assertHostId(String expectedValue, HostIdResource hostIdResource) {
MapAssert, Object> that =
assertThat(hostIdResource.createResource().getAttributes().asMap());