11package com .timgroup .statsd ;
22
3+ import java .io .BufferedReader ;
34import java .io .IOException ;
5+ import java .io .StringReader ;
46import java .nio .file .Files ;
57import java .nio .file .Path ;
68import java .nio .file .Paths ;
9+ import java .util .Arrays ;
10+ import java .util .HashMap ;
11+ import java .util .List ;
12+ import java .util .Map ;
713import java .util .regex .Matcher ;
814import java .util .regex .Pattern ;
915
1016/**
11- * A reader class that retrieves the current container ID parsed from a the cgroup file.
17+ * A reader class that retrieves the current container ID or the cgroup controller
18+ * inode parsed from the cgroup file.
1219 *
1320 */
1421class CgroupReader {
1522 private static final Path CGROUP_PATH = Paths .get ("/proc/self/cgroup" );
23+ /**
24+ * DEFAULT_CGROUP_MOUNT_PATH is the default cgroup mount path.
25+ **/
26+ private static final Path DEFAULT_CGROUP_MOUNT_PATH = Paths .get ("/sys/fs/cgroup" );
27+ /**
28+ * CGROUP_NS_PATH is the path to the cgroup namespace file.
29+ **/
30+ private static final Path CGROUP_NS_PATH = Paths .get ("/proc/self/ns/cgroup" );
1631 private static final String CONTAINER_SOURCE = "[0-9a-f]{64}" ;
1732 private static final String TASK_SOURCE = "[0-9a-f]{32}-\\ d+" ;
1833 private static final Pattern LINE_RE = Pattern .compile ("^\\ d+:[^:]*:(.+)$" , Pattern .MULTILINE | Pattern .UNIX_LINES );
19- private static final Pattern CONTAINER_RE =
20- Pattern .compile (
34+ private static final Pattern CONTAINER_RE = Pattern .compile (
2135 "(" + CONTAINER_SOURCE + "|" + TASK_SOURCE + ")(?:.scope)?$" );
2236
37+ /**
38+ * CGROUPV1_BASE_CONTROLLER is the controller used to identify the container-id
39+ * in cgroup v1 (memory).
40+ **/
41+ private static final String CGROUPV1_BASE_CONTROLLER = "memory" ;
42+ /**
43+ * CGROUPV2_BASE_CONTROLLER is the controller used to identify the container-id
44+ * in cgroup v2.
45+ **/
46+ private static final String CGROUPV2_BASE_CONTROLLER = "" ;
47+ /**
48+ * HOST_CGROUP_NAMESPACE_INODE is the inode of the host cgroup namespace.
49+ **/
50+ private static final long HOST_CGROUP_NAMESPACE_INODE = 0xEFFFFFFBL ;
51+
2352 private boolean readOnce = false ;
53+ /**
54+ * containerID holds either the container ID or the cgroup controller inode.
55+ **/
2456 public String containerID ;
2557
2658 /**
27- * Parses /proc/self/cgroup and returns the container ID if available.
59+ * Returns the container ID if available or the cgroup controller inode .
2860 *
29- * @throws IOException
30- * if /proc/self/cgroup is readable and still an I/O error occurs reading from the stream
61+ * @throws IOException if /proc/self/cgroup is readable and still an I/O error
62+ * occurs reading from the stream.
3163 */
3264 public String getContainerID () throws IOException {
3365 if (readOnce ) {
3466 return containerID ;
3567 }
3668
37- containerID = read (CGROUP_PATH );
69+ final String cgroupContent = read (CGROUP_PATH );
70+ if (cgroupContent == null || cgroupContent .isEmpty ()) {
71+ return null ;
72+ }
73+ containerID = parse (cgroupContent );
74+ /*
75+ * If the container ID is not available it means that the application is either
76+ * not running in a container or running is private cgroup namespace, we
77+ * fallback to the cgroup controller inode. The agent (7.51+) will use it to get
78+ * the container ID.
79+ * In Host cgroup namespace, the container ID should be found. If it is not
80+ * found, it means that the application is running on a host/vm.
81+ *
82+ */
83+ if ((containerID == null || containerID .equals ("" )) && !isHostCgroupNamespace (CGROUP_NS_PATH )) {
84+ containerID = getCgroupInode (DEFAULT_CGROUP_MOUNT_PATH , cgroupContent );
85+ }
3886 return containerID ;
3987 }
4088
89+ /**
90+ * Returns the content of `path` (=/proc/self/cgroup).
91+ *
92+ * @throws IOException if /proc/self/cgroup is readable and still an I/O error
93+ * occurs reading from the stream.
94+ */
4195 private String read (Path path ) throws IOException {
4296 readOnce = true ;
4397 if (!Files .isReadable (path )) {
4498 return null ;
4599 }
46100
47- final String content = new String (Files .readAllBytes (path ));
48- if (content .isEmpty ()) {
49- return null ;
50- }
51-
52- return parse (content );
101+ return new String (Files .readAllBytes (path ));
53102 }
54103
55104 /**
56- * Parses a Cgroup file content and returns the corresponding container ID.
105+ * Parses a Cgroup file (=/proc/self/cgroup) content and returns the
106+ * corresponding container ID. It can be found only if the container
107+ * is running in host cgroup namespace.
57108 *
58- * @param cgroupsContent
59- * Cgroup file content
109+ * @param cgroupsContent Cgroup file content
60110 */
61111 public static String parse (final String cgroupsContent ) {
62112 final Matcher lines = LINE_RE .matcher (cgroupsContent );
@@ -70,4 +120,88 @@ public static String parse(final String cgroupsContent) {
70120
71121 return null ;
72122 }
123+
124+ /**
125+ * Returns true if the host cgroup namespace is used.
126+ * It looks at the inode of `/proc/self/ns/cgroup` and compares it to
127+ * HOST_CGROUP_NAMESPACE_INODE.
128+ *
129+ * @param path Path to the cgroup namespace file.
130+ */
131+ private static boolean isHostCgroupNamespace (final Path path ) {
132+ long hostCgroupInode = inodeForPath (path );
133+ return hostCgroupInode == HOST_CGROUP_NAMESPACE_INODE ;
134+ }
135+
136+ /**
137+ * Returns the inode for the given path.
138+ *
139+ * @param path Path to the cgroup namespace file.
140+ */
141+ private static long inodeForPath (final Path path ) {
142+ try {
143+ long inode = (long ) Files .getAttribute (path , "unix:ino" );
144+ return inode ;
145+ } catch (Exception e ) {
146+ return 0 ;
147+ }
148+ }
149+
150+ /**
151+ * Returns the cgroup controller inode for the given cgroup mount path and
152+ * procSelfCgroupPath.
153+ *
154+ * @param cgroupMountPath Path to the cgroup mount point.
155+ * @param cgroupContent String content of the cgroup file.
156+ */
157+ public static String getCgroupInode (final Path cgroupMountPath , final String cgroupContent ) throws IOException {
158+ Map <String , String > cgroupControllersPaths = parseCgroupNodePath (cgroupContent );
159+ if (cgroupControllersPaths == null ) {
160+ return null ;
161+ }
162+
163+ // Retrieve the cgroup inode from /sys/fs/cgroup+controller+cgroupNodePath
164+ List <String > controllers = Arrays .asList (CGROUPV1_BASE_CONTROLLER , CGROUPV2_BASE_CONTROLLER );
165+ for (String controller : controllers ) {
166+ String cgroupNodePath = cgroupControllersPaths .get (controller );
167+ if (cgroupNodePath == null ) {
168+ continue ;
169+ }
170+ Path path = Paths .get (cgroupMountPath .toString (), controller , cgroupNodePath );
171+ long inode = inodeForPath (path );
172+ /*
173+ * Inode 0 is not a valid inode. Inode 1 is a bad block inode and inode 2 is the
174+ * root of a filesystem. We can safely ignore them.
175+ */
176+ if (inode > 2 ) {
177+ return "in-" + inode ;
178+ }
179+ }
180+
181+ return null ;
182+ }
183+
184+ /**
185+ * Returns a map of cgroup controllers and their corresponding cgroup path.
186+ *
187+ * @param cgroupContent Cgroup file content.
188+ */
189+ public static Map <String , String > parseCgroupNodePath (final String cgroupContent ) throws IOException {
190+ Map <String , String > res = new HashMap <>();
191+ BufferedReader br = new BufferedReader (new StringReader (cgroupContent ));
192+
193+ String line ;
194+ while ((line = br .readLine ()) != null ) {
195+ String [] tokens = line .split (":" );
196+ if (tokens .length != 3 ) {
197+ continue ;
198+ }
199+ if (CGROUPV1_BASE_CONTROLLER .equals (tokens [1 ]) || CGROUPV2_BASE_CONTROLLER .equals (tokens [1 ])) {
200+ res .put (tokens [1 ], tokens [2 ]);
201+ }
202+ }
203+
204+ br .close ();
205+ return res ;
206+ }
73207}
0 commit comments