From 002933282c16d936a6efef78c102bdff87a87972 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Wed, 13 Mar 2024 00:01:41 +0100 Subject: [PATCH] add host.id resource provider (#10627) Co-authored-by: Jason Plumb --- instrumentation/resources/library/README.md | 8 + .../resources/HostIdResourceProvider.java | 184 ++++++++++++++++++ .../resources/HostIdResourceProviderTest.java | 125 ++++++++++++ 3 files changed, 317 insertions(+) create mode 100644 instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostIdResourceProvider.java create mode 100644 instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostIdResourceProviderTest.java diff --git a/instrumentation/resources/library/README.md b/instrumentation/resources/library/README.md index 3a3923d65b54..ca7a790ec2cc 100644 --- a/instrumentation/resources/library/README.md +++ b/instrumentation/resources/library/README.md @@ -26,6 +26,14 @@ Implemented attributes: - `host.name` - `host.arch` +Provider: `io.opentelemetry.instrumentation.resources.HostIdResourceProvider` + +Specification: + +Implemented attributes: + +- `host.id` + ### Operating System Provider: `io.opentelemetry.instrumentation.resources.OsResource` diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostIdResourceProvider.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostIdResourceProvider.java new file mode 100644 index 000000000000..1c7b366ce04c --- /dev/null +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostIdResourceProvider.java @@ -0,0 +1,184 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.resources; + +import static java.util.logging.Level.FINE; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ConditionalResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ResourceAttributes; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.logging.Logger; + +/** + * {@link ResourceProvider} for automatically configuring host.id according to the + * semantic conventions + */ +public final class HostIdResourceProvider implements ConditionalResourceProvider { + + private static final Logger logger = Logger.getLogger(HostIdResourceProvider.class.getName()); + + public static final String REGISTRY_QUERY = + "reg query HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid"; + + private final Supplier getOsType; + + private final Function> machineIdReader; + + private final Supplier> queryWindowsRegistry; + + public HostIdResourceProvider() { + this( + HostIdResourceProvider::getOsTypeSystemProperty, + HostIdResourceProvider::readMachineIdFile, + HostIdResourceProvider::queryWindowsRegistry); + } + + // Visible for testing + + HostIdResourceProvider( + Supplier getOsType, + Function> machineIdReader, + Supplier> queryWindowsRegistry) { + this.getOsType = getOsType; + this.machineIdReader = machineIdReader; + this.queryWindowsRegistry = queryWindowsRegistry; + } + + @Override + public Resource createResource(ConfigProperties config) { + if (runningWindows()) { + return readWindowsGuid(); + } + if (runningLinux()) { + return readLinuxMachineId(); + } + logger.log(FINE, "Unsupported OS type: {0}", getOsType.get()); + return Resource.empty(); + } + + private boolean runningLinux() { + return getOsType.get().toLowerCase(Locale.ROOT).equals("linux"); + } + + private boolean runningWindows() { + return getOsType.get().startsWith("Windows"); + } + + // see + // https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/SystemUtils.java + // for values + private static String getOsTypeSystemProperty() { + return System.getProperty("os.name", ""); + } + + private Resource readLinuxMachineId() { + Path path = FileSystems.getDefault().getPath("/etc/machine-id"); + List lines = machineIdReader.apply(path); + if (lines.isEmpty()) { + return Resource.empty(); + } + return Resource.create(Attributes.of(ResourceAttributes.HOST_ID, lines.get(0))); + } + + 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"); + } + return lines; + } catch (IOException e) { + logger.log(FINE, "Failed to read /etc/machine-id", e); + return Collections.emptyList(); + } + } + + private Resource readWindowsGuid() { + List lines = queryWindowsRegistry.get(); + + for (String line : lines) { + if (line.contains("MachineGuid")) { + String[] parts = line.trim().split("\\s+"); + if (parts.length == 3) { + return Resource.create(Attributes.of(ResourceAttributes.HOST_ID, parts[2])); + } + } + } + logger.fine("Failed to read Windows registry: No MachineGuid found in output: " + lines); + return Resource.empty(); + } + + private static List queryWindowsRegistry() { + try { + ProcessBuilder processBuilder = new ProcessBuilder("cmd", "/c", REGISTRY_QUERY); + processBuilder.redirectErrorStream(true); + Process process = processBuilder.start(); + + List output = getProcessOutput(process); + int exitedValue = process.waitFor(); + if (exitedValue != 0) { + logger.fine( + "Failed to read Windows registry. Exit code: " + + exitedValue + + " Output: " + + String.join("\n", output)); + + return Collections.emptyList(); + } + + return output; + } catch (IOException | InterruptedException e) { + logger.log(FINE, "Failed to read Windows registry", e); + return Collections.emptyList(); + } + } + + public static List getProcessOutput(Process process) throws IOException { + List result = new ArrayList<>(); + + try (BufferedReader processOutputReader = + new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + String readLine; + + while ((readLine = processOutputReader.readLine()) != null) { + result.add(readLine); + } + } + return result; + } + + @Override + public boolean shouldApply(ConfigProperties config, Resource existing) { + return !config + .getMap("otel.resource.attributes") + .containsKey(ResourceAttributes.HOST_ID.getKey()) + && existing.getAttribute(ResourceAttributes.HOST_ID) == null; + } + + @Override + public int order() { + // Run after cloud provider resource providers + return Integer.MAX_VALUE - 1; + } +} diff --git a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostIdResourceProviderTest.java b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostIdResourceProviderTest.java new file mode 100644 index 000000000000..100b8cab2a53 --- /dev/null +++ b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostIdResourceProviderTest.java @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ResourceAttributes; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.assertj.core.api.MapAssert; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +class HostIdResourceProviderTest { + + private static class LinuxTestCase { + private final String name; + private final String expectedValue; + private final Function> pathReader; + + private LinuxTestCase( + String name, String expectedValue, Function> pathReader) { + this.name = name; + this.expectedValue = expectedValue; + this.pathReader = pathReader; + } + } + + private static class WindowsTestCase { + private final String name; + private final String expectedValue; + private final Supplier> queryWindowsRegistry; + + private WindowsTestCase( + String name, String expectedValue, Supplier> queryWindowsRegistry) { + this.name = name; + this.expectedValue = expectedValue; + this.queryWindowsRegistry = queryWindowsRegistry; + } + } + + @TestFactory + Collection createResourceLinux() { + return Stream.of( + new LinuxTestCase("default", "test", path -> Collections.singletonList("test")), + new LinuxTestCase("empty file or error reading", null, path -> Collections.emptyList())) + .map( + testCase -> + DynamicTest.dynamicTest( + testCase.name, + () -> { + HostIdResourceProvider provider = + new HostIdResourceProvider(() -> "linux", testCase.pathReader, null); + + assertHostId(testCase.expectedValue, provider); + })) + .collect(Collectors.toList()); + } + + @TestFactory + Collection createResourceWindows() { + return Stream.of( + new WindowsTestCase( + "default", + "test", + () -> + Arrays.asList( + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography", + " MachineGuid REG_SZ test")), + new WindowsTestCase("short output", null, Collections::emptyList)) + .map( + testCase -> + DynamicTest.dynamicTest( + testCase.name, + () -> { + HostIdResourceProvider provider = + new HostIdResourceProvider( + () -> "Windows 95", null, testCase.queryWindowsRegistry); + + assertHostId(testCase.expectedValue, provider); + })) + .collect(Collectors.toList()); + } + + private static void assertHostId(String expectedValue, HostIdResourceProvider provider) { + MapAssert, Object> that = + assertThat(provider.createResource(null).getAttributes().asMap()); + + if (expectedValue == null) { + that.isEmpty(); + } else { + that.containsEntry(ResourceAttributes.HOST_ID, expectedValue); + } + } + + @Test + void shouldApply() { + HostIdResourceProvider provider = new HostIdResourceProvider(); + assertThat( + provider.shouldApply( + DefaultConfigProperties.createFromMap(Collections.emptyMap()), + Resource.getDefault())) + .isTrue(); + assertThat( + provider.shouldApply( + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.resource.attributes", "host.id=foo")), + null)) + .isFalse(); + } +}