mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
test(agentcompat): capture FD observations in process samples
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
@@ -103,14 +103,23 @@ func readParentPID(pid int) (int, error) {
|
|||||||
return parentPID, nil
|
return parentPID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func processFDs(pid int) (int, map[uint64]struct{}, error) {
|
type FDObservation struct {
|
||||||
|
Number int
|
||||||
|
Target string
|
||||||
|
}
|
||||||
|
|
||||||
|
func processFDs(pid int, captureObservations bool) (int, map[uint64]struct{}, []FDObservation, error) {
|
||||||
directory := filepath.Join("/proc", strconv.Itoa(pid), "fd")
|
directory := filepath.Join("/proc", strconv.Itoa(pid), "fd")
|
||||||
entries, err := os.ReadDir(directory)
|
entries, err := os.ReadDir(directory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, nil, err
|
||||||
}
|
}
|
||||||
count := 0
|
count := 0
|
||||||
sockets := make(map[uint64]struct{})
|
sockets := make(map[uint64]struct{})
|
||||||
|
var observations []FDObservation
|
||||||
|
if captureObservations {
|
||||||
|
observations = make([]FDObservation, 0, len(entries))
|
||||||
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
descriptor, err := strconv.Atoi(entry.Name())
|
descriptor, err := strconv.Atoi(entry.Name())
|
||||||
if err != nil || descriptor < 3 {
|
if err != nil || descriptor < 3 {
|
||||||
@@ -121,14 +130,22 @@ func processFDs(pid int) (int, map[uint64]struct{}, error) {
|
|||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return 0, nil, err
|
return 0, nil, nil, err
|
||||||
}
|
}
|
||||||
count++
|
count++
|
||||||
|
if captureObservations {
|
||||||
|
observations = append(observations, FDObservation{Number: descriptor, Target: target})
|
||||||
|
}
|
||||||
if inode, exists := parseSocketInode(target); exists {
|
if inode, exists := parseSocketInode(target); exists {
|
||||||
sockets[inode] = struct{}{}
|
sockets[inode] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count, sockets, nil
|
if captureObservations {
|
||||||
|
sort.Slice(observations, func(left, right int) bool {
|
||||||
|
return observations[left].Number < observations[right].Number || (observations[left].Number == observations[right].Number && observations[left].Target < observations[right].Target)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return count, sockets, observations, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSocketInode(target string) (uint64, bool) {
|
func parseSocketInode(target string) (uint64, bool) {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ type Sample struct {
|
|||||||
NonStdioFDCount int `json:"non_stdio_fd_count"`
|
NonStdioFDCount int `json:"non_stdio_fd_count"`
|
||||||
TCPListenerCount int `json:"tcp_listener_count"`
|
TCPListenerCount int `json:"tcp_listener_count"`
|
||||||
TCP6ListenerCount int `json:"tcp6_listener_count"`
|
TCP6ListenerCount int `json:"tcp6_listener_count"`
|
||||||
|
FDObservations []FDObservation `json:"-"`
|
||||||
|
SampledAt time.Time `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Window struct {
|
type Window struct {
|
||||||
@@ -31,10 +33,19 @@ type WindowSpec struct {
|
|||||||
PID int
|
PID int
|
||||||
Interval time.Duration
|
Interval time.Duration
|
||||||
AllowTerminated bool
|
AllowTerminated bool
|
||||||
|
CaptureFDObservations bool
|
||||||
ObserveSample func(context.Context, Sample) error
|
ObserveSample func(context.Context, Sample) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func SampleProcess(pid int) (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)
|
rssBytes, err := readRSSBytes(pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Sample{}, err
|
return Sample{}, err
|
||||||
@@ -43,7 +54,7 @@ func SampleProcess(pid int) (Sample, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Sample{}, err
|
return Sample{}, err
|
||||||
}
|
}
|
||||||
fdCount, socketInodes, err := processFDs(pid)
|
fdCount, socketInodes, fdObservations, err := processFDs(pid, captureFDObservations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Sample{}, err
|
return Sample{}, err
|
||||||
}
|
}
|
||||||
@@ -63,6 +74,8 @@ func SampleProcess(pid int) (Sample, error) {
|
|||||||
NonStdioFDCount: fdCount,
|
NonStdioFDCount: fdCount,
|
||||||
TCPListenerCount: intersectionCount(socketInodes, tcpListeners),
|
TCPListenerCount: intersectionCount(socketInodes, tcpListeners),
|
||||||
TCP6ListenerCount: intersectionCount(socketInodes, tcp6Listeners),
|
TCP6ListenerCount: intersectionCount(socketInodes, tcp6Listeners),
|
||||||
|
FDObservations: fdObservations,
|
||||||
|
SampledAt: time.Now(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +94,7 @@ func SampleWindow(ctx context.Context, spec WindowSpec) (Window, error) {
|
|||||||
return Window{}, ctx.Err()
|
return Window{}, ctx.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sample, err := SampleProcess(spec.PID)
|
sample, err := sampleProcess(spec.PID, spec.CaptureFDObservations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if spec.AllowTerminated && os.IsNotExist(err) {
|
if spec.AllowTerminated && os.IsNotExist(err) {
|
||||||
return window, nil
|
return window, nil
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSampleProcessWithFDObservations_CapturesEverySuccessfulNonStdioReadlink(t *testing.T) {
|
||||||
|
// Given / When
|
||||||
|
sample, err := SampleProcessWithFDObservations(os.Getpid())
|
||||||
|
|
||||||
|
// Then
|
||||||
|
requireNoError(t, err)
|
||||||
|
if len(sample.FDObservations) != sample.NonStdioFDCount {
|
||||||
|
t.Fatalf("FD observations = %d, non-stdio FD count = %d", len(sample.FDObservations), sample.NonStdioFDCount)
|
||||||
|
}
|
||||||
|
if sample.SampledAt.IsZero() {
|
||||||
|
t.Fatal("sampled at is zero")
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(sample)
|
||||||
|
requireNoError(t, err)
|
||||||
|
var evidence map[string]json.RawMessage
|
||||||
|
requireNoError(t, json.Unmarshal(encoded, &evidence))
|
||||||
|
if _, exists := evidence["sampled_at"]; exists {
|
||||||
|
t.Fatalf("sampled_at leaked into evidence JSON: %s", encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSampleWindowWithFDObservations_RecordsCompletionTimeForEverySample(t *testing.T) {
|
||||||
|
// Given / When
|
||||||
|
window, err := SampleWindow(context.Background(), WindowSpec{PID: os.Getpid(), Interval: time.Nanosecond, CaptureFDObservations: true})
|
||||||
|
|
||||||
|
// Then
|
||||||
|
requireNoError(t, err)
|
||||||
|
for index, sample := range window.Samples {
|
||||||
|
if sample.SampledAt.IsZero() {
|
||||||
|
t.Fatalf("sample %d sampled at is zero", index+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user