Skip to content

Commit edb30df

Browse files
authored
[CONTINT-1132] Send in-<inode> using the cgroup controller when container-id cannot be retrieved
2 parents dfc4d3b + c15f5f4 commit edb30df

2 files changed

Lines changed: 226 additions & 16 deletions

File tree

Lines changed: 150 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,112 @@
11
package com.timgroup.statsd;
22

3+
import java.io.BufferedReader;
34
import java.io.IOException;
5+
import java.io.StringReader;
46
import java.nio.file.Files;
57
import java.nio.file.Path;
68
import java.nio.file.Paths;
9+
import java.util.Arrays;
10+
import java.util.HashMap;
11+
import java.util.List;
12+
import java.util.Map;
713
import java.util.regex.Matcher;
814
import 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
*/
1421
class 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
}

src/test/java/com/timgroup/statsd/CgroupReaderTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
package com.timgroup.statsd;
22

3+
import static org.junit.Assert.assertEquals;
34
import static org.junit.Assert.assertNull;
45

6+
import java.io.IOException;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
10+
import org.junit.Assume;
11+
import org.junit.Before;
12+
import org.junit.Rule;
513
import org.junit.Test;
14+
import org.junit.rules.TemporaryFolder;
615

716
import static org.hamcrest.MatcherAssert.assertThat;
817
import static org.hamcrest.Matchers.equalTo;
@@ -120,4 +129,71 @@ public void containerID_parse() throws Exception {
120129

121130
assertThat(CgroupReader.parse(linux44), equalTo("cde7c2bab394630a42d73dc610b9c57415dced996106665d427f6d0566594411"));
122131
}
132+
133+
@Rule
134+
public TemporaryFolder folder = new TemporaryFolder();
135+
136+
@Before
137+
public void assumeNotWindows() {
138+
Assume.assumeFalse(System.getProperty("os.name").startsWith("Windows"));
139+
}
140+
141+
@Test
142+
public void testWithExistingCgroupV1Inode() throws IOException {
143+
144+
folder.create();
145+
Path cgroupMountPath = folder.newFolder("sys", "fs", "cgroup").toPath();
146+
Path controllerDir = cgroupMountPath.resolve("memory");
147+
Path cgroupPath = controllerDir.resolve("docker");
148+
Path nodePath = cgroupPath.resolve("3726184226f5d3147c25fdeab5b60097e378e8a720503a5e19ecfdf29f869860");
149+
Files.createDirectories(nodePath);
150+
151+
long inode = (long) Files.getAttribute(nodePath, "unix:ino");
152+
153+
String cgroupContent = "myfile\5:memory:/docker/3726184226f5d3147c25fdeab5b60097e378e8a720503a5e19ecfdf29f869860\n";
154+
String inodeResult = CgroupReader.getCgroupInode(cgroupMountPath, cgroupContent);
155+
156+
assertEquals("in-" + inode, inodeResult);
157+
}
158+
159+
@Test
160+
public void testWithExistingCgroupV2Inode() throws IOException {
161+
162+
folder.create();
163+
Path cgroupMountPath = folder.newFolder("sys", "fs", "cgroup").toPath();
164+
Files.createDirectories(cgroupMountPath);
165+
166+
long inodeNumber = (long) Files.getAttribute(cgroupMountPath, "unix:ino");
167+
String cgroupContent = "0::/\n";
168+
String inodeResult = CgroupReader.getCgroupInode(cgroupMountPath, cgroupContent);
169+
170+
assertEquals("in-" + inodeNumber, inodeResult);
171+
}
172+
173+
@Test
174+
public void testWithNonExistentCgroupNodePath() throws IOException {
175+
176+
folder.create();
177+
Path cgroupMountPath = folder.newFolder("sys", "fs", "cgroup").toPath();
178+
179+
String cgroupContent = "memory:/nonexistentpath\n";
180+
String inodeResult = CgroupReader.getCgroupInode(cgroupMountPath,
181+
cgroupContent);
182+
183+
assertNull(inodeResult);
184+
185+
}
186+
187+
@Test
188+
public void testWithEmptyCgroupContent() throws IOException {
189+
190+
folder.create();
191+
Path cgroupMountPath = folder.newFolder("sys", "fs", "cgroup").toPath();
192+
193+
String cgroupContent = "";
194+
String inodeResult = CgroupReader.getCgroupInode(cgroupMountPath,
195+
cgroupContent);
196+
197+
assertNull(inodeResult);
198+
}
123199
}

0 commit comments

Comments
 (0)