Refactor `io.opentelemetry.instrumentation.resources.ContainerResource` to avoid using null (#6889)

While I was looking at issues
open-telemetry/opentelemetry-java-instrumentation#6694 and
open-telemetry/opentelemetry-java#2337, I saw that the code in
`io.opentelemetry.instrumentation.resources.ContainerResource` used
`null` several times as return value which isn't safe. Nowadays,
`Optional` is better suited to signal the absence of a result, so I
refactored `ContainerResource` to use `Optional`s instead of null.

On the way, I also refactored this class's unit tests into parameterised
tests to reduce test code duplication. These improvements should help
implementing a solution to
open-telemetry/opentelemetry-java-instrumentation#6694.

Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
This commit is contained in:
Etienne Dysli Metref 2022-10-19 05:00:43 +02:00 committed by GitHub
parent 465d4d3f9b
commit 6fb1f00241
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 74 additions and 93 deletions

View File

@ -12,12 +12,10 @@ import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
/** Factory for {@link Resource} retrieving Container ID information. */ /** Factory for {@link Resource} retrieving Container ID information. */
@ -35,13 +33,11 @@ public final class ContainerResource {
// package private for testing // package private for testing
static Resource buildResource(Path path) { static Resource buildResource(Path path) {
String containerId = extractContainerId(path); return extractContainerId(path)
.map(
if (containerId == null || containerId.isEmpty()) { containerId ->
return Resource.empty(); Resource.create(Attributes.of(ResourceAttributes.CONTAINER_ID, containerId)))
} else { .orElseGet(Resource::empty);
return Resource.create(Attributes.of(ResourceAttributes.CONTAINER_ID, containerId));
}
} }
/** Returns resource with container information. */ /** Returns resource with container information. */
@ -57,33 +53,28 @@ public final class ContainerResource {
* @return containerId * @return containerId
*/ */
@IgnoreJRERequirement @IgnoreJRERequirement
@Nullable private static Optional<String> extractContainerId(Path cgroupFilePath) {
private static String extractContainerId(Path cgroupFilePath) {
if (!Files.exists(cgroupFilePath) || !Files.isReadable(cgroupFilePath)) { if (!Files.exists(cgroupFilePath) || !Files.isReadable(cgroupFilePath)) {
return null; return Optional.empty();
} }
try (Stream<String> lines = Files.lines(cgroupFilePath)) { try (Stream<String> lines = Files.lines(cgroupFilePath)) {
Optional<String> value = return lines
lines
.filter(line -> !line.isEmpty()) .filter(line -> !line.isEmpty())
.map(ContainerResource::getIdFromLine) .map(ContainerResource::getIdFromLine)
.filter(Objects::nonNull) .filter(Optional::isPresent)
.findFirst(); .findFirst()
if (value.isPresent()) { .orElse(Optional.empty());
return value.get();
}
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.WARNING, "Unable to read file", e); logger.log(Level.WARNING, "Unable to read file", e);
} }
return null; return Optional.empty();
} }
@Nullable private static Optional<String> getIdFromLine(String line) {
private static String getIdFromLine(String line) {
// This cgroup output line should have the container id in it // This cgroup output line should have the container id in it
int lastSlashIdx = line.lastIndexOf('/'); int lastSlashIdx = line.lastIndexOf('/');
if (lastSlashIdx < 0) { if (lastSlashIdx < 0) {
return null; return Optional.empty();
} }
String containerId; String containerId;
@ -105,16 +96,16 @@ public final class ContainerResource {
endIdx = lastSection.length(); endIdx = lastSection.length();
} }
if (startIdx > endIdx) { if (startIdx > endIdx) {
return null; return Optional.empty();
} }
containerId = lastSection.substring(startIdx, endIdx); containerId = lastSection.substring(startIdx, endIdx);
} }
if (OtelEncodingUtils.isValidBase16String(containerId) && !containerId.isEmpty()) { if (OtelEncodingUtils.isValidBase16String(containerId) && !containerId.isEmpty()) {
return containerId; return Optional.of(containerId);
} else { } else {
return null; return Optional.empty();
} }
} }

View File

