Skip to content

Commit

Permalink
Add GetOneRootEvent method to process handler (#150)
Browse files Browse the repository at this point in the history
## What does this PR do?

This is part of elastic/elastic-agent#4083 

This adds a `GetOneRootEvent()`. method to `ProcStats` we need this for
elastic/elastic-agent#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 elastic/elastic-agent#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`
  • Loading branch information
fearful-symmetry authored May 10, 2024
1 parent 38970da commit f6572de
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 7 deletions.
28 changes: 23 additions & 5 deletions metric/system/process/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand Down
15 changes: 13 additions & 2 deletions metric/system/process/process_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,18 @@ 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"

"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
Expand Down Expand Up @@ -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
}
29 changes: 29 additions & 0 deletions metric/system/process/process_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{".*"},
Expand Down
2 changes: 2 additions & 0 deletions metric/system/process/process_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}

Expand Down

0 comments on commit f6572de

Please sign in to comment.