Copilot commented on code in PR #214:
URL: https://github.com/apache/skywalking-rover/pull/214#discussion_r3593145029


##########
pkg/process/finders/kubernetes/finder.go:
##########
@@ -59,10 +60,42 @@ var log = logger.GetLogger("process", "finder", 
"kubernetes")
 var (
        kubepodsRegex      = 
regexp.MustCompile(`cri-containerd-(?P<Group>\w+)\.scope`)
        openShiftPodsRegex = regexp.MustCompile(`crio-(?P<Group>\w+)\.scope`)
+       dockerPodsRegex    = regexp.MustCompile(`docker-(?P<Group>\w+)\.scope`)
        ipExistTimeout     = time.Minute * 10
        ipSearchParallel   = 10
 )
 
+// containerIDFromCgroupDir turns the base name of a cgroup directory into the 
id of the container
+// it holds, or "" when it holds none. It mirrors the naming GetProcessCGroup 
already parses out of
+// /proc/<pid>/cgroup, so that a container resolved by walking the tree and 
one resolved by reading
+// a process's cgroup line come out with the same id.
+func containerIDFromCgroupDir(dirName string) string {
+       for _, re := range []*regexp.Regexp{kubepodsRegex, openShiftPodsRegex, 
dockerPodsRegex} {
+               if m := re.FindStringSubmatch(dirName); len(m) > 1 {
+                       return m[1]
+               }
+       }
+       // the cgroupfs driver, unlike the systemd one, names the directory 
after the container id
+       if isContainerID(dirName) {
+               return dirName
+       }
+       return ""
+}

Review Comment:
   `containerIDFromCgroupDir` allocates a new slice of regex pointers on every 
call (`[]*regexp.Regexp{...}`). This function is used as the cgroup directory 
normalizer during a full tree walk, so the allocation happens once per 
directory and adds avoidable GC/CPU overhead. Consider doing the regex checks 
without per-call slice allocation (or moving the slice to a package-level var).



