Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

parse container ID with cgroups v2 #3199

Merged
merged 8 commits into from Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 6 additions & 6 deletions .ci/updatecli.d/update-json-specs.yml
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
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
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