@ -7,6 +7,7 @@ package io.opentelemetry.instrumentation.resources;
import static io.opentelemetry.instrumentation.resources.ContainerResource.buildResource; import static io.opentelemetry.instrumentation.resources.ContainerResource.buildResource;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
@ -14,85 +15,74 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
class ContainerResourceTest { class ContainerResourceTest {
@Test @ParameterizedTest
void buildResource_Invalid(@TempDir Path tempFolder) throws IOException { @ValueSource(
strings = {
// invalid containerId (non-hex) // invalid containerId (non-hex)
Path cgroup = "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23zzzz",
createCgroup(
tempFolder.resolve("cgroup1"),
"13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23zzzz");
assertThat(buildResource(cgroup)).isEqualTo(Resource.empty());
// unrecognized format (last "-" is after last ".") // unrecognized format (last "-" is after last ".")
cgroup = "13:name=systemd:/podruntime/docker/kubepods/ac679f8.a8319c8cf7d38e1adf263bc08-d23zzzz"
createCgroup( })
tempFolder.resolve("cgroup1"), void buildResource_returnsEmptyResource_whenContainerIdIsInvalid(
"13:name=systemd:/podruntime/docker/kubepods/ac679f8.a8319c8cf7d38e1adf263bc08-d23zzzz"); String line, @TempDir Path tempFolder) throws IOException {
assertThat(buildResource(cgroup)).isEqualTo(Resource.empty()); Path cgroup = createCgroup(tempFolder.resolve("cgroup"), line);
// test invalid file
cgroup = tempFolder.resolve("DoesNotExist");
assertThat(buildResource(cgroup)).isEqualTo(Resource.empty()); assertThat(buildResource(cgroup)).isEqualTo(Resource.empty());
} }
@Test @Test
void buildResource_Valid(@TempDir Path tempFolder) throws IOException { void buildResource_returnsEmptyResource_whenFileDoesNotExist(@TempDir Path tempFolder) {
Path cgroup = tempFolder.resolve("DoesNotExist");
assertThat(buildResource(cgroup)).isEqualTo(Resource.empty());
}
@ParameterizedTest
@MethodSource("validLines")
void buildResource_extractsContainerIdFromValidLines(
String line, String expectedContainerId, @TempDir Path tempFolder) throws IOException {
Path cgroup = createCgroup(tempFolder.resolve("cgroup"), line);
assertThat(getContainerId(buildResource(cgroup))).isEqualTo(expectedContainerId);
}
static Stream<Arguments> validLines() {
return Stream.of(
// with suffix // with suffix
Path cgroup = arguments(
createCgroup( "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23.aaaa",
tempFolder.resolve("cgroup1"), "ac679f8a8319c8cf7d38e1adf263bc08d23"),
"13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23.aaaa");
assertThat(getContainerId(buildResource(cgroup)))
.isEqualTo("ac679f8a8319c8cf7d38e1adf263bc08d23");
// with prefix and suffix // with prefix and suffix
Path cgroup2 = arguments(
createCgroup( "13:name=systemd:/podruntime/docker/kubepods/crio-dc679f8a8319c8cf7d38e1adf263bc08d23.stuff",
tempFolder.resolve("cgroup2"), "dc679f8a8319c8cf7d38e1adf263bc08d23"),
"13:name=systemd:/podruntime/docker/kubepods/crio-dc679f8a8319c8cf7d38e1adf263bc08d23.stuff");
assertThat(getContainerId(buildResource(cgroup2)))
.isEqualTo("dc679f8a8319c8cf7d38e1adf263bc08d23");
// just container id // just container id
Path cgroup3 = arguments(
createCgroup( "13:name=systemd:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356",
tempFolder.resolve("cgroup3"), "d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356"),
"13:name=systemd:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356");
assertThat(getContainerId(buildResource(cgroup3)))
.isEqualTo("d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356");
// with prefix // with prefix
Path cgroup4 = arguments(
createCgroup(
tempFolder.resolve("cgroup4"),
"//\n" "//\n"
+ "1:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23" + "1:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23"
+ "2:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23" + "2:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23"
+ "3:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23"); + "3:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23",
assertThat(getContainerId(buildResource(cgroup4))) "dc579f8a8319c8cf7d38e1adf263bc08d23"),
.isEqualTo("dc579f8a8319c8cf7d38e1adf263bc08d23");
// with two dashes in prefix // with two dashes in prefix
Path cgroup5 = arguments(
createCgroup( "11:perf_event:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod4415fd05_2c0f_4533_909b_f2180dca8d7c.slice/cri-containerd-713a77a26fe2a38ebebd5709604a048c3d380db1eb16aa43aca0b2499e54733c.scope",
tempFolder.resolve("cgroup5"), "713a77a26fe2a38ebebd5709604a048c3d380db1eb16aa43aca0b2499e54733c"),
"11:perf_event:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod4415fd05_2c0f_4533_909b_f2180dca8d7c.slice/cri-containerd-713a77a26fe2a38ebebd5709604a048c3d380db1eb16aa43aca0b2499e54733c.scope");
assertThat(getContainerId(buildResource(cgroup5)))
.isEqualTo("713a77a26fe2a38ebebd5709604a048c3d380db1eb16aa43aca0b2499e54733c");
// with colon, env: k8s v1.24.0, the cgroupDriver by systemd(default), and container is // with colon, env: k8s v1.24.0, the cgroupDriver by systemd(default), and container is
// cri-containerd v1.6.8 // cri-containerd v1.6.8
Path cgroup6 = arguments(
createCgroup( "11:devices:/system.slice/containerd.service/kubepods-pod87a18a64_b74a_454a_b10b_a4a36059d0a3.slice:cri-containerd:05c48c82caff3be3d7f1e896981dd410e81487538936914f32b624d168de9db0",
tempFolder.resolve("cgroup6"), "05c48c82caff3be3d7f1e896981dd410e81487538936914f32b624d168de9db0"));
"11:devices:/system.slice/containerd.service/kubepods-pod87a18a64_b74a_454a_b10b_a4a36059d0a3.slice:cri-containerd:05c48c82caff3be3d7f1e896981dd410e81487538936914f32b624d168de9db0");
assertThat(getContainerId(buildResource(cgroup6)))
.isEqualTo("05c48c82caff3be3d7f1e896981dd410e81487538936914f32b624d168de9db0");
} }
private static String getContainerId(Resource resource) { private static String getContainerId(Resource resource) {