Skip to content

Commit 17a5f1c

Browse files
wdhifvickenty
andauthored
Support inode resolution mechanism for Origin Detection (#813)
* [dogstatsd] Send in-<inode> when container_id cannot be retrieved Signed-off-by: Wassim DHIF <[email protected]> * Update datadog/dogstatsd/container.py Co-authored-by: Vickenty Fesunov <[email protected]> * Update datadog/dogstatsd/container.py Co-authored-by: Vickenty Fesunov <[email protected]> * Update datadog/dogstatsd/container.py Co-authored-by: Vickenty Fesunov <[email protected]> * Update datadog/dogstatsd/container.py Co-authored-by: Vickenty Fesunov <[email protected]> * Add inode > 2 case Signed-off-by: Wassim DHIF <[email protected]> * Rename _is_cgroup_namespace and add inode comment Signed-off-by: Wassim DHIF <[email protected]> * Add try/except for _is_host_cgroup_namespace() Signed-off-by: Wassim DHIF <[email protected]> * Add controller priority unittest Signed-off-by: Wassim DHIF <[email protected]> --------- Signed-off-by: Wassim DHIF <[email protected]> Co-authored-by: Vickenty Fesunov <[email protected]>
1 parent 7f612d8 commit 17a5f1c

File tree

3 files changed

+112
-11
lines changed

3 files changed

+112
-11
lines changed

datadog/dogstatsd/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
DistributedContextManagerDecorator,
3333
)
3434
from datadog.dogstatsd.route import get_default_route
35-
from datadog.dogstatsd.container import ContainerID
35+
from datadog.dogstatsd.container import Cgroup
3636
from datadog.util.compat import is_p3k, text
3737
from datadog.util.format import normalize_tags
3838
from datadog.version import __version__
@@ -1288,7 +1288,7 @@ def _set_container_id(self, container_id, origin_detection_enabled):
12881288
return
12891289
if origin_detection_enabled:
12901290
try:
1291-
reader = ContainerID()
1291+
reader = Cgroup()
12921292
self._container_id = reader.container_id
12931293
except Exception as e:
12941294
log.debug("Couldn't get container ID: %s", str(e))

datadog/dogstatsd/container.py

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Copyright 2015-Present Datadog, Inc
55

66
import errno
7+
import os
78
import re
89

910

