Skip to content

Commit

Permalink
parse container ID with cgroups v2 (#3199)
Browse files Browse the repository at this point in the history
  • Loading branch information
SylvainJuge authored Jul 10, 2023
1 parent 259ebf5 commit d4a651f
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 60 deletions.
12 changes: 6 additions & 6 deletions .ci/updatecli.d/update-json-specs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ sources:
- findsubmatch:
pattern: "[0-9a-f]{40}"

cgroup_parsing.json:
container_metadata_discovery.json:
kind: file
spec:
file: https://raw.githubusercontent.com/elastic/apm/main/tests/agents/json-specs/cgroup_parsing.json
file: https://raw.githubusercontent.com/elastic/apm/main/tests/agents/json-specs/container_metadata_discovery.json
service_resource_inference.json:
kind: file
spec:
Expand Down Expand Up @@ -70,13 +70,13 @@ actions:
* https://github.com/elastic/apm/commit/{{ source "sha" }}
targets:
cgroup_parsing.json:
name: cgroup_parsing.json
container_metadata_discovery.json:
name: container_metadata_discovery.json
scmid: default
sourceid: cgroup_parsing.json
sourceid: container_metadata_discovery.json
kind: file
spec:
file: apm-agent-core/src/test/resources/json-specs/cgroup_parsing.json
file: apm-agent-core/src/test/resources/json-specs/container_metadata_discovery.json
service_resource_inference.json:
name: service_resource_inference.json
scmid: default
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ Use subheadings with the "=====" level for adding notes for unreleased changes:
=== Unreleased
[float]
===== Features
* Capture `container.id` for cgroups v2 - {pull}3199[#3199]
[float]
===== Bug fixes
* Fixed agent programmatic attach with immutable config - {pull}3170[#3170]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,18 @@
public class SystemInfo {
private static final Logger logger = LoggerFactory.getLogger(SystemInfo.class);

private static final String CONTAINER_UID_REGEX = "^[0-9a-fA-F]{64}$";
private static final String CONTAINER_REGEX_64 = "[0-9a-fA-F]{64}";
private static final String CONTAINER_UID_REGEX = "^" + CONTAINER_REGEX_64 + "$";
private static final String SHORTENED_UUID_PATTERN = "^[0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4,}";
private static final String AWS_FARGATE_UID_REGEX = "^[0-9a-fA-F]{32}\\-[0-9]{10}$";
private static final String POD_REGEX = "(?:^/kubepods[\\S]*/pod([^/]+)$)|(?:kubepods[^/]*-pod([^/]+)\\.slice)";

private static final String CGROUPV2_HOSTNAME_FILE = "/etc/hostname";
private static final Pattern CGROUPV2_CONTAINER_PATTERN = Pattern.compile("^.*(" + CONTAINER_REGEX_64 + ").*$");

private static final String SELF_CGROUP = "/proc/self/cgroup";
private static final String SELF_MOUNTINFO = "/proc/self/mountinfo";

/**
* Architecture of the system the agent is running on.
*/
Expand All @@ -58,6 +65,7 @@ public class SystemInfo {
/**
* Hostname configured manually through {@link co.elastic.apm.agent.configuration.CoreConfiguration#hostname}.
*/
@SuppressWarnings("JavadocReference")
@Nullable
private final String configuredHostname;

Expand Down Expand Up @@ -101,11 +109,13 @@ public SystemInfo(String architecture, @Nullable String configuredHostname, @Nul
/**
* Creates a {@link SystemInfo} containing auto-discovered info about the system.
* This method may block on reading files and executing external processes.
* @param configuredHostname hostname configured through the {@link co.elastic.apm.agent.configuration.CoreConfiguration#hostname} config
* @param timeoutMillis enables to limit the execution of the system discovery task
*
* @param configuredHostname hostname configured through the {@link co.elastic.apm.agent.configuration.CoreConfiguration#hostname} config
* @param timeoutMillis enables to limit the execution of the system discovery task
* @param serverlessConfiguration serverless config
* @return a future from which this system's info can be obtained
*/
@SuppressWarnings("JavadocReference")
public static SystemInfo create(final @Nullable String configuredHostname, final long timeoutMillis, ServerlessConfiguration serverlessConfiguration) {
final String osName = System.getProperty("os.name");
final String osArch = System.getProperty("os.arch");
Expand Down Expand Up @@ -136,7 +146,8 @@ static boolean isWindows(String osName) {
* Discover the current host's name. This method separates operating systems only to Windows and non-Windows,
* both in the executed hostname-discovery-command and the fallback environment variables.
* It always starts with execution of a command on an external process, so it may block up to the specified timeout.
* @param isWindows used to decide how hostname discovery should be executed
*
* @param isWindows used to decide how hostname discovery should be executed
* @param timeoutMillis limits the time this method may block on executing external commands
* @return the discovered hostname
*/
Expand Down Expand Up @@ -182,7 +193,8 @@ static String discoverHostnameThroughCommand(boolean isWindows, long timeoutMill
/**
* Tries to discover the current host name by executing the provided command in a spawned process.
* This method may block up to the specified timeout, waiting for the spawned process to terminate.
* @param cmd the hostname discovery command
*
* @param cmd the hostname discovery command
* @param timeoutMillis maximum time to allow to the provided command to execute
* @return the discovered hostname
*/
Expand Down Expand Up @@ -246,21 +258,9 @@ static String discoverHostnameThroughEnv(boolean isWindows) {
* @return container ID parsed from {@code /proc/self/cgroup} file lines, or {@code null} if can't find/read/parse file lines
*/
SystemInfo findContainerDetails() {
String containerId = null;
try {
Path path = FileSystems.getDefault().getPath("/proc/self/cgroup");
if (path.toFile().exists()) {
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
for (final String line : lines) {
parseContainerId(line);
if (container != null) {
containerId = container.getId();
break;
}
}
}
} catch (Throwable e) {
logger.warn("Failed to read/parse container ID from '/proc/self/cgroup'", e);
parseCgroupsFile(FileSystems.getDefault().getPath(SELF_CGROUP));
if (container == null) {
parseMountInfo(FileSystems.getDefault().getPath(SELF_MOUNTINFO));
}

try {
Expand All @@ -284,29 +284,68 @@ SystemInfo findContainerDetails() {
logger.warn("Failed to read environment variables for Kubernetes Downward API discovery", e);
}

logger.debug("container ID is {}", containerId);
logger.debug("container ID is {}", container != null ? container.getId() : null);
return this;
}

@Nullable
private void parseMountInfo(Path path) {
if (!Files.isRegularFile(path)) {
logger.debug("Could not parse container ID from '{}'", path);
return;
}
try {
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
parseCgroupsV2ContainerId(lines);
if (container != null) {
return;
}
logger.debug("Could not parse container ID from '{}' lines: {}", path, lines);
} catch (Throwable e) {
logger.warn(String.format("Failed to read/parse container ID from '%s'", path), e);
}
}

@Nullable
private void parseCgroupsFile(Path path) {
if(!Files.isRegularFile(path)){
logger.debug("Could not parse container ID from '{}'", path);
return;
}
try {
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
for (String line : lines) {
parseCgroupsLine(line);
if (container != null) {
return;
}
}
} catch (Throwable e) {
logger.warn(String.format("Failed to read/parse container ID from '%s'", path), e);
}
}

/**
* The virtual file /proc/self/cgroup lists the control groups that the process is a member of. Each line contains
* three colon-separated fields of the form hierarchy-ID:subsystem-list:cgroup-path.
*
* <p>
* Depending on the filesystem driver used for cgroup management, the cgroup-path will have
* one of the following formats in a Docker container:
*
* systemd: /system.slice/docker-<container-ID>.scope
* cgroupfs: /docker/<container-ID>
*
* In a Kubernetes pod, the cgroup path will look like:
*
* systemd: /kubepods.slice/kubepods-<QoS-class>.slice/kubepods-<QoS-class>-pod<pod-UID>.slice/<container-iD>.scope
* cgroupfs: /kubepods/<QoS-class>/pod<pod-UID>/<container-iD>
* </p>
* <pre>
* systemd: /system.slice/docker-<container-ID>.scope
* cgroupfs: /docker/<container-ID>
* </pre>
* In a Kubernetes pod, the cgroup path will look like:
* <pre>
* systemd: /kubepods.slice/kubepods-<QoS-class>.slice/kubepods-<QoS-class>-pod<pod-UID>.slice/<container-iD>.scope
* cgroupfs: /kubepods/<QoS-class>/pod<pod-UID>/<container-iD>
* </pre
*
* @param line a line from the /proc/self/cgroup file
* @return this SystemInfo object after parsing
*/
SystemInfo parseContainerId(String line) {
SystemInfo parseCgroupsLine(String line) {
final String[] fields = line.split(":", 3);
if (fields.length == 3) {
String cGroupPath = fields[2];
Expand Down Expand Up @@ -351,14 +390,38 @@ SystemInfo parseContainerId(String line) {
if (kubernetes != null ||
idPart.matches(CONTAINER_UID_REGEX) ||
idPart.matches(SHORTENED_UUID_PATTERN) ||
idPart.matches(AWS_FARGATE_UID_REGEX)) {
idPart.matches(AWS_FARGATE_UID_REGEX)) {
container = new Container(idPart);
}
}
}
if (container == null) {
logger.debug("Could not parse container ID from '/proc/self/cgroup' line: {}", line);
logger.debug("Could not parse container ID from line: {}", line);
}
return this;
}

/**
* @param lines lines from the /proc/self/mountinfo file
* @return this SystemInfo object after parsing
*/
SystemInfo parseCgroupsV2ContainerId(List<String> lines) {
for (String line : lines) {
int index = line.indexOf(CGROUPV2_HOSTNAME_FILE);
if (index > 0) {
String[] parts = line.split(" ");
if (parts.length > 3) {
Matcher matcher = CGROUPV2_CONTAINER_PATTERN.matcher(parts[3]);
if (matcher.matches() && matcher.groupCount() == 1) {
container = new Container(matcher.group(1));
}
}
}
}




return this;
}

Expand All @@ -373,23 +436,26 @@ public String getArchitecture() {
* Returns the hostname. If a non-empty hostname was configured manually, it will be returned.
* Otherwise, the automatically discovered hostname will be returned.
* If both are null or empty, this method returns {@code <unknown>}.
*
* @deprecated should only be used when communicating to APM Server of version lower than 7.4
*/
@Deprecated
@Deprecated
public String getHostname() {
if (configuredHostname != null && !configuredHostname.isEmpty()) {
return configuredHostname;
}
if (detectedHostname != null && !detectedHostname.isEmpty()) {
return detectedHostname;
}
return "<unknown>";
if (configuredHostname != null && !configuredHostname.isEmpty()) {
return configuredHostname;
}
if (detectedHostname != null && !detectedHostname.isEmpty()) {
return detectedHostname;
}
return "<unknown>";
}

/**
* The hostname manually configured through {@link co.elastic.apm.agent.configuration.CoreConfiguration#hostname}
*
* @return the manually configured hostname
*/
@SuppressWarnings("JavadocReference")
@Nullable
public String getConfiguredHostname() {
return configuredHostname;
Expand Down Expand Up @@ -431,7 +497,7 @@ public Kubernetes getKubernetesInfo() {
}

public static class Container {
private String id;
private final String id;

Container(String id) {
this.id = id;
Expand Down
Loading

0 comments on commit d4a651f

Please sign in to comment.