diff --git a/statsd/container.go b/statsd/container.go index 20d69ef6..ff3b43c2 100644 --- a/statsd/container.go +++ b/statsd/container.go @@ -12,8 +12,9 @@ var ( ) // getContainerID returns the container ID configured at the client creation -// It can either be auto-discovered with origin detection or provided by the user. -// User-defined container ID is prioritized. +// It can be provided by the user or read from cgroups. If the container ID +// is not available, it returns the cgroup inode prefixed by "in-" if available. +// If the cgroup inode is not available, it returns an empty string. func getContainerID() string { return containerID } diff --git a/statsd/container_linux.go b/statsd/container_linux.go index 6960b95a..f2623ed5 100644 --- a/statsd/container_linux.go +++ b/statsd/container_linux.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "os" + "path" "regexp" "strings" "syscall" @@ -17,6 +18,9 @@ const ( // cgroupPath is the path to the cgroup file where we can find the container id if one exists. cgroupPath = "/proc/self/cgroup" + // cgroupRootPath is the default path to the cgroup root path + cgroupRootPath = "/sys/fs/cgroup" + // selfMountinfo is the path to the mountinfo path where we can find the container id in case cgroup namespace is preventing the use of /proc/self/cgroup selfMountInfoPath = "/proc/self/mountinfo" @@ -48,29 +52,62 @@ var ( ) // parseContainerID finds the first container ID reading from r and returns it. -func parseContainerID(r io.Reader) string { - scn := bufio.NewScanner(r) - for scn.Scan() { - path := expLine.FindStringSubmatch(scn.Text()) - if len(path) != 2 { - // invalid entry, continue - continue - } - if parts := expContainerID.FindStringSubmatch(path[1]); len(parts) == 2 { - return parts[1] - } +func parseContainerID(line string) string { + path := expLine.FindStringSubmatch(line) + if len(path) != 2 { + // invalid entry, continue + return "" + } + if parts := expContainerID.FindStringSubmatch(path[1]); len(parts) == 2 { + return parts[1] } return "" } -// readContainerID attempts to return the container ID from the provided file path or empty on failure. -func readContainerID(fpath string) string { +// parseCgroupNodePath parses the cgroup root path from a line formatted as +// 0:: where is the cgroup node prefixed by /sys/fs/cgroup +func parseCgroupNodePath(line string) string { + if strings.HasPrefix(line, "0::") { + // return root cgroup + return line[3:] + } + return "" +} + +// readContainerIDOrCgroupInode attempts to return the container ID from the provided file path +// or the cgroup inode if the container ID is not available. Otherwise, it returns an empty string. +func readContainerIDOrCgroupInode(cgroupPrefix, fpath string) string { f, err := os.Open(fpath) if err != nil { return "" } defer f.Close() - return parseContainerID(f) + + cgroupNodePath := "" + + scn := bufio.NewScanner(f) + for scn.Scan() { + line := scn.Text() + // Return the container id if found + if containerID := parseContainerID(line); containerID != "" { + return containerID + } + cgroupNodePath = parseCgroupNodePath(line) + } + + // Assumes the file is not a symlink + fi, err := os.Stat(path.Clean(cgroupPrefix + cgroupNodePath)) + if err != nil { + return "" + } + inode := fi.Sys().(*syscall.Stat_t).Ino + // Inode 0 indicates that there is no inode. + // Inode 1 indicates a bad block + // Inode 2 indicates starting of fs nodes + if inode > 2 { + return fmt.Sprintf("in-%d", inode) + } + return "" } // Parsing /proc/self/mountinfo is not always reliable in Kubernetes+containerd (at least) @@ -138,9 +175,10 @@ func isHostCgroupNamespace() bool { return inode == hostCgroupNamespaceInode } -// initContainerID initializes the container ID. -// It can either be provided by the user or read from cgroups. -func initContainerID(userProvidedID string, cgroupFallback bool) { +// initContainerIDOrCgroupInode initializes the container ID or the cgroup inode. +// The container ID can either be provided by the user or read from cgroups. +// The cgroup inode can only be auto discovered. +func initContainerIDOrCgroupInode(userProvidedID string, cgroupFallback bool) { initOnce.Do(func() { if userProvidedID != "" { containerID = userProvidedID @@ -148,11 +186,12 @@ func initContainerID(userProvidedID string, cgroupFallback bool) { } if cgroupFallback { - if isCgroupV1(mountsPath) || isHostCgroupNamespace() { - containerID = readContainerID(cgroupPath) - } else { + if !(isCgroupV1(mountsPath) || isHostCgroupNamespace()) { containerID = readMountinfo(selfMountInfoPath) } + if containerID == "" { + containerID = readContainerIDOrCgroupInode(cgroupRootPath, cgroupPath) + } } }) } diff --git a/statsd/container_stub.go b/statsd/container_stub.go index 8489f436..a71e1eee 100644 --- a/statsd/container_stub.go +++ b/statsd/container_stub.go @@ -3,7 +3,7 @@ package statsd -func initContainerID(userProvidedID string, cgroupFallback bool) { +func initContainerIDOrCgroupInode(userProvidedID string, cgroupFallback bool) { initOnce.Do(func() { if userProvidedID != "" { containerID = userProvidedID diff --git a/statsd/container_test.go b/statsd/container_test.go index 48ff2965..1f804044 100644 --- a/statsd/container_test.go +++ b/statsd/container_test.go @@ -8,7 +8,9 @@ import ( "io" "io/ioutil" "os" + "path" "strings" + "syscall" "testing" "github.com/stretchr/testify/assert" @@ -16,32 +18,78 @@ import ( func TestParseContainerID(t *testing.T) { for input, expectedResult := range map[string]string{ - `other_line -10:hugetlb:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa -9:cpuset:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa -8:pids:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa -7:freezer:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa -6:cpu,cpuacct:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa -5:perf_event:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa -4:blkio:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa -3:devices:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa -2:net_cls,net_prio:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa`: "8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa", - "10:hugetlb:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa": "8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa", + "10:hugetlb:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa": "8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa", + "9:cpuset:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa": "8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa", + "8:pids:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa": "8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa", + "7:freezer:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa": "8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa", + "6:cpu,cpuacct:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa": "8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa", + "5:perf_event:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa": "8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa", + "4:blkio:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa": "8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa", + "3:devices:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa": "8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa", + "2:net_cls,net_prio:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa": "8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa", "10:hugetlb:/kubepods": "", "11:hugetlb:/ecs/55091c13-b8cf-4801-b527-f4601742204d/432624d2150b349fe35ba397284dea788c2bf66b885d14dfc1569b01890ca7da": "432624d2150b349fe35ba397284dea788c2bf66b885d14dfc1569b01890ca7da", "1:name=systemd:/docker/34dc0b5e626f2c5c4c5170e34b10e7654ce36f0fcd532739f4445baabea03376": "34dc0b5e626f2c5c4c5170e34b10e7654ce36f0fcd532739f4445baabea03376", "1:name=systemd:/uuid/34dc0b5e-626f-2c5c-4c51-70e34b10e765": "34dc0b5e-626f-2c5c-4c51-70e34b10e765", "1:name=systemd:/ecs/34dc0b5e626f2c5c4c5170e34b10e765-1234567890": "34dc0b5e626f2c5c4c5170e34b10e765-1234567890", "1:name=systemd:/docker/34dc0b5e626f2c5c4c5170e34b10e7654ce36f0fcd532739f4445baabea03376.scope": "34dc0b5e626f2c5c4c5170e34b10e7654ce36f0fcd532739f4445baabea03376", - `1:name=systemd:/nope -2:pids:/docker/34dc0b5e626f2c5c4c5170e34b10e7654ce36f0fcd532739f4445baabea03376 -3:cpu:/invalid`: "34dc0b5e626f2c5c4c5170e34b10e7654ce36f0fcd532739f4445baabea03376", + "2:pids:/docker/34dc0b5e626f2c5c4c5170e34b10e7654ce36f0fcd532739f4445baabea03376": "34dc0b5e626f2c5c4c5170e34b10e7654ce36f0fcd532739f4445baabea03376", } { - id := parseContainerID(strings.NewReader(input)) + id := parseContainerID(input) assert.Equal(t, expectedResult, id) } } +// In order to test with the actual /sys/fs/cgroup file, an integration test running in a container is necessary. +// When ran on host, /sys/fs/cgroup's inode is 2. +func TestDefaultCase(t *testing.T) { + // Create a dummy cgroup node + dummyCgroupNode, err := ioutil.TempDir(os.TempDir(), "sysfscgroup-") // ex: 0::/tmp/sysfscgroup-4222027333/ + assert.NoError(t, err) + defer os.Remove(dummyCgroupNode) + + // Create the file corresponding to /proc/self/cgroup redirecting to the cgroup node + tmpFile, err := ioutil.TempFile(os.TempDir(), "procselfcgroup-") + assert.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + // Write the path to the cgroup node in the tmp file + procFileContent := `other_line +0::` + dummyCgroupNode + _, err = io.WriteString(tmpFile, procFileContent) + assert.NoError(t, err) + err = tmpFile.Close() + assert.NoError(t, err) + + // Verify that readContainerIDOrCgroupInode returns the inode of dummyCgroupNode + inode := readContainerIDOrCgroupInode("", tmpFile.Name()) + + fi, err := os.Stat(path.Clean(dummyCgroupNode)) + assert.NoError(t, err) + expectedInode := fi.Sys().(*syscall.Stat_t).Ino + assert.Equal(t, fmt.Sprintf("in-%d", expectedInode), inode) +} + +func TestContainerIDIsReturned(t *testing.T) { + // Create the file corresponding to /proc/self/cgroup redirecting to the cgroup node + tmpFile, err := ioutil.TempFile(os.TempDir(), "fake-cgroup-") + assert.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + // Write the path to the cgroup node in the tmp file + procFileContent := `other_line +0::/ +10:hugetlb:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa` + _, err = io.WriteString(tmpFile, procFileContent) + assert.NoError(t, err) + err = tmpFile.Close() + assert.NoError(t, err) + + // Verify that readContainerIDOrCgroupInode returns the cid + cid := readContainerIDOrCgroupInode("", tmpFile.Name()) + assert.Equal(t, "8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa", cid) +} + func TestReadContainerID(t *testing.T) { cid := "8c046cb0b72cd4c99f51b5591cd5b095967f58ee003710a45280c28ee1a9c7fa" cgroupContents := "10:hugetlb:/kubepods/burstable/podfd52ef25-a87d-11e9-9423-0800271a638e/" + cid @@ -57,7 +105,7 @@ func TestReadContainerID(t *testing.T) { err = tmpFile.Close() assert.NoError(t, err) - actualCID := readContainerID(tmpFile.Name()) + actualCID := readContainerIDOrCgroupInode("", tmpFile.Name()) assert.Equal(t, cid, actualCID) } diff --git a/statsd/statsd.go b/statsd/statsd.go index 2e2917a9..b089e356 100644 --- a/statsd/statsd.go +++ b/statsd/statsd.go @@ -453,7 +453,7 @@ func newWithWriter(w io.WriteCloser, o *Options, writerName string) (*Client, er } if !hasEntityID { - initContainerID(o.containerID, isOriginDetectionEnabled(o, hasEntityID)) + initContainerIDOrCgroupInode(o.containerID, isOriginDetectionEnabled(o, hasEntityID)) } if o.maxBytesPerPayload == 0 { diff --git a/statsd/test_helpers_test.go b/statsd/test_helpers_test.go index 49ac251c..e83b7bef 100644 --- a/statsd/test_helpers_test.go +++ b/statsd/test_helpers_test.go @@ -59,6 +59,7 @@ type testServer struct { tags string namespace string containerID string + inode string aggregation bool extendedAggregation bool @@ -328,6 +329,13 @@ func (ts *testServer) getContainerID() string { return "|c:" + ts.containerID } +func (ts *testServer) getCgroupInode() string { + if ts.inode == "" { + return "" + } + return "|e:" + ts.inode +} + func (ts *testServer) getFinalTelemetryTags() string { base := "|#" if ts.tags != "" {