diff --git a/plugins/inputs/procstat/README.md b/plugins/inputs/procstat/README.md index 5217df6f0..5f91f4004 100644 --- a/plugins/inputs/procstat/README.md +++ b/plugins/inputs/procstat/README.md @@ -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 diff --git a/plugins/inputs/procstat/process.go b/plugins/inputs/procstat/process.go index e2b227a0e..bb92bc675 100644 --- a/plugins/inputs/procstat/process.go +++ b/plugins/inputs/procstat/process.go @@ -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 diff --git a/plugins/inputs/procstat/procstat.go b/plugins/inputs/procstat/procstat.go index ab17aea37..51ed32393 100644 --- a/plugins/inputs/procstat/procstat.go +++ b/plugins/inputs/procstat/procstat.go @@ -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, + } }) } diff --git a/plugins/inputs/procstat/procstat_test.go b/plugins/inputs/procstat/procstat_test.go index 3cc02b920..5221d8314 100644 --- a/plugins/inputs/procstat/procstat_test.go +++ b/plugins/inputs/procstat/procstat_test.go @@ -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}), } diff --git a/plugins/inputs/procstat/sample.conf b/plugins/inputs/procstat/sample.conf index 1609a958f..20e003d5b 100644 --- a/plugins/inputs/procstat/sample.conf +++ b/plugins/inputs/procstat/sample.conf @@ -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