in metric/system/cgroup/util.go [433:564]
func (r *Reader) ProcessCgroupPaths(pid int) (PathList, error) {
cgroupPath := filepath.Join("proc", strconv.Itoa(pid), "cgroup")
cgroup, err := os.Open(r.rootfsMountpoint.ResolveHostFS(cgroupPath))
if err != nil {
return PathList{}, err //return a blank error so other events can use any file not found errors
}
defer cgroup.Close()
version, err := r.CgroupsVersion(pid)
if err != nil {
return PathList{}, fmt.Errorf("error finding cgroup version for pid %d: %w", pid, err)
}
cPaths := PathList{V1: map[string]ControllerPath{}, V2: map[string]ControllerPath{}}
sc := bufio.NewScanner(cgroup)
for sc.Scan() {
// http://man7.org/linux/man-pages/man7/cgroups.7.html
// Format: hierarchy-ID:subsystem-list:cgroup-path
// Example:
// 2:cpu:/docker/b29faf21b7eff959f64b4192c34d5d67a707fe8561e9eaa608cb27693fba4242
line := sc.Text()
fields := strings.Split(line, ":")
if len(fields) != 3 {
continue
}
path := fields[2]
if r.cgroupsHierarchyOverride != "" {
path = r.cgroupsHierarchyOverride
}
//on newer docker versions (1.41+?), docker will do namespacing with cgroups
// such that we'll get a cgroup path like `0::/../../user.slice/user-1000.slice/session-520.scope`
// `man 7 cgroups` says the following about the path field in the `cgroup` file (emphasis mine):
//
// This field contains the pathname of the control group
// in the hierarchy to which the process belongs. This
// pathname is **relative to the mount point of the hierarchy**.
//
// However, when we try to append something like `/../..` to another path, we obviously blow things up.
// we need to use the absolute path of the container cgroup
if cgroupNSStateFetch() && r.rootfsMountpoint.IsSet() {
if r.cgroupMountpoints.ContainerizedRootMount == "" {
logp.L().Debugf("cgroup for process %d contains a relative cgroup path (%s), but we were not able to find a root cgroup. Cgroup monitoring for this PID may be incomplete",
pid, path)
} else {
logp.L().Debugf("using root mount %s and path %s", r.cgroupMountpoints.ContainerizedRootMount, path)
path = filepath.Join(r.cgroupMountpoints.ContainerizedRootMount, path)
}
}
// cgroup V2
// cgroup v2 controllers will always start with this string
if strings.HasPrefix(line, "0::/") {
// if you're running inside a container
// that's operating with a hybrid cgroups config,
// the containerized process won't see the V2 mount
// inside /proc/self/mountinfo if docker is using cgroups V1
// For this very annoying edge case, revert to the hostfs flag
// If it's not set, warn the user that they've hit this.
// we skip reading paths in case there are cgroups V1 controllers, we are at the cgroup V2 root and the cgroup V2 mount is not available
// instead of returning an error because we don't want to break V1 metric collection for misconfigured hybrid systems that have only
// a cgroup V2 root but don't have any other controllers. This case happens when cgroup V2 FS is mounted at a special location but not used
if version == CgroupsV1 && line == "0::/" && r.cgroupMountpoints.V2Loc == "" {
continue
}
controllerPath := filepath.Join(r.cgroupMountpoints.V2Loc, path)
if r.cgroupMountpoints.V2Loc == "" && !r.rootfsMountpoint.IsSet() {
logp.L().Debugf(`PID %d contains a cgroups V2 path (%s) but no V2 mountpoint was found.
This may be because metricbeat is running inside a container on a hybrid system.
To monitor cgroups V2 processess in this way, mount the unified (V2) hierarchy inside
the container as /sys/fs/cgroup/unified and start the system module with the hostfs setting.`, pid, line)
continue
} else if r.cgroupMountpoints.V2Loc == "" && r.rootfsMountpoint.IsSet() {
controllerPath = r.rootfsMountpoint.ResolveHostFS(filepath.Join("/sys/fs/cgroup/unified", path))
}
// Check if there is an entry for controllerPath already cached.
r.v2ControllerPathCache.Lock()
cacheEntry, ok := r.v2ControllerPathCache.cache[controllerPath]
if ok {
// If the cached entry for controllerPath is not older than 5 minutes,
// return the cached entry.
if time.Since(cacheEntry.added) < 5*time.Minute {
cPaths.V2 = cacheEntry.pathList.V2
r.v2ControllerPathCache.Unlock()
continue
}
// Consider the existing entry for controllerPath invalid, as it is
// older than 5 minutes.
delete(r.v2ControllerPathCache.cache, controllerPath)
}
r.v2ControllerPathCache.Unlock()
cgpaths, err := os.ReadDir(controllerPath)
if err != nil {
return cPaths, fmt.Errorf("error fetching cgroupV2 controllers for cgroup location '%s' and path line '%s': %w", r.cgroupMountpoints.V2Loc, line, err)
}
// In order to produce the same kind of data for cgroups V1 and V2 controllers,
// We iterate over the group, and look for controllers, since the V2 unified system doesn't list them under the PID
for _, singlePath := range cgpaths {
if strings.Contains(singlePath.Name(), "stat") {
controllerName := strings.TrimSuffix(singlePath.Name(), ".stat")
cPaths.V2[controllerName] = ControllerPath{ControllerPath: path, FullPath: controllerPath, IsV2: true}
}
}
r.v2ControllerPathCache.Lock()
r.v2ControllerPathCache.cache[controllerPath] = pathListWithTime{
added: time.Now(),
pathList: cPaths,
}
r.v2ControllerPathCache.Unlock()
// cgroup v1
} else {
subsystems := strings.Split(fields[1], ",")
for _, subsystem := range subsystems {
fullPath := filepath.Join(r.cgroupMountpoints.V1Mounts[subsystem], path)
cPaths.V1[subsystem] = ControllerPath{ControllerPath: path, FullPath: fullPath, IsV2: false}
}
}
}
if sc.Err() != nil {
return cPaths, fmt.Errorf("error scanning cgroup file: %w", err)
}
return cPaths, nil
}