feat(inputs.procstat): Add option to select properties to collect (#15299)

This commit is contained in:
Sven Rebhan 2024-05-08 12:22:25 -04:00 committed by GitHub
parent 2072deb34c
commit a3b105e88e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 161 additions and 101 deletions

View File

@ -71,6 +71,9 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## user -- username owning the process
# tag_with = []
## Properties to collect
## Available options are "cpu", "limits", "memory", "mmap"
# properties = ["cpu", "limits", "memory", "mmap"]
## Method to use when finding process IDs. Can be one of 'pgrep', or
## 'native'. The pgrep finder calls the pgrep executable in the PATH while

View File

@ -17,7 +17,7 @@ type Process interface {
Name() (string, error)
SetTag(string, string)
MemoryMaps(bool) (*[]process.MemoryMapsStat, error)
Metric(prefix string, tagging map[string]bool, solarisMode bool) telegraf.Metric
Metric(string, *collectionConfig) telegraf.Metric
}
type PIDFinder interface {
@ -66,7 +66,7 @@ func (p *Proc) percent(_ time.Duration) (float64, error) {
}
// Add metrics a single Process
func (p *Proc) Metric(prefix string, tagging map[string]bool, solarisMode bool) telegraf.Metric {
func (p *Proc) Metric(prefix string, cfg *collectionConfig) telegraf.Metric {
if prefix != "" {
prefix += "_"
}
@ -118,71 +118,79 @@ func (p *Proc) Metric(prefix string, tagging map[string]bool, solarisMode bool)
fields[prefix+"created_at"] = createdAt * 1000000 // ms to ns
}
cpuTime, err := p.Times()
if err == nil {
fields[prefix+"cpu_time_user"] = cpuTime.User
fields[prefix+"cpu_time_system"] = cpuTime.System
fields[prefix+"cpu_time_iowait"] = cpuTime.Iowait // only reported on Linux
}
if cfg.features["cpu"] {
cpuTime, err := p.Times()
if err == nil {
fields[prefix+"cpu_time_user"] = cpuTime.User
fields[prefix+"cpu_time_system"] = cpuTime.System
fields[prefix+"cpu_time_iowait"] = cpuTime.Iowait // only reported on Linux
}
cpuPerc, err := p.percent(time.Duration(0))
if err == nil {
if solarisMode {
fields[prefix+"cpu_usage"] = cpuPerc / float64(runtime.NumCPU())
} else {
fields[prefix+"cpu_usage"] = cpuPerc
cpuPerc, err := p.percent(time.Duration(0))
if err == nil {
if cfg.solarisMode {
fields[prefix+"cpu_usage"] = cpuPerc / float64(runtime.NumCPU())
} else {
fields[prefix+"cpu_usage"] = cpuPerc
}
}
}
// This only returns values for RSS and VMS
mem, err := p.MemoryInfo()
if err == nil {
fields[prefix+"memory_rss"] = mem.RSS
fields[prefix+"memory_vms"] = mem.VMS
if cfg.features["memory"] {
mem, err := p.MemoryInfo()
if err == nil {
fields[prefix+"memory_rss"] = mem.RSS
fields[prefix+"memory_vms"] = mem.VMS
}
memPerc, err := p.MemoryPercent()
if err == nil {
fields[prefix+"memory_usage"] = memPerc
}
}
collectMemmap(p, prefix, fields)
memPerc, err := p.MemoryPercent()
if err == nil {
fields[prefix+"memory_usage"] = memPerc
if cfg.features["mmap"] {
collectMemmap(p, prefix, fields)
}
rlims, err := p.RlimitUsage(true)
if err == nil {
for _, rlim := range rlims {
var name string
switch rlim.Resource {
case process.RLIMIT_CPU:
name = "cpu_time"
case process.RLIMIT_DATA:
name = "memory_data"
case process.RLIMIT_STACK:
name = "memory_stack"
case process.RLIMIT_RSS:
name = "memory_rss"
case process.RLIMIT_NOFILE:
name = "num_fds"
case process.RLIMIT_MEMLOCK:
name = "memory_locked"
case process.RLIMIT_AS:
name = "memory_vms"
case process.RLIMIT_LOCKS:
name = "file_locks"
case process.RLIMIT_SIGPENDING:
name = "signals_pending"
case process.RLIMIT_NICE:
name = "nice_priority"
case process.RLIMIT_RTPRIO:
name = "realtime_priority"
default:
continue
}
if cfg.features["limits"] {
rlims, err := p.RlimitUsage(true)
if err == nil {
for _, rlim := range rlims {
var name string
switch rlim.Resource {
case process.RLIMIT_CPU:
name = "cpu_time"
case process.RLIMIT_DATA:
name = "memory_data"
case process.RLIMIT_STACK:
name = "memory_stack"
case process.RLIMIT_RSS:
name = "memory_rss"
case process.RLIMIT_NOFILE:
name = "num_fds"
case process.RLIMIT_MEMLOCK:
name = "memory_locked"
case process.RLIMIT_AS:
name = "memory_vms"
case process.RLIMIT_LOCKS:
name = "file_locks"
case process.RLIMIT_SIGPENDING:
name = "signals_pending"
case process.RLIMIT_NICE:
name = "nice_priority"
case process.RLIMIT_RTPRIO:
name = "realtime_priority"
default:
continue
}
fields[prefix+"rlimit_"+name+"_soft"] = rlim.Soft
fields[prefix+"rlimit_"+name+"_hard"] = rlim.Hard
if name != "file_locks" { // gopsutil doesn't currently track the used file locks count
fields[prefix+name] = rlim.Used
fields[prefix+"rlimit_"+name+"_soft"] = rlim.Soft
fields[prefix+"rlimit_"+name+"_hard"] = rlim.Hard
if name != "file_locks" { // gopsutil doesn't currently track the used file locks count
fields[prefix+name] = rlim.Used
}
}
}
}
@ -190,14 +198,14 @@ func (p *Proc) Metric(prefix string, tagging map[string]bool, solarisMode bool)
// Add the tags as requested by the user
cmdline, err := p.Cmdline()
if err == nil {
if tagging["cmdline"] {
if cfg.tagging["cmdline"] {
p.tags["cmdline"] = cmdline
} else {
fields[prefix+"cmdline"] = cmdline
}
}
if tagging["pid"] {
if cfg.tagging["pid"] {
p.tags["pid"] = strconv.Itoa(int(p.Pid))
} else {
fields["pid"] = p.Pid
@ -205,7 +213,7 @@ func (p *Proc) Metric(prefix string, tagging map[string]bool, solarisMode bool)
ppid, err := p.Ppid()
if err == nil {
if tagging["ppid"] {
if cfg.tagging["ppid"] {
p.tags["ppid"] = strconv.Itoa(int(ppid))
} else {
fields[prefix+"ppid"] = ppid
@ -214,7 +222,7 @@ func (p *Proc) Metric(prefix string, tagging map[string]bool, solarisMode bool)
status, err := p.Status()
if err == nil {
if tagging["status"] {
if cfg.tagging["status"] {
p.tags["status"] = status[0]
} else {
fields[prefix+"status"] = status[0]
@ -223,7 +231,7 @@ func (p *Proc) Metric(prefix string, tagging map[string]bool, solarisMode bool)
user, err := p.Username()
if err == nil {
if tagging["user"] {
if cfg.tagging["user"] {
p.tags["user"] = user
} else {
fields[prefix+"user"] = user

View File

@ -26,10 +26,15 @@ var sampleConfig string
// execCommand is so tests can mock out exec.Command usage.
var execCommand = exec.Command
var availableTags = []string{"cmdline", "pid", "ppid", "status", "user"}
type PID int32
type collectionConfig struct {
solarisMode bool
tagging map[string]bool
features map[string]bool
}
type Procstat struct {
PidFinder string `toml:"pid_finder"`
PidFile string `toml:"pid_file"`
@ -47,15 +52,15 @@ type Procstat struct {
PidTag bool `toml:"pid_tag" deprecated:"1.29.0;use 'tag_with' instead"`
WinService string `toml:"win_service"`
Mode string `toml:"mode"`
Properties []string `toml:"properties"`
TagWith []string `toml:"tag_with"`
Filter []Filter `toml:"filter"`
Log telegraf.Logger `toml:"-"`
solarisMode bool
finder PIDFinder
processes map[PID]Process
tagging map[string]bool
oldMode bool
finder PIDFinder
processes map[PID]Process
cfg collectionConfig
oldMode bool
createProcess func(PID) (Process, error)
}
@ -75,9 +80,6 @@ func (*Procstat) SampleConfig() string {
}
func (p *Procstat) Init() error {
// Check solaris mode
p.solarisMode = strings.EqualFold(p.Mode, "solaris")
// Keep the old settings for compatibility
if p.PidTag && !choice.Contains("pid", p.TagWith) {
p.TagWith = append(p.TagWith, "pid")
@ -86,13 +88,29 @@ func (p *Procstat) Init() error {
p.TagWith = append(p.TagWith, "cmdline")
}
// Check tagging and setup LUT
if err := choice.CheckSlice(p.TagWith, availableTags); err != nil {
return fmt.Errorf("invalid tag_with setting: %w", err)
}
p.tagging = make(map[string]bool, len(p.TagWith))
// Configure metric collection features
p.cfg.solarisMode = strings.EqualFold(p.Mode, "solaris")
// Convert tagging settings
p.cfg.tagging = make(map[string]bool, len(p.TagWith))
for _, tag := range p.TagWith {
p.tagging[tag] = true
switch tag {
case "cmdline", "pid", "ppid", "status", "user":
default:
return fmt.Errorf("invalid 'tag_with' setting %q", tag)
}
p.cfg.tagging[tag] = true
}
// Convert collection properties
p.cfg.features = make(map[string]bool, len(p.Properties))
for _, prop := range p.Properties {
switch prop {
case "cpu", "limits", "memory", "mmap":
default:
return fmt.Errorf("invalid 'properties' setting %q", prop)
}
p.cfg.features[prop] = true
}
// Check if we got any new-style configuration options and determine
@ -234,7 +252,7 @@ func (p *Procstat) gatherOld(acc telegraf.Accumulator) error {
p.processes[pid] = proc
}
running[pid] = true
m := proc.Metric(p.Prefix, p.tagging, p.solarisMode)
m := proc.Metric(p.Prefix, &p.cfg)
m.SetTime(now)
acc.AddMetric(m)
}
@ -333,7 +351,7 @@ func (p *Procstat) gatherNew(acc telegraf.Accumulator) error {
p.processes[pid] = proc
}
running[pid] = true
m := proc.Metric(p.Prefix, p.tagging, p.solarisMode)
m := proc.Metric(p.Prefix, &p.cfg)
m.SetTime(now)
acc.AddMetric(m)
}
@ -624,6 +642,9 @@ func (p *Procstat) winServicePIDs() ([]PID, error) {
func init() {
inputs.Add("procstat", func() telegraf.Input {
return &Procstat{createProcess: newProc}
return &Procstat{
Properties: []string{"cpu", "memory", "mmap"},
createProcess: newProc,
}
})
}

View File

@ -142,7 +142,7 @@ func (p *testProc) MemoryMaps(bool) (*[]process.MemoryMapsStat, error) {
return &[]process.MemoryMapsStat{}, nil
}
func (p *testProc) Metric(prefix string, tagging map[string]bool, _ bool) telegraf.Metric {
func (p *testProc) Metric(prefix string, cfg *collectionConfig) telegraf.Metric {
if prefix != "" {
prefix += "_"
}
@ -161,13 +161,17 @@ func (p *testProc) Metric(prefix string, tagging map[string]bool, _ bool) telegr
prefix + "write_bytes": uint64(0),
prefix + "write_count": uint64(0),
prefix + "created_at": int64(0),
prefix + "cpu_time_user": float64(0),
prefix + "cpu_time_system": float64(0),
prefix + "cpu_time_iowait": float64(0),
prefix + "cpu_usage": float64(0),
prefix + "memory_rss": uint64(0),
prefix + "memory_vms": uint64(0),
prefix + "memory_usage": float32(0),
}
if cfg.features["cpu"] {
fields[prefix+"cpu_time_user"] = float64(0)
fields[prefix+"cpu_time_system"] = float64(0)
fields[prefix+"cpu_time_iowait"] = float64(0)
fields[prefix+"cpu_usage"] = float64(0)
}
if cfg.features["memory"] {
fields[prefix+"memory_rss"] = uint64(0)
fields[prefix+"memory_vms"] = uint64(0)
fields[prefix+"memory_usage"] = float32(0)
}
tags := map[string]string{
@ -178,31 +182,31 @@ func (p *testProc) Metric(prefix string, tagging map[string]bool, _ bool) telegr
}
// Add the tags as requested by the user
if tagging["cmdline"] {
if cfg.tagging["cmdline"] {
tags["cmdline"] = "test_proc"
} else {
fields[prefix+"cmdline"] = "test_proc"
}
if tagging["pid"] {
if cfg.tagging["pid"] {
tags["pid"] = strconv.Itoa(int(p.pid))
} else {
fields["pid"] = int32(p.pid)
}
if tagging["ppid"] {
if cfg.tagging["ppid"] {
tags["ppid"] = "0"
} else {
fields[prefix+"ppid"] = int32(0)
}
if tagging["status"] {
if cfg.tagging["status"] {
tags["status"] = "running"
} else {
fields[prefix+"status"] = "running"
}
if tagging["user"] {
if cfg.tagging["user"] {
tags["user"] = "testuser"
} else {
fields[prefix+"user"] = "testuser"
@ -217,6 +221,7 @@ var exe = "foo"
func TestInitInvalidFinder(t *testing.T) {
plugin := Procstat{
PidFinder: "foo",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
createProcess: newTestProc,
}
@ -232,6 +237,7 @@ func TestInitRequiresChildDarwin(t *testing.T) {
Pattern: "somepattern",
SupervisorUnits: []string{"a_unit"},
PidFinder: "native",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
}
require.ErrorContains(t, p.Init(), "requires 'pgrep' finder")
@ -239,6 +245,7 @@ func TestInitRequiresChildDarwin(t *testing.T) {
func TestInitMissingPidMethod(t *testing.T) {
p := Procstat{
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
createProcess: newTestProc,
}
@ -265,10 +272,11 @@ func TestGather_CreateProcessErrorOk(t *testing.T) {
}
p := Procstat{
Exe: exe,
PidFinder: "test",
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
Exe: exe,
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
createProcess: func(PID) (Process, error) {
return nil, errors.New("createProcess error")
},
@ -339,6 +347,7 @@ func TestGather_ProcessName(t *testing.T) {
Exe: exe,
ProcessName: "custom_name",
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
createProcess: newTestProc,
@ -357,6 +366,7 @@ func TestGather_NoProcessNameUsesReal(t *testing.T) {
p := Procstat{
Exe: exe,
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
createProcess: newTestProc,
@ -373,6 +383,7 @@ func TestGather_NoPidTag(t *testing.T) {
p := Procstat{
Exe: exe,
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
createProcess: newTestProc,
@ -391,6 +402,7 @@ func TestGather_PidTag(t *testing.T) {
Exe: exe,
PidTag: true,
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
createProcess: newTestProc,
@ -409,6 +421,7 @@ func TestGather_Prefix(t *testing.T) {
Exe: exe,
Prefix: "custom_prefix",
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
createProcess: newTestProc,
@ -425,6 +438,7 @@ func TestGather_Exe(t *testing.T) {
p := Procstat{
Exe: exe,
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
createProcess: newTestProc,
@ -443,6 +457,7 @@ func TestGather_User(t *testing.T) {
p := Procstat{
User: user,
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
createProcess: newTestProc,
@ -461,6 +476,7 @@ func TestGather_Pattern(t *testing.T) {
p := Procstat{
Pattern: pattern,
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
createProcess: newTestProc,
@ -479,6 +495,7 @@ func TestGather_PidFile(t *testing.T) {
p := Procstat{
PidFile: pidfile,
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
createProcess: newTestProc,
@ -498,6 +515,7 @@ func TestGather_PercentFirstPass(t *testing.T) {
Pattern: "foo",
PidTag: true,
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
createProcess: newProc,
@ -518,6 +536,7 @@ func TestGather_PercentSecondPass(t *testing.T) {
Pattern: "foo",
PidTag: true,
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
createProcess: newProc,
@ -536,6 +555,7 @@ func TestGather_systemdUnitPIDs(t *testing.T) {
p := Procstat{
SystemdUnit: "TestGather_systemdUnitPIDs",
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
}
@ -560,10 +580,11 @@ func TestGather_cgroupPIDs(t *testing.T) {
require.NoError(t, err)
p := Procstat{
CGroup: td,
PidFinder: "test",
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
CGroup: td,
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
}
require.NoError(t, p.Init())
@ -579,6 +600,7 @@ func TestProcstatLookupMetric(t *testing.T) {
p := Procstat{
Exe: "-Gsys",
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{543}),
createProcess: newProc,
@ -596,6 +618,7 @@ func TestGather_SameTimestamps(t *testing.T) {
p := Procstat{
PidFile: pidfile,
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
createProcess: newTestProc,
@ -615,6 +638,7 @@ func TestGather_supervisorUnitPIDs(t *testing.T) {
p := Procstat{
SupervisorUnits: []string{"TestGather_supervisorUnitPIDs"},
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
}
@ -632,6 +656,7 @@ func TestGather_MoresupervisorUnitPIDs(t *testing.T) {
p := Procstat{
SupervisorUnits: []string{"TestGather_STARTINGsupervisorUnitPIDs", "TestGather_FATALsupervisorUnitPIDs"},
PidFinder: "test",
Properties: []string{"cpu", "memory", "mmap"},
Log: testutil.Logger{},
finder: newTestFinder([]PID{pid}),
}

View File

@ -42,6 +42,9 @@
## user -- username owning the process
# tag_with = []
## Properties to collect
## Available options are "cpu", "limits", "memory", "mmap"
# properties = ["cpu", "limits", "memory", "mmap"]
## Method to use when finding process IDs. Can be one of 'pgrep', or
## 'native'. The pgrep finder calls the pgrep executable in the PATH while