Skip to content

Commit

Permalink
refactor cgroup parsing to use regexes, heavily inspired by elastic/a…
Browse files Browse the repository at this point in the history
…pm-agent-go#342

(as in, pretty much copied from elastic/apm-agent-go#342)
  • Loading branch information
beniwohli committed Dec 19, 2018
1 parent f7aae13 commit 99e00fd
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 66 deletions.
4 changes: 2 additions & 2 deletions elasticapm/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from elasticapm.conf import Config, constants
from elasticapm.conf.constants import ERROR
from elasticapm.traces import Tracer, get_transaction
from elasticapm.utils import compat, docker, is_master_process, stacks, varmap
from elasticapm.utils import cgroup, compat, is_master_process, stacks, varmap
from elasticapm.utils.encoding import keyword_field, shorten, transform
from elasticapm.utils.module_import import import_string

Expand Down Expand Up @@ -250,7 +250,7 @@ def get_system_info(self):
"architecture": platform.machine(),
"platform": platform.system().lower(),
}
system_data.update(docker.get_docker_metadata())
system_data.update(cgroup.get_cgroup_container_metadata())
pod_name = os.environ.get("KUBERNETES_POD_NAME") or system_data["hostname"]
changed = False
if "kubernetes" in system_data:
Expand Down
75 changes: 75 additions & 0 deletions elasticapm/utils/cgroup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import os
import re

CGROUP_PATH = "/proc/self/cgroup"

SYSTEMD_SCOPE_SUFFIX = ".scope"

kubepods_regexp = re.compile(
r"(?:^/kubepods/[^/]+/pod([^/]+)$)|(?:^/kubepods\.slice/kubepods-[^/]+\.slice/kubepods-[^/]+-pod([^/]+)\.slice$)"
)

container_id_regexp = re.compile("^[0-9A-Fa-f]{64}$")


def get_cgroup_container_metadata():
"""
Reads docker/kubernetes metadata (container id, pod id) from /proc/self/cgroup
The result is a nested dictionary with the detected IDs, e.g.
{
"container": {"id": "2227daf62df6694645fee5df53c1f91271546a9560e8600a525690ae252b7f63"},
"pod": {"uid": "90d81341_92de_11e7_8cf2_507b9d4141fa"}
}
:return: a dictionary with the detected ids or {}
"""
if not os.path.exists(CGROUP_PATH):
return {}
with open(CGROUP_PATH) as f:
return parse_cgroups(f) or {}


def parse_cgroups(filehandle):
"""
Reads lines from a file handle and tries to parse docker container IDs and kubernetes Pod IDs.
See tests.utils.docker_tests.test_cgroup_parsing for a set of test cases
:param filehandle:
:return: nested dictionary or None
"""
for line in filehandle:
parts = line.strip().split(":")
if len(parts) != 3:
continue
cgroup_path = parts[2]

# Depending on the filesystem driver used for cgroup
# management, the paths in /proc/pid/cgroup 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>

directory, container_id = os.path.split(cgroup_path)
if container_id.endswith(SYSTEMD_SCOPE_SUFFIX):
container_id = container_id[: -len(SYSTEMD_SCOPE_SUFFIX)]
if "-" in container_id:
container_id = container_id.split("-", 1)[1]
kubepods_match = kubepods_regexp.match(directory)
if kubepods_match:
pod_id = kubepods_match.group(1)
if not pod_id:
pod_id = kubepods_match.group(2)
return {"container": {"id": container_id}, "pod": {"uid": pod_id}}
elif container_id_regexp.match(container_id):
return {"container": {"id": container_id}}
57 changes: 0 additions & 57 deletions elasticapm/utils/docker.py

This file was deleted.

8 changes: 4 additions & 4 deletions tests/client/client_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ def test_process_info(elasticapm_client):

def test_system_info(elasticapm_client):
# mock docker/kubernetes data here to get consistent behavior if test is run in docker
with mock.patch("elasticapm.utils.docker.get_docker_metadata") as mocked:
with mock.patch("elasticapm.utils.cgroup.get_cgroup_container_metadata") as mocked:
mocked.return_value = {}
system_info = elasticapm_client.get_system_info()
assert {"hostname", "architecture", "platform"} == set(system_info.keys())


def test_docker_kubernetes_system_info(elasticapm_client):
# mock docker/kubernetes data here to get consistent behavior if test is run in docker
with mock.patch("elasticapm.utils.docker.get_docker_metadata") as mock_metadata, mock.patch(
with mock.patch("elasticapm.utils.cgroup.get_cgroup_container_metadata") as mock_metadata, mock.patch(
"socket.gethostname"
) as mock_gethostname:
mock_metadata.return_value = {"container": {"id": "123"}, "kubernetes": {"pod": {"uid": "456"}}}
Expand All @@ -71,7 +71,7 @@ def test_docker_kubernetes_system_info_from_environ():
# initialize agent only after overriding environment
elasticapm_client = Client()
# mock docker/kubernetes data here to get consistent behavior if test is run in docker
with mock.patch("elasticapm.utils.docker.get_docker_metadata") as mock_metadata:
with mock.patch("elasticapm.utils.cgroup.get_cgroup_container_metadata") as mock_metadata:
mock_metadata.return_value = {}
system_info = elasticapm_client.get_system_info()
assert "kubernetes" in system_info
Expand All @@ -95,7 +95,7 @@ def test_docker_kubernetes_system_info_from_environ_overrides_cgroups():
# initialize agent only after overriding environment
elasticapm_client = Client()
# mock docker/kubernetes data here to get consistent behavior if test is run in docker
with mock.patch("elasticapm.utils.docker.get_docker_metadata") as mock_metadata, mock.patch(
with mock.patch("elasticapm.utils.cgroup.get_cgroup_container_metadata") as mock_metadata, mock.patch(
"socket.gethostname"
) as mock_gethostname:
mock_metadata.return_value = {"container": {"id": "123"}, "kubernetes": {"pod": {"uid": "456"}}}
Expand Down
6 changes: 3 additions & 3 deletions tests/utils/docker_tests.py → tests/utils/cgroup_tests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import mock
import pytest

from elasticapm.utils import compat, docker
from elasticapm.utils import cgroup, compat


@pytest.mark.parametrize(
Expand Down Expand Up @@ -33,5 +33,5 @@
)
def test_cgroup_parsing(test_input, expected):
f = compat.StringIO(test_input)
result = docker.parse_cgroups(f)
assert expected == result
result = cgroup.parse_cgroups(f)
assert result == expected

0 comments on commit 99e00fd

Please sign in to comment.