From f6572deaebf84fb8defb98113a9d513aa1c3a16e Mon Sep 17 00:00:00 2001 From: Alex K <8418476+fearful-symmetry@users.noreply.github.com> Date: Fri, 10 May 2024 09:13:52 -0700 Subject: [PATCH] Add GetOneRootEvent method to process handler (#150) ## What does this PR do? This is part of https://github.com/elastic/elastic-agent/issues/4083 This adds a `GetOneRootEvent()`. method to `ProcStats` we need this for https://github.com/elastic/elastic-agent/issues/4083 , as that issue requires metricbeat's system/process metricset to monitor a specific PID, which it can't do now. This will have to be followed up with a change to beat itself to rope this in. This also cleans up a bit of code and adds some docs. ## Why is it important? needed for https://github.com/elastic/elastic-agent/issues/4083 ## Checklist - [x] My code follows the style guidelines of this project - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have added tests that prove my fix is effective or that my feature works - [ ] I have added an entry in `CHANGELOG.md` --- metric/system/process/process.go | 28 +++++++++++++++++++----- metric/system/process/process_common.go | 15 +++++++++++-- metric/system/process/process_test.go | 29 +++++++++++++++++++++++++ metric/system/process/process_types.go | 2 ++ 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/metric/system/process/process.go b/metric/system/process/process.go index 26e4fd0990..06af0f1f6b 100644 --- a/metric/system/process/process.go +++ b/metric/system/process/process.go @@ -62,7 +62,7 @@ func ListStates(hostfs resolve.Resolver) ([]ProcState, error) { } // GetPIDState returns the state of a given PID -// It will return ProcNotExist if the process was not found. +// It will return ErrProcNotExist if the process was not found. func GetPIDState(hostfs resolve.Resolver, pid int) (PidState, error) { // This library still doesn't have a good cross-platform way to distinguish between "does not eixst" and other process errors. // This is a fairly difficult problem to solve in a cross-platform way @@ -71,7 +71,7 @@ func GetPIDState(hostfs resolve.Resolver, pid int) (PidState, error) { return "", fmt.Errorf("Error trying to find process: %d: %w", pid, err) } if !exists { - return "", ProcNotExist + return "", ErrProcNotExist } // GetInfoForPid will return the smallest possible dataset for a PID procState, err := GetInfoForPid(hostfs, pid) @@ -122,9 +122,7 @@ func (procStats *Stats) Get() ([]mapstr.M, []mapstr.M, error) { // Add the RSS pct memory first process.Memory.Rss.Pct = GetProcMemPercentage(process, totalPhyMem) // Create the root event - root := process.FormatForRoot() - rootMap := mapstr.M{} - _ = typeconv.Convert(&rootMap, root) + rootMap := processRootEvent(process) proc, err := procStats.getProcessEvent(&process) if err != nil { @@ -150,6 +148,26 @@ func (procStats *Stats) GetOne(pid int) (mapstr.M, error) { return procStats.getProcessEvent(&pidStat) } +// GetOneRootEvent is the same as `GetOne()` but it returns an +// event formatted as expected by ECS +func (procStats *Stats) GetOneRootEvent(pid int) (mapstr.M, mapstr.M, error) { + pidStat, _, err := procStats.pidFill(pid, false) + if err != nil { + return nil, nil, fmt.Errorf("error fetching PID %d: %w", pid, err) + } + + procStats.ProcsMap.SetPid(pid, pidStat) + + procMap, err := procStats.getProcessEvent(&pidStat) + if err != nil { + return nil, nil, fmt.Errorf("error formatting process %d: %w", pid, err) + } + + rootMap := processRootEvent(pidStat) + + return procMap, rootMap, err +} + // GetSelf gets process info for the beat itself // Be advised that if you call this method on a Stats object that was created with an alternate // `Hostfs` setting, this method will return data for that pid as it exists on that hostfs. diff --git a/metric/system/process/process_common.go b/metric/system/process/process_common.go index f2382edfda..29bce386b3 100644 --- a/metric/system/process/process_common.go +++ b/metric/system/process/process_common.go @@ -25,7 +25,9 @@ import ( "sync" "github.com/elastic/elastic-agent-libs/logp" + "github.com/elastic/elastic-agent-libs/mapstr" "github.com/elastic/elastic-agent-libs/match" + "github.com/elastic/elastic-agent-libs/transform/typeconv" "github.com/elastic/elastic-agent-system-metrics/metric/system/cgroup" "github.com/elastic/elastic-agent-system-metrics/metric/system/resolve" "github.com/elastic/go-sysinfo/types" @@ -33,8 +35,8 @@ import ( "github.com/elastic/go-sysinfo" ) -// ProcNotExist indicates that a process was not found. -var ProcNotExist = errors.New("process does not exist") +// ErrProcNotExist indicates that a process was not found. +var ErrProcNotExist = errors.New("process does not exist") // ProcsMap is a convenience wrapper for the oft-used idiom of map[int]ProcState type ProcsMap map[int]ProcState @@ -207,3 +209,12 @@ func (procStats *Stats) Init() error { } return nil } + +// processRootEvent formats the process state event for the ECS root fields used by the system/process metricsets +func processRootEvent(process ProcState) mapstr.M { + // Create the root event + root := process.FormatForRoot() + rootMap := mapstr.M{} + _ = typeconv.Convert(&rootMap, root) + return rootMap +} diff --git a/metric/system/process/process_test.go b/metric/system/process/process_test.go index f73e7ab00b..0ee589e044 100644 --- a/metric/system/process/process_test.go +++ b/metric/system/process/process_test.go @@ -102,6 +102,35 @@ func TestGetState(t *testing.T) { "got process state %q. Last error: %v", got, err) } +func TestGetOneRoot(t *testing.T) { + testConfig := Stats{ + Procs: []string{".*"}, + Hostfs: resolve.NewTestResolver("/"), + CPUTicks: false, + CacheCmdLine: true, + EnvWhitelist: []string{".*"}, + IncludeTop: IncludeTopConfig{ + Enabled: true, + ByCPU: 4, + ByMemory: 0, + }, + EnableCgroups: false, + CgroupOpts: cgroup.ReaderOptions{ + RootfsMountpoint: resolve.NewTestResolver("/"), + IgnoreRootCgroups: true, + }, + } + err := testConfig.Init() + assert.NoError(t, err, "Init") + + evt, rootEvt, err := testConfig.GetOneRootEvent(os.Getpid()) + require.NoError(t, err) + + require.NotEmpty(t, rootEvt["process"].(map[string]interface{})["pid"]) + + require.NotEmpty(t, evt["cpu"]) +} + func TestGetOne(t *testing.T) { testConfig := Stats{ Procs: []string{".*"}, diff --git a/metric/system/process/process_types.go b/metric/system/process/process_types.go index 5603ad6a36..6df41f29b0 100644 --- a/metric/system/process/process_types.go +++ b/metric/system/process/process_types.go @@ -139,6 +139,8 @@ func (t ProcFDInfo) IsZero() bool { return t.Open.IsZero() && t.Limit.Hard.IsZero() && t.Limit.Soft.IsZero() } +// FormatForRoot takes the ProcState event and turns the fields into a ProcStateRootEvent +// struct. These are the root fields expected for events sent from the system/process metricset. func (p *ProcState) FormatForRoot() ProcStateRootEvent { root := ProcStateRootEvent{}