Skip to content

Commit

Permalink
[dogstatsd] Send in-<inode> when container_id cannot be retrieved
Browse files Browse the repository at this point in the history
Signed-off-by: Wassim DHIF <[email protected]>
  • Loading branch information
wdhif committed Jan 16, 2024
1 parent a51acab commit 878714e
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 11 deletions.
4 changes: 2 additions & 2 deletions datadog/dogstatsd/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
DistributedContextManagerDecorator,
)
from datadog.dogstatsd.route import get_default_route
from datadog.dogstatsd.container import ContainerID
from datadog.dogstatsd.container import Cgroup
from datadog.util.compat import is_p3k, text
from datadog.util.format import normalize_tags
from datadog.version import __version__
Expand Down Expand Up @@ -1288,7 +1288,7 @@ def _set_container_id(self, container_id, origin_detection_enabled):
return
if origin_detection_enabled:
try:
reader = ContainerID()
reader = Cgroup()
self._container_id = reader.container_id
except Exception as e:
log.debug("Couldn't get container ID: %s", str(e))
Expand Down
59 changes: 53 additions & 6 deletions datadog/dogstatsd/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Copyright 2015-Present Datadog, Inc

import errno
import os
import re


Expand All @@ -13,31 +14,51 @@ class UnresolvableContainerID(Exception):
"""


class ContainerID(object):
class Cgroup(object):
"""
A reader class that retrieves the current container ID parsed from a the cgroup file.
A reader class that retrieves either:
- The current container ID parsed from the cgroup file
- The cgroup controller inode.
Returns:
object: ContainerID
object: Cgroup
Raises:
`NotImplementedError`: No proc filesystem is found (non-Linux systems)
`UnresolvableContainerID`: Unable to read the container ID
"""

CGROUP_PATH = "/proc/self/cgroup"
DEFAULT_CGROUP_MOUNT_PATH = "/sys/fs/cgroup" # default cgroup mount path.
CGROUP_NS_PATH = "/proc/self/ns/cgroup" # path to the cgroup namespace file.
CGROUPV1_BASE_CONTROLLER = "memory" # controller used to identify the container-id in cgroup v1 (memory).
CGROUPV2_BASE_CONTROLLER = "" # controller used to identify the container-id in cgroup v2.
HOST_CGROUP_NAMESPACE_INODE = "0xEFFFFFFBL" # inode of the host cgroup namespace.

UUID_SOURCE = r"[0-9a-f]{8}[-_][0-9a-f]{4}[-_][0-9a-f]{4}[-_][0-9a-f]{4}[-_][0-9a-f]{12}"
CONTAINER_SOURCE = r"[0-9a-f]{64}"
TASK_SOURCE = r"[0-9a-f]{32}-\d+"
LINE_RE = re.compile(r"^(\d+):([^:]*):(.+)$")
CONTAINER_RE = re.compile(r"(?:.+)?({0}|{1}|{2})(?:\.scope)?$".format(UUID_SOURCE, CONTAINER_SOURCE, TASK_SOURCE))

def __init__(self):
self.container_id = self._read_container_id(self.CGROUP_PATH)
if self._is_cgroup_namespace():
self.container_id = self._read_cgroup_path()
return
self.container_id = self._get_cgroup_from_inode()

def _is_cgroup_namespace(self):
"""Check if the current process is in a host cgroup namespace."""
return (
os.stat(self.CGROUP_NS_PATH).st_ino == self.HOST_CGROUP_NAMESPACE_INODE
if os.path.exists(self.CGROUP_NS_PATH)
else False
)

def _read_container_id(self, fpath):
def _read_cgroup_path(self):
"""Read the container ID from the cgroup file."""
try:
with open(fpath, mode="r") as fp:
with open(self.CGROUP_PATH, mode="r") as fp:
for line in fp:
line = line.strip()
match = self.LINE_RE.match(line)
Expand All @@ -55,3 +76,29 @@ def _read_container_id(self, fpath):
except Exception as e:
raise UnresolvableContainerID("Unable to read the container ID: " + str(e))
return None

def _get_cgroup_from_inode(self):
"""Read the container ID from the cgroup inode."""
# Parse /proc/self/cgroup and get a map of controller to its associated cgroup node path.
cgroup_controllers_paths = {}
for line in open(self.CGROUP_PATH, mode="r"):
tokens = line.strip().split(":")
if len(tokens) != 3:
continue
if tokens[1] == self.CGROUPV1_BASE_CONTROLLER or tokens[1] == "":
cgroup_controllers_paths[tokens[1]] = tokens[2]

# Retrieve the cgroup inode from "/sys/fs/cgroup + controller + cgroupNodePath"
for controller in [
self.CGROUPV1_BASE_CONTROLLER,
self.CGROUPV2_BASE_CONTROLLER,
]:
if controller in cgroup_controllers_paths:
inode_path = os.path.join(
self.DEFAULT_CGROUP_MOUNT_PATH,
controller,
cgroup_controllers_paths[controller] if cgroup_controllers_paths[controller] != "/" else "",
)
return "in-{0}".format(os.stat(inode_path).st_ino)

return None
16 changes: 13 additions & 3 deletions tests/unit/dogstatsd/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import mock
import pytest

from datadog.dogstatsd.container import ContainerID
from datadog.dogstatsd.container import Cgroup


def get_mock_open(read_data=None):
Expand Down Expand Up @@ -125,12 +125,22 @@ def get_mock_open(read_data=None):
),
),
)
def test_container_id(file_contents, expected_container_id):
def test_container_id_from_cgroup(file_contents, expected_container_id):
with get_mock_open(read_data=file_contents) as mock_open:
if file_contents is None:
mock_open.side_effect = IOError

reader = ContainerID()
with mock.patch("os.stat", mock.MagicMock(return_value=mock.Mock(st_ino="0xEFFFFFFBL"))):
reader = Cgroup()
assert expected_container_id == reader.container_id

mock_open.assert_called_once_with("/proc/self/cgroup", mode="r")


def test_container_id_inode():
"""Test that the inode is returned when the container ID cannot be found."""
with mock.patch("datadog.dogstatsd.container.open", mock.mock_open(read_data="0::/")) as mock_open:
with mock.patch("os.stat", mock.MagicMock(return_value=mock.Mock(st_ino="1234"))):
reader = Cgroup()
assert reader.container_id == "in-1234"
mock_open.assert_called_once_with("/proc/self/cgroup", mode="r")

0 comments on commit 878714e

Please sign in to comment.