Skip to content

Commit

Permalink
Support cgroup v2 (#7167)
Browse files Browse the repository at this point in the history
This resolves #6694.

We've been tracking the update to cgroup version support and want to get
ahead of the widespread usage. The surface of the existing
`ContainerResource` has not changed, but its internals have been
factored out to two "extractor" utilities -- one that understands cgroup
v1 and another for v2. v1 is attempted and, if successful, the result is
used. If v1 fails, then the `ContainerResource` will fall back to v2.

As mentioned in #6694, the approach taken in this PR is borrowed from
[this SO
post](https://stackoverflow.com/questions/68816329/how-to-get-docker-container-id-from-within-the-container-with-cgroup-v2)
combined with local experimentation on docker desktop on a Mac, which
already uses cgroup2 v2.
  • Loading branch information
breedx-splk committed Nov 15, 2022
1 parent 33b0b58 commit b09fb67
Show file tree
Hide file tree
Showing 6 changed files with 395 additions and 148 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.resources;

import io.opentelemetry.api.internal.OtelEncodingUtils;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;

/** Utility for extracting the container ID from runtimes inside cgroup v1 containers. */
final class CgroupV1ContainerIdExtractor {

private static final Logger logger =
Logger.getLogger(CgroupV1ContainerIdExtractor.class.getName());
static final Path V1_CGROUP_PATH = Paths.get("/proc/self/cgroup");
private final ContainerResource.Filesystem filesystem;

CgroupV1ContainerIdExtractor() {
this(ContainerResource.FILESYSTEM_INSTANCE);
}

// Exists for testing
CgroupV1ContainerIdExtractor(ContainerResource.Filesystem filesystem) {
this.filesystem = filesystem;
}

/**
* 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
*/
Optional<String> extractContainerId() {
if (!filesystem.isReadable(V1_CGROUP_PATH)) {
return Optional.empty();
}
try (Stream<String> lines = filesystem.lines(V1_CGROUP_PATH)) {
return lines
.filter(line -> !line.isEmpty())
.map(CgroupV1ContainerIdExtractor::getIdFromLine)
.filter(Optional::isPresent)
.findFirst()
.orElse(Optional.empty());
} catch (Exception e) {
logger.log(Level.WARNING, "Unable to read file", e);
}
return Optional.empty();
}

private static Optional<String> getIdFromLine(String line) {
// This cgroup output line should have the container id in it
int lastSlashIdx = line.lastIndexOf('/');
if (lastSlashIdx < 0) {
return Optional.empty();
}

String containerId;

String lastSection = line.substring(lastSlashIdx + 1);
int colonIdx = lastSection.lastIndexOf(':');

if (colonIdx != -1) {
// since containerd v1.5.0+, containerId is divided by the last colon when the cgroupDriver is
// systemd:
// https://github.com/containerd/containerd/blob/release/1.5/pkg/cri/server/helpers_linux.go#L64
containerId = lastSection.substring(colonIdx + 1);
} else {
int startIdx = lastSection.lastIndexOf('-');
int endIdx = lastSection.lastIndexOf('.');

startIdx = startIdx == -1 ? 0 : startIdx + 1;
if (endIdx == -1) {
endIdx = lastSection.length();
}
if (startIdx > endIdx) {
return Optional.empty();
}

containerId = lastSection.substring(startIdx, endIdx);
}

if (OtelEncodingUtils.isValidBase16String(containerId) && !containerId.isEmpty()) {
return Optional.of(containerId);
} else {
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.resources;

import static java.util.Optional.empty;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Utility for extracting the container ID from runtimes inside cgroup v2 containers. */
class CgroupV2ContainerIdExtractor {

private static final Logger logger =
Logger.getLogger(CgroupV2ContainerIdExtractor.class.getName());

static final Path V2_CGROUP_PATH = Paths.get("/proc/self/mountinfo");
private static final Pattern CONTAINER_RE =
Pattern.compile(".*/docker/containers/([0-9a-f]{64})/.*");

private final ContainerResource.Filesystem filesystem;

CgroupV2ContainerIdExtractor() {
this(ContainerResource.FILESYSTEM_INSTANCE);
}

// Exists for testing
CgroupV2ContainerIdExtractor(ContainerResource.Filesystem filesystem) {
this.filesystem = filesystem;
}

Optional<String> extractContainerId() {
if (!filesystem.isReadable(V2_CGROUP_PATH)) {
return empty();
}
try {
return filesystem
.lines(V2_CGROUP_PATH)
.map(CONTAINER_RE::matcher)
.filter(Matcher::matches)
.findFirst()
.map(matcher -> matcher.group(1));
} catch (IOException e) {
logger.log(Level.WARNING, "Unable to read v2 cgroup path", e);
}
return empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,106 +5,75 @@

package io.opentelemetry.instrumentation.resources;

import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.CONTAINER_ID;

import com.google.errorprone.annotations.MustBeClosed;
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.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;

/** Factory for {@link Resource} retrieving Container ID information. */
/**
* Factory for {@link Resource} retrieving Container ID information. It supports both cgroup v1 and
* v2 runtimes.
*/
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);
static final Filesystem FILESYSTEM_INSTANCE = new Filesystem();
private static final Resource INSTANCE = buildSingleton();

private static Resource buildSingleton(String uniqueHostNameFileName) {
private static Resource buildSingleton() {
// can't initialize this statically without running afoul of animalSniffer on paths
return buildResource(Paths.get(uniqueHostNameFileName));
return new ContainerResource().buildResource();
}

// package private for testing
static Resource buildResource(Path path) {
return extractContainerId(path)
.map(
containerId ->
Resource.create(Attributes.of(ResourceAttributes.CONTAINER_ID, containerId)))
.orElseGet(Resource::empty);
private final CgroupV1ContainerIdExtractor v1Extractor;
private final CgroupV2ContainerIdExtractor v2Extractor;

private ContainerResource() {
this(new CgroupV1ContainerIdExtractor(), new CgroupV2ContainerIdExtractor());
}

/** Returns resource with container information. */
public static Resource get() {
return INSTANCE;
// Visible for testing
ContainerResource(
CgroupV1ContainerIdExtractor v1Extractor, CgroupV2ContainerIdExtractor v2Extractor) {
this.v1Extractor = v1Extractor;
this.v2Extractor = v2Extractor;
}

/**
* 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
*/
private static Optional<String> extractContainerId(Path cgroupFilePath) {
if (!Files.exists(cgroupFilePath) || !Files.isReadable(cgroupFilePath)) {
return Optional.empty();
}
try (Stream<String> lines = Files.lines(cgroupFilePath)) {
return lines
.filter(line -> !line.isEmpty())
.map(ContainerResource::getIdFromLine)
.filter(Optional::isPresent)
.findFirst()
.orElse(Optional.empty());
} catch (Exception e) {
logger.log(Level.WARNING, "Unable to read file", e);
}
return Optional.empty();
// Visible for testing
Resource buildResource() {
return getContainerId()
.map(id -> Resource.create(Attributes.of(CONTAINER_ID, id)))
.orElseGet(Resource::empty);
}

private static Optional<String> getIdFromLine(String line) {
// This cgroup output line should have the container id in it
int lastSlashIdx = line.lastIndexOf('/');
if (lastSlashIdx < 0) {
return Optional.empty();
private Optional<String> getContainerId() {
Optional<String> v1Result = v1Extractor.extractContainerId();
if (v1Result.isPresent()) {
return v1Result;
}
return v2Extractor.extractContainerId();
}

String containerId;

String lastSection = line.substring(lastSlashIdx + 1);
int colonIdx = lastSection.lastIndexOf(':');

if (colonIdx != -1) {
// since containerd v1.5.0+, containerId is divided by the last colon when the cgroupDriver is
// systemd:
// https://github.com/containerd/containerd/blob/release/1.5/pkg/cri/server/helpers_linux.go#L64
containerId = lastSection.substring(colonIdx + 1);
} else {
int startIdx = lastSection.lastIndexOf('-');
int endIdx = lastSection.lastIndexOf('.');
/** Returns resource with container information. */
public static Resource get() {
return INSTANCE;
}

startIdx = startIdx == -1 ? 0 : startIdx + 1;
if (endIdx == -1) {
endIdx = lastSection.length();
}
if (startIdx > endIdx) {
return Optional.empty();
}
// Exists for testing
static class Filesystem {

containerId = lastSection.substring(startIdx, endIdx);
boolean isReadable(Path path) {
return Files.isReadable(path);
}

if (OtelEncodingUtils.isValidBase16String(containerId) && !containerId.isEmpty()) {
return Optional.of(containerId);
} else {
return Optional.empty();
@MustBeClosed
Stream<String> lines(Path path) throws IOException {
return Files.lines(path);
}
}

private ContainerResource() {}
}
Loading

0 comments on commit b09fb67

Please sign in to comment.