@@ -13,31 +14,54 @@ class UnresolvableContainerID(Exception):
1314
"""
1415

1516

16-
class ContainerID(object):
17+
class Cgroup(object):
1718
"""
18-
A reader class that retrieves the current container ID parsed from a the cgroup file.
19+
A reader class that retrieves either:
20+
- The current container ID parsed from the cgroup file
21+
- The cgroup controller inode.
1922
2023
Returns:
21-
object: ContainerID
24+
object: Cgroup
2225
2326
Raises:
2427
`NotImplementedError`: No proc filesystem is found (non-Linux systems)
2528
`UnresolvableContainerID`: Unable to read the container ID
2629
"""
2730

2831
CGROUP_PATH = "/proc/self/cgroup"
32+
CGROUP_MOUNT_PATH = "/sys/fs/cgroup" # cgroup mount path.
33+
CGROUP_NS_PATH = "/proc/self/ns/cgroup" # path to the cgroup namespace file.
34+
CGROUPV1_BASE_CONTROLLER = "memory" # controller used to identify the container-id in cgroup v1 (memory).
35+
CGROUPV2_BASE_CONTROLLER = "" # controller used to identify the container-id in cgroup v2.
36+
HOST_CGROUP_NAMESPACE_INODE = 0xEFFFFFFB # inode of the host cgroup namespace.
37+
2938
UUID_SOURCE = r"[0-9a-f]{8}[-_][0-9a-f]{4}[-_][0-9a-f]{4}[-_][0-9a-f]{4}[-_][0-9a-f]{12}"
3039
CONTAINER_SOURCE = r"[0-9a-f]{64}"
3140
TASK_SOURCE = r"[0-9a-f]{32}-\d+"
3241
LINE_RE = re.compile(r"^(\d+):([^:]*):(.+)$")
3342
CONTAINER_RE = re.compile(r"(?:.+)?({0}|{1}|{2})(?:\.scope)?$".format(UUID_SOURCE, CONTAINER_SOURCE, TASK_SOURCE))
3443

3544
def __init__(self):
36-
self.container_id = self._read_container_id(self.CGROUP_PATH)
45+
if self._is_host_cgroup_namespace():
46+
self.container_id = self._read_cgroup_path()
47+
return
48+
self.container_id = self._get_cgroup_from_inode()
49+
50+
def _is_host_cgroup_namespace(self):
51+
"""Check if the current process is in a host cgroup namespace."""
52+
try:
53+
return (
54+
os.stat(self.CGROUP_NS_PATH).st_ino == self.HOST_CGROUP_NAMESPACE_INODE
55+
if os.path.exists(self.CGROUP_NS_PATH)
56+
else False
57+
)
58+
except Exception:
59+
return False
3760

38-
def _read_container_id(self, fpath):
61+
def _read_cgroup_path(self):
62+
"""Read the container ID from the cgroup file."""
3963
try:
40-
with open(fpath, mode="r") as fp:
64+
with open(self.CGROUP_PATH, mode="r") as fp:
4165
for line in fp:
4266
line = line.strip()
4367
match = self.LINE_RE.match(line)
@@ -55,3 +79,33 @@ def _read_container_id(self, fpath):
5579
except Exception as e:
5680
raise UnresolvableContainerID("Unable to read the container ID: " + str(e))
5781
return None
82+
83+
def _get_cgroup_from_inode(self):
84+
"""Read the container ID from the cgroup inode."""
85+
# Parse /proc/self/cgroup and get a map of controller to its associated cgroup node path.
86+
cgroup_controllers_paths = {}
87+
with open(self.CGROUP_PATH, mode="r") as fp:
88+
for line in fp:
89+
tokens = line.strip().split(":")
90+
if len(tokens) != 3:
91+
continue
92+
if tokens[1] == self.CGROUPV1_BASE_CONTROLLER or tokens[1] == self.CGROUPV2_BASE_CONTROLLER:
93+
cgroup_controllers_paths[tokens[1]] = tokens[2]
94+
95+
# Retrieve the cgroup inode from "/sys/fs/cgroup + controller + cgroupNodePath"
96+
for controller in [
97+
self.CGROUPV1_BASE_CONTROLLER,
98+
self.CGROUPV2_BASE_CONTROLLER,
99+
]:
100+
if controller in cgroup_controllers_paths:
101+
inode_path = os.path.join(
102+
self.CGROUP_MOUNT_PATH,
103+
controller,
104+
cgroup_controllers_paths[controller] if cgroup_controllers_paths[controller] != "/" else "",
105+
)
106+
inode = os.stat(inode_path).st_ino
107+
# 0 is not a valid inode. 1 is a bad block inode and 2 is the root of a filesystem.
108+
if inode > 2:
109+
return "in-{0}".format(inode)
110+
111+
return None

tests/unit/dogstatsd/test_container.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import mock
1111
import pytest
1212

13-
from datadog.dogstatsd.container import ContainerID
13+
from datadog.dogstatsd.container import Cgroup
1414

1515

1616
def get_mock_open(read_data=None):
@@ -125,12 +125,59 @@ def get_mock_open(read_data=None):
125125
),
126126
),
127127
)
128-
def test_container_id(file_contents, expected_container_id):
128+
def test_container_id_from_cgroup(file_contents, expected_container_id):
129129
with get_mock_open(read_data=file_contents) as mock_open:
130130
if file_contents is None:
131131
mock_open.side_effect = IOError
132132

133-
reader = ContainerID()
133+
with mock.patch("os.stat", mock.MagicMock(return_value=mock.Mock(st_ino=0xEFFFFFFB))):
134+
reader = Cgroup()
134135
assert expected_container_id == reader.container_id
135136

136137
mock_open.assert_called_once_with("/proc/self/cgroup", mode="r")
138+
139+
140+
def test_container_id_inode():
141+
"""Test that the inode is returned when the container ID cannot be found."""
142+
with mock.patch("datadog.dogstatsd.container.open", mock.mock_open(read_data="0::/")) as mock_open:
143+
with mock.patch("os.stat", mock.MagicMock(return_value=mock.Mock(st_ino=1234))):
144+
reader = Cgroup()
145+
assert reader.container_id == "in-1234"
146+
mock_open.assert_called_once_with("/proc/self/cgroup", mode="r")
147+
148+
cgroupv1_priority = """
149+
12:cpu,cpuacct:/
150+
11:hugetlb:/
151+
10:devices:/
152+
9:rdma:/
153+
8:net_cls,net_prio:/
154+
7:memory:/
155+
6:cpuset:/
156+
5:pids:/
157+
4:freezer:/
158+
3:perf_event:/
159+
2:blkio:/
160+
1:name=systemd:/
161+
0::/
162+
"""
163+
164+
paths_checked = []
165+
166+
def inode_stat_mock(path):
167+
paths_checked.append(path)
168+
169+
# The cgroupv1 controller is mounted on inode 0. This will cause a fallback to the cgroupv2 controller.
170+
if path == "/sys/fs/cgroup/memory/":
171+
return mock.Mock(st_ino=0)
172+
elif path == "/sys/fs/cgroup/":
173+
return mock.Mock(st_ino=1234)
174+
175+
with mock.patch("datadog.dogstatsd.container.open", mock.mock_open(read_data=cgroupv1_priority)) as mock_open:
176+
with mock.patch("os.stat", mock.MagicMock(side_effect=inode_stat_mock)):
177+
reader = Cgroup()
178+
assert reader.container_id == "in-1234"
179+
assert paths_checked[-2:] == [
180+
"/sys/fs/cgroup/memory/",
181+
"/sys/fs/cgroup/"
182+
]
183+
mock_open.assert_called_once_with("/proc/self/cgroup", mode="r")

0 commit comments

Comments
 (0)