Files

123 lines
3.2 KiB
Go

//go:build linux
package process
import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
)
type Sample struct {
PID int `json:"pid"`
RSSBytes uint64 `json:"rss_bytes"`
DescendantPIDs []int `json:"descendant_pids"`
DescendantCount int `json:"descendant_count"`
NonStdioFDCount int `json:"non_stdio_fd_count"`
TCPListenerCount int `json:"tcp_listener_count"`
TCP6ListenerCount int `json:"tcp6_listener_count"`
FDObservations []FDObservation `json:"-"`
SampledAt time.Time `json:"-"`
}
type Window struct {
PID int `json:"pid"`
Samples []Sample `json:"samples"`
}
type WindowSpec struct {
PID int
Interval time.Duration
AllowTerminated bool
CaptureFDObservations bool
ObserveSample func(context.Context, Sample) error
}
func SampleProcess(pid int) (Sample, error) {
return sampleProcess(pid, false)
}
func SampleProcessWithFDObservations(pid int) (Sample, error) {
return sampleProcess(pid, true)
}
func sampleProcess(pid int, captureFDObservations bool) (Sample, error) {
rssBytes, err := readRSSBytes(pid)
if err != nil {
return Sample{}, err
}
descendants, err := descendantPIDs(pid)
if err != nil {
return Sample{}, err
}
fdCount, socketInodes, fdObservations, err := processFDs(pid, captureFDObservations)
if err != nil {
return Sample{}, err
}
tcpListeners, err := listeningSocketInodes(pid, "tcp")
if err != nil {
return Sample{}, err
}
tcp6Listeners, err := listeningSocketInodes(pid, "tcp6")
if err != nil {
return Sample{}, err
}
return Sample{
PID: pid,
RSSBytes: rssBytes,
DescendantPIDs: descendants,
DescendantCount: len(descendants),
NonStdioFDCount: fdCount,
TCPListenerCount: intersectionCount(socketInodes, tcpListeners),
TCP6ListenerCount: intersectionCount(socketInodes, tcp6Listeners),
FDObservations: fdObservations,
SampledAt: time.Now(),
}, nil
}
func SampleWindow(ctx context.Context, spec WindowSpec) (Window, error) {
if spec.PID < 1 || spec.Interval <= 0 {
return Window{}, errors.New("invalid sample window specification")
}
window := Window{PID: spec.PID, Samples: make([]Sample, 0, contract.ResourceSampleCount)}
for index := 0; index < contract.ResourceSampleCount; index++ {
if index > 0 {
timer := time.NewTimer(spec.Interval)
select {
case <-timer.C:
case <-ctx.Done():
timer.Stop()
return Window{}, ctx.Err()
}
}
sample, err := sampleProcess(spec.PID, spec.CaptureFDObservations)
if err != nil {
if spec.AllowTerminated && os.IsNotExist(err) {
return window, nil
}
return Window{}, fmt.Errorf("sample %d of PID %d: %w", index+1, spec.PID, err)
}
window.Samples = append(window.Samples, sample)
if spec.ObserveSample != nil {
if err := spec.ObserveSample(ctx, sample); err != nil {
return window, err
}
}
}
return window, nil
}
func intersectionCount(left, right map[uint64]struct{}) int {
count := 0
for value := range left {
if _, exists := right[value]; exists {
count++
}
}
return count
}