##########
pkg/tools/cgroup/resolver.go:
##########
@@ -0,0 +1,199 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Package cgroup resolves the cgroup ids that eBPF programs report(through
+// bpf_get_current_cgroup_id) to the container they belong to, and back.
+package cgroup
+
+import (
+       "fmt"
+       "io/fs"
+       "os"
+       "path/filepath"
+       "strings"
+       "sync"
+       "syscall"
+
+       "github.com/apache/skywalking-rover/pkg/logger"
+       "github.com/apache/skywalking-rover/pkg/tools/host"
+)
+
+var log = logger.GetLogger("tools", "cgroup")
+
+// DefaultMountPoint is where the unified(v2) cgroup hierarchy lives, as seen 
from inside the agent
+// container.
+//
+// It has to be the *host's* /sys: the agent's own only exposes its own cgroup 
subtree, and the
+// containers whose ids we need are not in it. Where the host's /sys is 
mounted is a property of the
+// deployment - the helm chart bind-mounts it at /sys, an environment where 
the node is itself a
+// container may put it elsewhere - so the prefix comes from 
ROVER_HOST_SYS_MAPPING rather than
+// being assumed, exactly as the /proc prefix does.
+func DefaultMountPoint() string {
+       return host.GetHostSysInHost("fs/cgroup")
+}
+
+const (
+       // controllersFile only exists on the unified(v2) hierarchy, which 
makes its presence the
+       // probe for "this host runs cgroup v2". On a v1-only host 
bpf_get_current_cgroup_id() reports
+       // the id from the unified hierarchy, where the containers are not, so 
every lookup would miss
+       // and the caller must disable the whole cgroup based path instead.
+       controllersFile = "cgroup.controllers"
+)
+
+// There is deliberately no bound on how deep the walk goes, because there is 
no depth to bound it
+// to. Where a container's cgroup sits is not fixed by anything:
+//
+//   - the cgroup driver decides both the naming and the number of 
levels(systemd:
+//     
kubepods.slice/kubepods-<qos>.slice/kubepods-<qos>-pod<uid>.slice/<id>.scope, 
cgroupfs:
+//     kubepods/<qos>/pod<uid>/<id>)
+//   - the QoS class adds a level for burstable/besteffort that guaranteed 
pods do not have
+//   - wherever the kubelet itself runs inside a container(kind, 
docker-in-docker) the whole
+//     hierarchy hangs under that container's cgroup, e.g. 
/system.slice/docker-<node>.scope/
+//     kubelet.slice/kubelet-kubepods.slice/..., which is three levels further 
down
+//   - the kubelet can be pointed at an arbitrary cgroup root
+//
+// If the layout were knowable the path could simply be built and stat'ed, and 
none of this would
+// exist. It is not, which is why the tree is walked and matched by name - so 
what makes a directory
+// a container is its name, never how deep it happens to be. An earlier 
version guessed a depth that
+// fit a bare node; on a nested one it matched nothing at all and did so 
silently.
+//
+// The walk itself is cheap: the tree holds at most a few thousand 
directories, only their names are
+// read, and a directory is stat'ed only once its name says it is a container.
+
+// NameNormalizer turns the base name of a cgroup directory into the id of the 
container it holds,
+// or "" when the directory does not belong to a container. It is injected 
rather than implemented
+// here because the naming is container runtime specific and the process 
finder already owns those
+// rules.
+type NameNormalizer func(dirName string) string
+
+// Resolver maps cgroup ids to container ids and back, by walking the host's 
cgroup v2 tree.
+//
+// The kernel identifies a cgroup v2 by the inode of its directory, and that 
inode is precisely
+// what bpf_get_current_cgroup_id() returns, so the mapping is built by 
stat'ing the tree.
+//
+// Reading the tree from the mounted cgroupfs - rather than from 
/proc/<pid>/cgroup - is the whole
+// point: the paths in /proc/<pid>/cgroup are rendered relative to the cgroup 
namespace of the
+// *reading* process, so an agent living in its own namespace reads 
unresolvable paths such as
+// "0::/../<container-id>", whereas a bind-mounted host cgroupfs exposes the 
full tree with real
+// inodes no matter which namespace the reader sits in.
+type Resolver struct {
+       mountPoint string
+       normalize  NameNormalizer
+
+       mu            sync.RWMutex
+       idByContainer map[string]uint64
+       containerByID map[uint64]string
+}
+
+// NewResolver builds a Resolver over mountPoint(use DefaultMountPoint unless 
testing). It does not
+// walk anything yet; call Refresh for that.
+func NewResolver(mountPoint string, normalize NameNormalizer) *Resolver {
+       return &Resolver{
+               mountPoint:    mountPoint,
+               normalize:     normalize,
+               idByContainer: make(map[string]uint64),
+               containerByID: make(map[uint64]string),
+       }
+}
+
+// Available reports whether mountPoint holds a usable unified(v2) hierarchy. 
A caller must treat a
+// false here as "the cgroup id of a process cannot be resolved on this host" 
and fall back to
+// whatever it did before, never as a reason to fail.
+func Available(mountPoint string) bool {
+       st, err := os.Stat(filepath.Join(mountPoint, controllersFile))
+       return err == nil && !st.IsDir()
+}
+
+// Refresh rebuilds the mapping from the current state of the tree. It is 
meant to be driven by the
+// periodic process discovery: a cgroup that appears between two refreshes is 
simply resolved on the
+// next one, and until then the caller behaves as it did before this package 
existed.
+func (r *Resolver) Refresh() error {
+       idByContainer := make(map[string]uint64)
+       containerByID := make(map[uint64]string)
+
+       root := filepath.Clean(r.mountPoint)
+       scanned := 0
+       err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err 
error) error {
+               if err != nil {
+                       // cgroups come and go while we walk; a vanished 
directory is normal churn on a busy
+                       // node, so skip it rather than abandoning the whole 
refresh.
+                       return nil //nolint:nilerr // deliberate: keep walking 
past transient errors
+               }

Review Comment:
   In cgroup tree walking, the WalkDir callback currently ignores *all* 
filesystem errors. That’s safe for transient `ENOENT` while cgroups churn, but 
it also silently hides non-transient errors (e.g. permission/IO errors), which 
can leave the resolver with an incomplete mapping without any signal. Consider 
only ignoring "not exists" errors and returning other errors so Refresh fails 
and the caller can reliably fall back.



##########
pkg/process/finders/kubernetes/finder.go:
##########
@@ -423,6 +498,81 @@ func (f *ProcessFinder) ShouldMonitor(pid int32) bool {
        return true
 }
 
+// ShouldMonitorExecuting judges a process the kernel has just started.
+//
+// It prefers the ordinary /proc based path, which is richer and is exactly 
what the periodic scan
+// does. Only when /proc has nothing left to say - the process already exited, 
which is the whole
+// reason it is worth catching this early - does it fall back to what the 
kernel handed us: the
+// cgroup id identifies the container, and the task name stands in for the 
command line.
+func (f *ProcessFinder) ShouldMonitorExecuting(exec 
*api.ProcessExecuteContext) bool {
+       if f.ShouldMonitor(exec.Pid) {
+               return true
+       }
+       if exec.CgroupID == 0 || f.cgroupResolver == nil {
+               // no kernel side identity to fall back on(cgroup v1, or the 
tree could not be walked)
+               return false
+       }
+       containerID, exist := 
f.cgroupResolver.ContainerByCgroupID(exec.CgroupID)
+       if !exist {
+               return false
+       }
+       pc, exist := f.registry.BuildPodContainers()[containerID]
+       if !exist || pc == nil {
+               return false
+       }

Review Comment:
   This fallback path rebuilds the full pod/container map via 
`BuildPodContainers()` to look up a single container ID. Since 
`ShouldMonitorExecuting` already calls `f.ShouldMonitor(exec.Pid)` (which also 
calls `BuildPodContainers()`), this can rebuild the map multiple times per 
kernel event. Consider caching the containers map (or adding a direct lookup 
API), and at minimum avoid rebuilding it again here by storing it in a local 
variable.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to