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..cf1feb6878c --- /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.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.File; +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.regex.Matcher; +import java.util.regex.Pattern; +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 Path UNIQUE_HOST_NAME_FILE_NAME = Paths.get("/proc/self/cgroup"); + private static final Pattern HEX_EXTRACTOR = Pattern.compile("^([a-fA-F0-9]+)$"); + private static final Resource INSTANCE = buildResource(UNIQUE_HOST_NAME_FILE_NAME); + + // package private for testing + static Resource buildResource(Path path) { + Optional containerId = extractContainerId(path); + + if (containerId.isPresent()) { + return Resource.create(Attributes.of(ResourceAttributes.CONTAINER_ID, containerId.get())); + } else { + return Resource.empty(); + } + } + + /** 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 '.' or '-' + * + *

We see this with CRI-O "crio-abcdef1234567890ABCDEF.freetext", then use {@linkplain + * ContainerResource#HEX_EXTRACTOR} to extract the container hex id + * + *

package private for testing purposes + * + * @return containerId + */ + @IgnoreJRERequirement + @Nullable + @SuppressWarnings("DefaultCharset") + private static Optional extractContainerId(Path cgroupFilePath) { + if (!Files.exists(cgroupFilePath) || !Files.isReadable(cgroupFilePath)) { + return Optional.empty(); + } + try (Stream lines = Files.lines(cgroupFilePath)) { + return lines + .filter(line -> !line.isEmpty()) + .map(line -> getIdFromLine(line)) + .filter(Objects::nonNull) + .findFirst(); + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to read file: " + e.getMessage()); + } + return Optional.empty(); + } + + @SuppressWarnings("SystemOut") + private static String getIdFromLine(String line) { + // This cgroup output line should have the container id in it + System.out.println("Processing: " + line); + String[] sections = line.split(File.separator); + if (sections.length <= 1) { + return null; + } + + String lastSection = sections[sections.length - 1]; + int startIdx = lastSection.indexOf("-"); + int endIdx = lastSection.lastIndexOf("."); + + Matcher matcher = + HEX_EXTRACTOR.matcher( + lastSection.substring( + startIdx == -1 ? 0 : startIdx + 1, endIdx == -1 ? lastSection.length() : endIdx)); + if (matcher.matches() && matcher.group(1) != null && !matcher.group(1).isEmpty()) { + return matcher.group(1); + } + 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..09fdf01cdd0 --- /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); + } + + public static Path createCGroup(Path path, String line) throws IOException { + return Files.write(path, line.getBytes(StandardCharsets.UTF_8)); + } +}