diff --git a/containers/container.go b/containers/container.go index 3ad9ede8..e9e0eb6c 100644 --- a/containers/container.go +++ b/containers/container.go @@ -569,22 +569,22 @@ func (c *Container) Collect(ch chan<- prometheus.Metric) { func (c *Container) onProcessStart(pid uint32) *Process { c.lock.Lock() defer c.lock.Unlock() - stats, err := TaskstatsPID(pid) - if err != nil { + startedAt := proc.GetStartTime(pid) + if startedAt.IsZero() { return nil } if p := c.processes[pid]; p != nil { // Already registered for this same process: a connection can be // handled before the process start event (see attachTlsUprobes). // Replacing it would drop its uprobes without closing them. - if p.StartedAt.Equal(stats.BeginTime) { + if p.StartedAt.Equal(startedAt) { return p } // The pid was reused: the previous process exited unnoticed. c.closeProcess(pid, p) } c.zombieAt = time.Time{} - p := NewProcess(pid, stats, c.registry.tracer) + p := NewProcess(pid, startedAt, c.registry.tracer) if p == nil { return nil @@ -592,9 +592,9 @@ func (c *Container) onProcessStart(pid uint32) *Process { c.processes[pid] = p if c.startedAt.IsZero() { - c.startedAt = stats.BeginTime + c.startedAt = startedAt } else { - min := stats.BeginTime + min := startedAt for _, p := range c.processes { if p.StartedAt.Before(min) { min = p.StartedAt diff --git a/containers/process.go b/containers/process.go index e3437b0b..824736df 100644 --- a/containers/process.go +++ b/containers/process.go @@ -15,7 +15,6 @@ import ( "github.com/coroot/coroot-node-agent/gpu" "github.com/coroot/coroot-node-agent/proc" "github.com/jpillora/backoff" - "github.com/mdlayher/taskstats" ) type GpuUsage struct { @@ -66,8 +65,8 @@ type Process struct { nodejsChecked bool } -func NewProcess(pid uint32, stats *taskstats.Stats, tracer *ebpftracer.Tracer) *Process { - p := &Process{Pid: pid, StartedAt: stats.BeginTime} +func NewProcess(pid uint32, startedAt time.Time, tracer *ebpftracer.Tracer) *Process { + p := &Process{Pid: pid, StartedAt: startedAt} p.Flags, _ = proc.GetFlags(pid) p.ctx, p.cancelFunc = context.WithCancel(context.Background()) go p.instrument(tracer) diff --git a/containers/taskstats.go b/containers/taskstats.go index 25a645fe..3ea9dff4 100644 --- a/containers/taskstats.go +++ b/containers/taskstats.go @@ -33,16 +33,3 @@ func TaskstatsTGID(pid uint32) (*taskstats.Stats, error) { } return s, nil } - -func TaskstatsPID(pid uint32) (*taskstats.Stats, error) { - if taskstatsClient == nil { - return nil, fmt.Errorf("taskstats client not initialized") - } - taskstatsLock.Lock() - defer taskstatsLock.Unlock() - s, err := taskstatsClient.PID(int(pid)) - if err != nil { - return nil, err - } - return s, nil -} diff --git a/proc/fixtures/123/stat b/proc/fixtures/123/stat new file mode 100644 index 00000000..2b95a181 --- /dev/null +++ b/proc/fixtures/123/stat @@ -0,0 +1 @@ +123 (my (odd) proc) S 1 4242 4242 0 -1 4194560 100 0 0 0 5 3 0 0 20 0 1 0 12345 1000000 200 18446744073709551615 0 0 0 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 diff --git a/proc/proc.go b/proc/proc.go index c51348b9..df00a6b4 100644 --- a/proc/proc.go +++ b/proc/proc.go @@ -8,11 +8,28 @@ import ( "path" "strconv" "strings" + "time" "github.com/coroot/coroot-node-agent/cgroup" ) -var root = "/proc" +var ( + root = "/proc" + bootTime int64 +) + +func init() { + data, err := os.ReadFile(root + "/stat") + if err != nil { + return + } + for _, line := range strings.Split(string(data), "\n") { + if fields := strings.Fields(line); len(fields) == 2 && fields[0] == "btime" { + bootTime, _ = strconv.ParseInt(fields[1], 10, 64) + return + } + } +} func Path(pid uint32, subpath ...string) string { return path.Join(append([]string{root, strconv.Itoa(int(pid))}, subpath...)...) @@ -64,6 +81,27 @@ func ReadCgroup(pid uint32) (*cgroup.Cgroup, error) { return cgroup.NewFromProcessCgroupFile(Path(pid, "cgroup")) } +func GetStartTime(pid uint32) time.Time { + data, err := os.ReadFile(Path(pid, "stat")) + if err != nil { + return time.Time{} + } + s := string(data) + idx := strings.LastIndex(s, ")") + if idx < 0 { + return time.Time{} + } + fields := strings.Fields(s[idx+1:]) + if len(fields) < 20 { + return time.Time{} + } + startTicks, err := strconv.ParseUint(fields[19], 10, 64) + if err != nil || bootTime == 0 { + return time.Time{} + } + return time.Unix(bootTime+int64(float64(startTicks)/100), 0) +} + func ListPids() ([]uint32, error) { root, err := os.Open(root) if err != nil { diff --git a/proc/start_time_test.go b/proc/start_time_test.go new file mode 100644 index 00000000..b5be80e7 --- /dev/null +++ b/proc/start_time_test.go @@ -0,0 +1,24 @@ +package proc + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestGetStartTime(t *testing.T) { + saved := bootTime + defer func() { bootTime = saved }() + bootTime = 1700000000 + + // starttime is field 22 of /proc//stat, in clock ticks since boot: + // 12345 ticks = 123.45s. The comm contains spaces and ')', so fields + // must be counted from the last ')'. + assert.Equal(t, time.Unix(1700000000+123, 0), GetStartTime(123)) + + assert.True(t, GetStartTime(999999).IsZero(), "missing pid") + + bootTime = 0 + assert.True(t, GetStartTime(123).IsZero(), "unknown boot time") +}