From 2cb461d4aef16f1ac1c5e67edc2fb41f90ed96a3 Mon Sep 17 00:00:00 2001 From: lo-jason <9317348+lo-jason@users.noreply.github.com> Date: Wed, 21 Jul 2021 16:40:57 -0700 Subject: [PATCH] #3308 Support adding container.id to resource metadata (#3321) * Support adding container.id to resource metadta * Address review comments * Address review feedback * Change containerResource to cached Resource factory * Make method private * Change to Paths and Files * Fix bad merge and change regex * Remove debug code, remove regex * Add nullable annotation Co-authored-by: Anuraag Agrawal --- .../opentelemetry-sdk-extension-resources.txt | 10 +- .../resources/ContainerResource.java | 105 ++++++++++++++++++ .../resources/ContainerResourceProvider.java | 18 +++ ...try.sdk.autoconfigure.spi.ResourceProvider | 1 + .../resources/ContainerResourceTest.java | 84 ++++++++++++++ 5 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ContainerResource.java create mode 100644 sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ContainerResourceProvider.java create mode 100644 sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/ContainerResourceTest.java diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-resources.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-resources.txt index df26146497b..18cfbddfbe9 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-resources.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-resources.txt @@ -1,2 +1,10 @@ Comparing source compatibility of against -No changes. \ No newline at end of file ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.extension.resources.ContainerResource (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.resources.Resource get() ++++ NEW CLASS: PUBLIC(+) io.opentelemetry.sdk.extension.resources.ContainerResourceProvider (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW CONSTRUCTOR: PUBLIC(+) ContainerResourceProvider() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.Resource createResource(io.opentelemetry.sdk.autoconfigure.ConfigProperties) diff --git a/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ContainerResource.java b/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ContainerResource.java new file mode 100644 index 00000000000..861c378fbfa --- /dev/null +++ b/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ContainerResource.java @@ -0,0 +1,105 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.internal.OtelEncodingUtils; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; + +/** Factory for {@link Resource} retrieving Container ID information. */ +public final class ContainerResource { + + private static final Logger logger = Logger.getLogger(ContainerResource.class.getName()); + private static final String UNIQUE_HOST_NAME_FILE_NAME = "/proc/self/cgroup"; + private static final Resource INSTANCE = buildSingleton(UNIQUE_HOST_NAME_FILE_NAME); + + @IgnoreJRERequirement + private static Resource buildSingleton(String uniqueHostNameFileName) { + // can't initialize this statically without running afoul of animalSniffer on paths + return buildResource(Paths.get(uniqueHostNameFileName)); + } + + // package private for testing + static Resource buildResource(Path path) { + String containerId = extractContainerId(path); + + if (containerId == null || containerId.isEmpty()) { + return Resource.empty(); + } else { + return Resource.create(Attributes.of(ResourceAttributes.CONTAINER_ID, containerId)); + } + } + + /** Returns resource with container information. */ + public static Resource get() { + return INSTANCE; + } + + /** + * Each line of cgroup file looks like "14:name=systemd:/docker/.../... A hex string is expected + * inside the last section separated by '/' Each segment of the '/' can contain metadata separated + * by either '.' (at beginning) or '-' (at end) + * + * @return containerId + */ + @IgnoreJRERequirement + @Nullable + private static String extractContainerId(Path cgroupFilePath) { + if (!Files.exists(cgroupFilePath) || !Files.isReadable(cgroupFilePath)) { + return null; + } + try (Stream lines = Files.lines(cgroupFilePath)) { + Optional value = + lines + .filter(line -> !line.isEmpty()) + .map(line -> getIdFromLine(line)) + .filter(Objects::nonNull) + .findFirst(); + if (value.isPresent()) { + return value.get(); + } + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to read file: " + e.getMessage()); + } + return null; + } + + @Nullable + private static String getIdFromLine(String line) { + // This cgroup output line should have the container id in it + int lastSlashIdx = line.lastIndexOf("/"); + if (lastSlashIdx < 0) { + return null; + } + + String lastSection = line.substring(lastSlashIdx + 1); + int startIdx = lastSection.indexOf("-"); + int endIdx = lastSection.lastIndexOf("."); + + String containerId = + lastSection.substring( + startIdx == -1 ? 0 : startIdx + 1, endIdx == -1 ? lastSection.length() : endIdx); + if (OtelEncodingUtils.isValidBase16String(containerId) && !containerId.isEmpty()) { + return containerId; + } else { + return null; + } + } + + private ContainerResource() {} +} diff --git a/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ContainerResourceProvider.java b/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ContainerResourceProvider.java new file mode 100644 index 00000000000..bb9adb27698 --- /dev/null +++ b/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ContainerResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link ResourceProvider}. */ +public class ContainerResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return ContainerResource.get(); + } +} diff --git a/sdk-extensions/resources/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider b/sdk-extensions/resources/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider index b7120017c45..8ac23c697a4 100644 --- a/sdk-extensions/resources/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider +++ b/sdk-extensions/resources/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider @@ -1,3 +1,4 @@ +io.opentelemetry.sdk.extension.resources.ContainerResourceProvider io.opentelemetry.sdk.extension.resources.HostResourceProvider io.opentelemetry.sdk.extension.resources.OsResourceProvider io.opentelemetry.sdk.extension.resources.ProcessResourceProvider diff --git a/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/ContainerResourceTest.java b/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/ContainerResourceTest.java new file mode 100644 index 00000000000..13d05b8ce80 --- /dev/null +++ b/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/ContainerResourceTest.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import static io.opentelemetry.sdk.extension.resources.ContainerResource.buildResource; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class ContainerResourceTest { + + // Invalid because ID is not a hex string + private static final String INVALID_CGROUP_LINE_1 = + "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23zzzz"; + + // with suffix + private static final String CGROUP_LINE_1 = + "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23.aaaa"; + private static final String EXPECTED_CGROUP_1 = "ac679f8a8319c8cf7d38e1adf263bc08d23"; + + // with prefix and suffix + private static final String CGROUP_LINE_2 = + "13:name=systemd:/podruntime/docker/kubepods/crio-dc679f8a8319c8cf7d38e1adf263bc08d23.stuff"; + private static final String EXPECTED_CGROUP_2 = "dc679f8a8319c8cf7d38e1adf263bc08d23"; + + // just container id + private static final String CGROUP_LINE_3 = + "13:name=systemd:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356"; + private static final String EXPECTED_CGROUP_3 = + "d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356"; + + // with prefix + private static final String CGROUP_LINE_4 = + "//\n" + + "1:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23" + + "2:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23" + + "3:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23"; + + private static final String EXPECTED_CGROUP_4 = "dc579f8a8319c8cf7d38e1adf263bc08d23"; + + @Test + public void testNegativeCases(@TempDir Path tempFolder) throws IOException { + // invalid containerId (non-hex) + Path cgroup = createCGroup(tempFolder.resolve("cgroup1"), INVALID_CGROUP_LINE_1); + assertThat(buildResource(cgroup)).isEqualTo(Resource.empty()); + + // test invalid file + cgroup = tempFolder.resolve("DoesNotExist"); + assertThat(buildResource(cgroup)).isEqualTo(Resource.empty()); + } + + @Test + public void testContainer(@TempDir Path tempFolder) throws IOException { + Path cgroup = createCGroup(tempFolder.resolve("cgroup1"), CGROUP_LINE_1); + assertThat(getContainerId(buildResource(cgroup))).isEqualTo(EXPECTED_CGROUP_1); + + Path cgroup2 = createCGroup(tempFolder.resolve("cgroup2"), CGROUP_LINE_2); + assertThat(getContainerId(buildResource(cgroup2))).isEqualTo(EXPECTED_CGROUP_2); + + Path cgroup3 = createCGroup(tempFolder.resolve("cgroup3"), CGROUP_LINE_3); + assertThat(getContainerId(buildResource(cgroup3))).isEqualTo(EXPECTED_CGROUP_3); + + Path cgroup4 = createCGroup(tempFolder.resolve("cgroup4"), CGROUP_LINE_4); + assertThat(getContainerId(buildResource(cgroup4))).isEqualTo(EXPECTED_CGROUP_4); + } + + private static String getContainerId(Resource resource) { + return resource.getAttributes().get(ResourceAttributes.CONTAINER_ID); + } + + private static Path createCGroup(Path path, String line) throws IOException { + return Files.write(path, line.getBytes(StandardCharsets.UTF_8)); + } +}