feat(inputs.procstat): Allow multiple selection criteria (#14948)
Co-authored-by: Joshua Powers <powersj@fastmail.com>
This commit is contained in:
parent
fa0dbba658
commit
2acae45d09
|
|
@ -77,6 +77,34 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||||
## the native finder performs the search directly in a manor dependent on the
|
## the native finder performs the search directly in a manor dependent on the
|
||||||
## platform. Default is 'pgrep'
|
## platform. Default is 'pgrep'
|
||||||
# pid_finder = "pgrep"
|
# pid_finder = "pgrep"
|
||||||
|
|
||||||
|
## New-style filtering configuration (multiple filter sections are allowed)
|
||||||
|
# [[inputs.procstat.filter]]
|
||||||
|
# ## Name of the filter added as 'filter' tag
|
||||||
|
# name = "shell"
|
||||||
|
#
|
||||||
|
# ## Service filters, only one is allowed
|
||||||
|
# ## Systemd unit names (wildcards are supported)
|
||||||
|
# # systemd_units = []
|
||||||
|
# ## CGroup name or path (wildcards are supported)
|
||||||
|
# # cgroups = []
|
||||||
|
# ## Supervisor service names of hypervisorctl management
|
||||||
|
# # supervisor_units = []
|
||||||
|
# ## Windows service names
|
||||||
|
# # win_service = []
|
||||||
|
#
|
||||||
|
# ## Process filters, multiple are allowed
|
||||||
|
# ## Regular expressions to use for matching againt the full command
|
||||||
|
# # patterns = ['.*']
|
||||||
|
# ## List of users owning the process (wildcards are supported)
|
||||||
|
# # users = ['*']
|
||||||
|
# ## List of executable paths of the process (wildcards are supported)
|
||||||
|
# # executables = ['*']
|
||||||
|
# ## List of process names (wildcards are supported)
|
||||||
|
# # process_names = ['*']
|
||||||
|
# ## Recursion depth for determining children of the matched processes
|
||||||
|
# ## A negative value means all children with infinite depth
|
||||||
|
# # recursion_depth = 0
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows support
|
### Windows support
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
package procstat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/influxdata/telegraf/filter"
|
||||||
|
"github.com/shirou/gopsutil/v3/process"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Filter struct {
|
||||||
|
Name string `toml:"name"`
|
||||||
|
PidFiles []string `toml:"pid_files"`
|
||||||
|
SystemdUnits []string `toml:"systemd_units"`
|
||||||
|
SupervisorUnits []string `toml:"supervisor_units"`
|
||||||
|
WinService []string `toml:"win_services"`
|
||||||
|
CGroups []string `toml:"cgroups"`
|
||||||
|
Patterns []string `toml:"patterns"`
|
||||||
|
Users []string `toml:"users"`
|
||||||
|
Executables []string `toml:"executables"`
|
||||||
|
ProcessNames []string `toml:"process_names"`
|
||||||
|
RecursionDepth int `toml:"recursion_depth"`
|
||||||
|
|
||||||
|
filterSupervisorUnit string
|
||||||
|
filterCmds []*regexp.Regexp
|
||||||
|
filterUser filter.Filter
|
||||||
|
filterExecutable filter.Filter
|
||||||
|
filterProcessName filter.Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) Init() error {
|
||||||
|
if f.Name == "" {
|
||||||
|
return errors.New("filter must be named")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for only one service selector being active
|
||||||
|
var active []string
|
||||||
|
if len(f.PidFiles) > 0 {
|
||||||
|
active = append(active, "pid_files")
|
||||||
|
}
|
||||||
|
if len(f.CGroups) > 0 {
|
||||||
|
active = append(active, "cgroups")
|
||||||
|
}
|
||||||
|
if len(f.SystemdUnits) > 0 {
|
||||||
|
active = append(active, "systemd_units")
|
||||||
|
}
|
||||||
|
if len(f.SupervisorUnits) > 0 {
|
||||||
|
active = append(active, "supervisor_units")
|
||||||
|
}
|
||||||
|
if len(f.WinService) > 0 {
|
||||||
|
active = append(active, "win_services")
|
||||||
|
}
|
||||||
|
if len(active) > 1 {
|
||||||
|
return fmt.Errorf("cannot select multiple services %q", strings.Join(active, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the filters
|
||||||
|
f.filterCmds = make([]*regexp.Regexp, 0, len(f.Patterns))
|
||||||
|
for _, p := range f.Patterns {
|
||||||
|
re, err := regexp.Compile(p)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("compiling pattern %q of filter %q failed: %w", p, f.Name, err)
|
||||||
|
}
|
||||||
|
f.filterCmds = append(f.filterCmds, re)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.filterSupervisorUnit = strings.TrimSpace(strings.Join(f.SupervisorUnits, " "))
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if f.filterUser, err = filter.Compile(f.Users); err != nil {
|
||||||
|
return fmt.Errorf("compiling users filter for %q failed: %w", f.Name, err)
|
||||||
|
}
|
||||||
|
if f.filterExecutable, err = filter.Compile(f.Executables); err != nil {
|
||||||
|
return fmt.Errorf("compiling executables filter for %q failed: %w", f.Name, err)
|
||||||
|
}
|
||||||
|
if f.filterProcessName, err = filter.Compile(f.ProcessNames); err != nil {
|
||||||
|
return fmt.Errorf("compiling process-names filter for %q failed: %w", f.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) ApplyFilter() ([]processGroup, error) {
|
||||||
|
// Determine processes on service level. if there is no constraint on the
|
||||||
|
// services, use all processes for matching.
|
||||||
|
var groups []processGroup
|
||||||
|
switch {
|
||||||
|
case len(f.PidFiles) > 0:
|
||||||
|
g, err := findByPidFiles(f.PidFiles)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groups = append(groups, g...)
|
||||||
|
case len(f.CGroups) > 0:
|
||||||
|
g, err := findByCgroups(f.CGroups)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groups = append(groups, g...)
|
||||||
|
case len(f.SystemdUnits) > 0:
|
||||||
|
g, err := findBySystemdUnits(f.CGroups)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groups = append(groups, g...)
|
||||||
|
case f.filterSupervisorUnit != "":
|
||||||
|
g, err := findBySupervisorUnits(f.filterSupervisorUnit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groups = append(groups, g...)
|
||||||
|
case len(f.WinService) > 0:
|
||||||
|
g, err := findByWindowsServices(f.WinService)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groups = append(groups, g...)
|
||||||
|
default:
|
||||||
|
procs, err := process.Processes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groups = append(groups, processGroup{processes: procs, tags: make(map[string]string)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by additional properties such as users, patterns etc
|
||||||
|
result := make([]processGroup, 0, len(groups))
|
||||||
|
for _, g := range groups {
|
||||||
|
var matched []*process.Process
|
||||||
|
for _, p := range g.processes {
|
||||||
|
// Users
|
||||||
|
if f.filterUser != nil {
|
||||||
|
if username, err := p.Username(); err != nil || !f.filterUser.Match(username) {
|
||||||
|
// Errors can happen if we don't have permissions or the process no longer exists
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executables
|
||||||
|
if f.filterExecutable != nil {
|
||||||
|
if exe, err := p.Exe(); err != nil || !f.filterExecutable.Match(exe) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process names
|
||||||
|
if f.filterProcessName != nil {
|
||||||
|
if name, err := p.Name(); err != nil || !f.filterProcessName.Match(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patterns
|
||||||
|
if len(f.filterCmds) > 0 {
|
||||||
|
cmd, err := p.Cmdline()
|
||||||
|
if err != nil {
|
||||||
|
// This can happen if we don't have permissions or the process no longer exists
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var found bool
|
||||||
|
for _, re := range f.filterCmds {
|
||||||
|
if re.MatchString(cmd) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matched = append(matched, p)
|
||||||
|
}
|
||||||
|
result = append(result, processGroup{processes: matched, tags: g.tags})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve children down to the requested depth
|
||||||
|
previous := result
|
||||||
|
for depth := 0; depth < f.RecursionDepth || f.RecursionDepth < 0; depth++ {
|
||||||
|
children := make([]processGroup, 0, len(previous))
|
||||||
|
for _, group := range previous {
|
||||||
|
for _, p := range group.processes {
|
||||||
|
c, err := getChildren(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get children of process %d: %w", p.Pid, err)
|
||||||
|
}
|
||||||
|
if len(c) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := make(map[string]string, len(group.tags)+1)
|
||||||
|
for k, v := range group.tags {
|
||||||
|
tags[k] = v
|
||||||
|
}
|
||||||
|
tags["parent_pid"] = strconv.FormatInt(int64(p.Pid), 10)
|
||||||
|
children = append(children, processGroup{
|
||||||
|
processes: c,
|
||||||
|
tags: tags,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(children) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result = append(result, children...)
|
||||||
|
previous = children
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getChildren(p *process.Process) ([]*process.Process, error) {
|
||||||
|
children, err := p.Children()
|
||||||
|
// Check for cases that do not really mean error but rather means that there
|
||||||
|
// is no match.
|
||||||
|
switch {
|
||||||
|
case err == nil,
|
||||||
|
errors.Is(err, process.ErrorNoChildren),
|
||||||
|
strings.Contains(err.Error(), "exit status 1"):
|
||||||
|
return children, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unable to get children of process %d: %w", p.Pid, err)
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,11 @@
|
||||||
package procstat
|
package procstat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/coreos/go-systemd/v22/dbus"
|
||||||
"github.com/shirou/gopsutil/v3/process"
|
"github.com/shirou/gopsutil/v3/process"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -31,3 +34,45 @@ func collectMemmap(proc Process, prefix string, fields map[string]any) {
|
||||||
fields[prefix+"memory_swap"] = memMap.Swap
|
fields[prefix+"memory_swap"] = memMap.Swap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findBySystemdUnits(units []string) ([]processGroup, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
conn, err := dbus.NewSystemConnectionContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to systemd: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
sdunits, err := conn.ListUnitsByPatternsContext(ctx, []string{"enabled", "disabled", "static"}, units)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list units: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make([]processGroup, 0, len(sdunits))
|
||||||
|
for _, u := range sdunits {
|
||||||
|
prop, err := conn.GetUnitTypePropertyContext(ctx, u.Name, "Service", "MainPID")
|
||||||
|
if err != nil {
|
||||||
|
// This unit might not be a service or similar
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
raw := prop.Value.Value()
|
||||||
|
pid, ok := raw.(uint32)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("failed to parse PID %v of unit %q: invalid type %T", raw, u, raw)
|
||||||
|
}
|
||||||
|
p, err := process.NewProcess(int32(pid))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find process for PID %d of unit %q: %w", pid, u, err)
|
||||||
|
}
|
||||||
|
groups = append(groups, processGroup{
|
||||||
|
processes: []*process.Process{p},
|
||||||
|
tags: map[string]string{"systemd_unit": u.Name},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findByWindowsServices(_ []string) ([]processGroup, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,11 @@ func queryPidWithWinServiceName(_ string) (uint32, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectMemmap(Process, string, map[string]any) {}
|
func collectMemmap(Process, string, map[string]any) {}
|
||||||
|
|
||||||
|
func findBySystemdUnits(_ []string) ([]processGroup, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findByWindowsServices(_ []string) ([]processGroup, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ package procstat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v3/process"
|
"github.com/shirou/gopsutil/v3/process"
|
||||||
|
|
@ -55,3 +56,29 @@ func queryPidWithWinServiceName(winServiceName string) (uint32, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectMemmap(Process, string, map[string]any) {}
|
func collectMemmap(Process, string, map[string]any) {}
|
||||||
|
|
||||||
|
func findBySystemdUnits(_ []string) ([]processGroup, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findByWindowsServices(services []string) ([]processGroup, error) {
|
||||||
|
groups := make([]processGroup, 0, len(services))
|
||||||
|
for _, service := range services {
|
||||||
|
pid, err := queryPidWithWinServiceName(service)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query PID of service %q: %w", service, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := process.NewProcess(int32(pid))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find process for PID %d of service %q: %w", pid, service, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups = append(groups, processGroup{
|
||||||
|
processes: []*process.Process{p},
|
||||||
|
tags: map[string]string{"win_service": service},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/v3/process"
|
||||||
|
|
||||||
"github.com/influxdata/telegraf"
|
"github.com/influxdata/telegraf"
|
||||||
"github.com/influxdata/telegraf/internal/choice"
|
"github.com/influxdata/telegraf/internal/choice"
|
||||||
"github.com/influxdata/telegraf/plugins/inputs"
|
"github.com/influxdata/telegraf/plugins/inputs"
|
||||||
|
|
@ -46,12 +48,14 @@ type Procstat struct {
|
||||||
WinService string `toml:"win_service"`
|
WinService string `toml:"win_service"`
|
||||||
Mode string `toml:"mode"`
|
Mode string `toml:"mode"`
|
||||||
TagWith []string `toml:"tag_with"`
|
TagWith []string `toml:"tag_with"`
|
||||||
|
Filter []Filter `toml:"filter"`
|
||||||
Log telegraf.Logger `toml:"-"`
|
Log telegraf.Logger `toml:"-"`
|
||||||
|
|
||||||
solarisMode bool
|
solarisMode bool
|
||||||
finder PIDFinder
|
finder PIDFinder
|
||||||
processes map[PID]Process
|
processes map[PID]Process
|
||||||
tagging map[string]bool
|
tagging map[string]bool
|
||||||
|
oldMode bool
|
||||||
|
|
||||||
createProcess func(PID) (Process, error)
|
createProcess func(PID) (Process, error)
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +65,11 @@ type PidsTags struct {
|
||||||
Tags map[string]string
|
Tags map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type processGroup struct {
|
||||||
|
processes []*process.Process
|
||||||
|
tags map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
func (*Procstat) SampleConfig() string {
|
func (*Procstat) SampleConfig() string {
|
||||||
return sampleConfig
|
return sampleConfig
|
||||||
}
|
}
|
||||||
|
|
@ -86,44 +95,64 @@ func (p *Procstat) Init() error {
|
||||||
p.tagging[tag] = true
|
p.tagging[tag] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the old settings for compatibility
|
// Check if we got any new-style configuration options and determine
|
||||||
for _, u := range p.SupervisorUnit {
|
// operation mode.
|
||||||
if !choice.Contains(u, p.SupervisorUnits) {
|
p.oldMode = len(p.Filter) == 0
|
||||||
p.SupervisorUnits = append(p.SupervisorUnits, u)
|
if p.oldMode {
|
||||||
|
// Keep the old settings for compatibility
|
||||||
|
for _, u := range p.SupervisorUnit {
|
||||||
|
if !choice.Contains(u, p.SupervisorUnits) {
|
||||||
|
p.SupervisorUnits = append(p.SupervisorUnits, u)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check filtering
|
// Check filtering
|
||||||
switch {
|
switch {
|
||||||
case len(p.SupervisorUnits) > 0, p.SystemdUnit != "", p.WinService != "",
|
case len(p.SupervisorUnits) > 0, p.SystemdUnit != "", p.WinService != "",
|
||||||
p.CGroup != "", p.PidFile != "", p.Exe != "", p.Pattern != "",
|
p.CGroup != "", p.PidFile != "", p.Exe != "", p.Pattern != "",
|
||||||
p.User != "":
|
p.User != "":
|
||||||
// Do nothing as those are valid settings
|
// Do nothing as those are valid settings
|
||||||
default:
|
default:
|
||||||
return errors.New("require filter option but none set")
|
return errors.New("require filter option but none set")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instantiate the finder
|
// Instantiate the finder
|
||||||
switch p.PidFinder {
|
switch p.PidFinder {
|
||||||
case "", "pgrep":
|
case "", "pgrep":
|
||||||
p.PidFinder = "pgrep"
|
p.PidFinder = "pgrep"
|
||||||
finder, err := newPgrepFinder()
|
finder, err := newPgrepFinder()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating pgrep finder failed: %w", err)
|
return fmt.Errorf("creating pgrep finder failed: %w", err)
|
||||||
|
}
|
||||||
|
p.finder = finder
|
||||||
|
case "native":
|
||||||
|
// gopsutil relies on pgrep when looking up children on darwin
|
||||||
|
// see https://github.com/shirou/gopsutil/blob/v3.23.10/process/process_darwin.go#L235
|
||||||
|
requiresChildren := len(p.SupervisorUnits) > 0 && p.Pattern != ""
|
||||||
|
if requiresChildren && runtime.GOOS == "darwin" {
|
||||||
|
return errors.New("configuration requires 'pgrep' finder on your OS")
|
||||||
|
}
|
||||||
|
p.finder = &NativeFinder{}
|
||||||
|
case "test":
|
||||||
|
p.Log.Warn("running in test mode")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown pid_finder %q", p.PidFinder)
|
||||||
}
|
}
|
||||||
p.finder = finder
|
} else {
|
||||||
case "native":
|
// Check for mixed mode
|
||||||
// gopsutil relies on pgrep when looking up children on darwin
|
switch {
|
||||||
// see https://github.com/shirou/gopsutil/blob/v3.23.10/process/process_darwin.go#L235
|
case p.PidFile != "", p.Exe != "", p.Pattern != "", p.User != "",
|
||||||
requiresChildren := len(p.SupervisorUnits) > 0 && p.Pattern != ""
|
p.SystemdUnit != "", len(p.SupervisorUnit) > 0,
|
||||||
if requiresChildren && runtime.GOOS == "darwin" {
|
len(p.SupervisorUnits) > 0, p.CGroup != "", p.WinService != "":
|
||||||
return errors.New("configuration requires 'pgrep' finder on your OS")
|
return errors.New("cannot operate in mixed mode with filters and old-style config")
|
||||||
|
}
|
||||||
|
|
||||||
|
// New-style operations
|
||||||
|
for i := range p.Filter {
|
||||||
|
if err := p.Filter[i].Init(); err != nil {
|
||||||
|
return fmt.Errorf("initializing filter %d failed: %w", i, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
p.finder = &NativeFinder{}
|
|
||||||
case "test":
|
|
||||||
p.Log.Warn("running in test mode")
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unknown pid_finder %q", p.PidFinder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the running process cache
|
// Initialize the running process cache
|
||||||
|
|
@ -133,6 +162,14 @@ func (p *Procstat) Init() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Procstat) Gather(acc telegraf.Accumulator) error {
|
func (p *Procstat) Gather(acc telegraf.Accumulator) error {
|
||||||
|
if p.oldMode {
|
||||||
|
return p.gatherOld(acc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.gatherNew(acc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Procstat) gatherOld(acc telegraf.Accumulator) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
results, err := p.findPids()
|
results, err := p.findPids()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -226,6 +263,100 @@ func (p *Procstat) Gather(acc telegraf.Accumulator) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Procstat) gatherNew(acc telegraf.Accumulator) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for _, f := range p.Filter {
|
||||||
|
groups, err := f.ApplyFilter()
|
||||||
|
if err != nil {
|
||||||
|
// Add lookup error-metric
|
||||||
|
acc.AddFields(
|
||||||
|
"procstat_lookup",
|
||||||
|
map[string]interface{}{
|
||||||
|
"pid_count": 0,
|
||||||
|
"running": 0,
|
||||||
|
"result_code": 1,
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"filter": f.Name,
|
||||||
|
"result": "lookup_error",
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
acc.AddError(fmt.Errorf("applying filter %q failed: %w", f.Name, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int
|
||||||
|
running := make(map[PID]bool)
|
||||||
|
for _, g := range groups {
|
||||||
|
count += len(g.processes)
|
||||||
|
for _, gp := range g.processes {
|
||||||
|
// Skip over non-running processes
|
||||||
|
if running, err := gp.IsRunning(); err != nil || !running {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the cached processes as we need the existing instances
|
||||||
|
// to compute delta-metrics (e.g. cpu-usage).
|
||||||
|
pid := PID(gp.Pid)
|
||||||
|
proc, found := p.processes[pid]
|
||||||
|
if !found {
|
||||||
|
// Assumption: if a process has no name, it probably does not exist
|
||||||
|
if name, _ := gp.Name(); name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've found a process that was not recorded before so add it
|
||||||
|
// to the list of processes
|
||||||
|
tags := make(map[string]string, len(g.tags)+1)
|
||||||
|
for k, v := range g.tags {
|
||||||
|
tags[k] = v
|
||||||
|
}
|
||||||
|
if p.ProcessName != "" {
|
||||||
|
proc.SetTag("process_name", p.ProcessName)
|
||||||
|
}
|
||||||
|
tags["filter"] = f.Name
|
||||||
|
|
||||||
|
proc = &Proc{
|
||||||
|
Process: gp,
|
||||||
|
hasCPUTimes: false,
|
||||||
|
tags: tags,
|
||||||
|
}
|
||||||
|
p.processes[pid] = proc
|
||||||
|
}
|
||||||
|
running[pid] = true
|
||||||
|
m := proc.Metric(p.Prefix, p.tagging, p.solarisMode)
|
||||||
|
m.SetTime(now)
|
||||||
|
acc.AddMetric(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup processes that are not running anymore
|
||||||
|
for pid := range p.processes {
|
||||||
|
if !running[pid] {
|
||||||
|
delete(p.processes, pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add lookup statistics-metric
|
||||||
|
acc.AddFields(
|
||||||
|
"procstat_lookup",
|
||||||
|
map[string]interface{}{
|
||||||
|
"pid_count": count,
|
||||||
|
"running": len(running),
|
||||||
|
"result_code": 0,
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"filter": f.Name,
|
||||||
|
"result": "success",
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Get matching PIDs and their initial tags
|
// Get matching PIDs and their initial tags
|
||||||
func (p *Procstat) findPids() ([]PidsTags, error) {
|
func (p *Procstat) findPids() ([]PidsTags, error) {
|
||||||
switch {
|
switch {
|
||||||
|
|
|
||||||
|
|
@ -48,3 +48,31 @@
|
||||||
## the native finder performs the search directly in a manor dependent on the
|
## the native finder performs the search directly in a manor dependent on the
|
||||||
## platform. Default is 'pgrep'
|
## platform. Default is 'pgrep'
|
||||||
# pid_finder = "pgrep"
|
# pid_finder = "pgrep"
|
||||||
|
|
||||||
|
## New-style filtering configuration (multiple filter sections are allowed)
|
||||||
|
# [[inputs.procstat.filter]]
|
||||||
|
# ## Name of the filter added as 'filter' tag
|
||||||
|
# name = "shell"
|
||||||
|
#
|
||||||
|
# ## Service filters, only one is allowed
|
||||||
|
# ## Systemd unit names (wildcards are supported)
|
||||||
|
# # systemd_units = []
|
||||||
|
# ## CGroup name or path (wildcards are supported)
|
||||||
|
# # cgroups = []
|
||||||
|
# ## Supervisor service names of hypervisorctl management
|
||||||
|
# # supervisor_units = []
|
||||||
|
# ## Windows service names
|
||||||
|
# # win_service = []
|
||||||
|
#
|
||||||
|
# ## Process filters, multiple are allowed
|
||||||
|
# ## Regular expressions to use for matching againt the full command
|
||||||
|
# # patterns = ['.*']
|
||||||
|
# ## List of users owning the process (wildcards are supported)
|
||||||
|
# # users = ['*']
|
||||||
|
# ## List of executable paths of the process (wildcards are supported)
|
||||||
|
# # executables = ['*']
|
||||||
|
# ## List of process names (wildcards are supported)
|
||||||
|
# # process_names = ['*']
|
||||||
|
# ## Recursion depth for determining children of the matched processes
|
||||||
|
# ## A negative value means all children with infinite depth
|
||||||
|
# # recursion_depth = 0
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
package procstat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/v3/process"
|
||||||
|
)
|
||||||
|
|
||||||
|
func findByPidFiles(paths []string) ([]processGroup, error) {
|
||||||
|
groups := make([]processGroup, 0, len(paths))
|
||||||
|
for _, path := range paths {
|
||||||
|
buf, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read pidfile %q: %w", path, err)
|
||||||
|
}
|
||||||
|
pid, err := strconv.ParseInt(strings.TrimSpace(string(buf)), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse PID in file %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := process.NewProcess(int32(pid))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find process for PID %d of file %q: %w", pid, path, err)
|
||||||
|
}
|
||||||
|
groups = append(groups, processGroup{
|
||||||
|
processes: []*process.Process{p},
|
||||||
|
tags: map[string]string{"pidfile": path},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findByCgroups(cgroups []string) ([]processGroup, error) {
|
||||||
|
groups := make([]processGroup, 0, len(cgroups))
|
||||||
|
for _, cgroup := range cgroups {
|
||||||
|
path := cgroup
|
||||||
|
if !filepath.IsAbs(cgroup) {
|
||||||
|
path = filepath.Join("sys", "fs", "cgroup"+cgroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := filepath.Glob(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to determin files for cgroup %q: %w", cgroup, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fpath := range files {
|
||||||
|
if f, err := os.Stat(fpath); err != nil {
|
||||||
|
return nil, fmt.Errorf("accessing %q failed: %w", fpath, err)
|
||||||
|
} else if !f.IsDir() {
|
||||||
|
return nil, fmt.Errorf("%q is not a directory", fpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := filepath.Join(fpath, "cgroup.procs")
|
||||||
|
buf, err := os.ReadFile(fn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lines := bytes.Split(buf, []byte{'\n'})
|
||||||
|
procs := make([]*process.Process, 0, len(lines))
|
||||||
|
for _, l := range lines {
|
||||||
|
l := strings.TrimSpace(string(l))
|
||||||
|
if len(l) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pid, err := strconv.ParseInt(l, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse PID %q in file %q", l, fpath)
|
||||||
|
}
|
||||||
|
p, err := process.NewProcess(int32(pid))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find process for PID %d of %q: %w", pid, fpath, err)
|
||||||
|
}
|
||||||
|
procs = append(procs, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups = append(groups, processGroup{
|
||||||
|
processes: procs,
|
||||||
|
tags: map[string]string{"cgroup": cgroup, "cgroup_full": fpath}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBySupervisorUnits(units string) ([]processGroup, error) {
|
||||||
|
buf, err := execCommand("supervisorctl", "status", units, " ").Output()
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "exit status 3") {
|
||||||
|
// Exit 3 means at least on process is in one of the "STOPPED" states
|
||||||
|
return nil, fmt.Errorf("failed to execute 'supervisorctl': %w", err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(buf), "\n")
|
||||||
|
|
||||||
|
// Get the PID, running status, running time and boot time of the main process:
|
||||||
|
// pid 11779, uptime 17:41:16
|
||||||
|
// Exited too quickly (process log may have details)
|
||||||
|
groups := make([]processGroup, 0, len(lines))
|
||||||
|
for _, line := range lines {
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kv := strings.Fields(line)
|
||||||
|
if len(kv) < 2 {
|
||||||
|
// Not a key-value pair
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name, status := kv[0], kv[1]
|
||||||
|
tags := map[string]string{
|
||||||
|
"supervisor_unit": name,
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
var procs []*process.Process
|
||||||
|
switch status {
|
||||||
|
case "FATAL", "EXITED", "BACKOFF", "STOPPING":
|
||||||
|
tags["error"] = strings.Join(kv[2:], " ")
|
||||||
|
case "RUNNING":
|
||||||
|
tags["uptimes"] = kv[5]
|
||||||
|
rawpid := strings.ReplaceAll(kv[3], ",", "")
|
||||||
|
grouppid, err := strconv.ParseInt(rawpid, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse group PID %q: %w", rawpid, err)
|
||||||
|
}
|
||||||
|
p, err := process.NewProcess(int32(grouppid))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find process for PID %d of unit %q: %w", grouppid, name, err)
|
||||||
|
}
|
||||||
|
// Get all children of the supervisor unit
|
||||||
|
procs, err = p.Children()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get children for PID %d of unit %q: %w", grouppid, name, err)
|
||||||
|
}
|
||||||
|
tags["parent_pid"] = rawpid
|
||||||
|
case "STOPPED", "UNKNOWN", "STARTING":
|
||||||
|
// No additional info
|
||||||
|
}
|
||||||
|
|
||||||
|
groups = append(groups, processGroup{
|
||||||
|
processes: procs,
|
||||||
|
tags: tags,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue