diff --git a/glide.yaml b/glide.yaml index bd20e230a27..1f9c67b9e0f 100644 --- a/glide.yaml +++ b/glide.yaml @@ -25,7 +25,7 @@ import: subpackages: - /difflib - package: github.com/elastic/gosigar - version: 171a3c9e31dde9688c154ba94be6cd5d8a78bf64 + version: b49e01eb1e5c68c469392a63feaccae9352ceb12 - package: github.com/elastic/procfs version: abf152e5f3e97f2fafac028d2cc06c1feb87ffa5 - package: github.com/samuel/go-parser diff --git a/metricbeat/_meta/beat.full.yml b/metricbeat/_meta/beat.full.yml index e8c7d5a0ae5..f4065b153c0 100644 --- a/metricbeat/_meta/beat.full.yml +++ b/metricbeat/_meta/beat.full.yml @@ -67,6 +67,10 @@ metricbeat.modules: # EXPERIMENTAL: cgroups can be enabled for the process metricset. #cgroups: false + # A list of regular expressions used to whitelist environment variables + # reported with the process metricset's events. Defaults to empty. + #process.env.whitelist: [] + # Configure reverse DNS lookup on remote IP addresses in the socket metricset. #socket.reverse_lookup.enabled: false #socket.reverse_lookup.success_ttl: 60s diff --git a/metricbeat/docs/fields.asciidoc b/metricbeat/docs/fields.asciidoc index c3c61afdca4..1afc271c34d 100644 --- a/metricbeat/docs/fields.asciidoc +++ b/metricbeat/docs/fields.asciidoc @@ -5797,6 +5797,14 @@ type: keyword The username of the user that created the process. If the username cannot be determined, the field will contain the user's numeric identifier (UID). On Windows, this field includes the user's domain and is formatted as `domain\username`. +[float] +=== system.process.env + +type: dict + +The environment variables used to start the process. The data is available on FreeBSD, Linux, and OS X. + + [float] == cpu Fields diff --git a/metricbeat/metricbeat.full.yml b/metricbeat/metricbeat.full.yml index d655806d98a..683008110ed 100644 --- a/metricbeat/metricbeat.full.yml +++ b/metricbeat/metricbeat.full.yml @@ -67,6 +67,10 @@ metricbeat.modules: # EXPERIMENTAL: cgroups can be enabled for the process metricset. #cgroups: false + # A list of regular expressions used to whitelist environment variables + # reported with the process metricset's events. Defaults to empty. + #process.env.whitelist: [] + # Configure reverse DNS lookup on remote IP addresses in the socket metricset. #socket.reverse_lookup.enabled: false #socket.reverse_lookup.success_ttl: 60s diff --git a/metricbeat/module/system/_meta/config.full.yml b/metricbeat/module/system/_meta/config.full.yml index a244aa157b9..8884a946386 100644 --- a/metricbeat/module/system/_meta/config.full.yml +++ b/metricbeat/module/system/_meta/config.full.yml @@ -39,6 +39,10 @@ # EXPERIMENTAL: cgroups can be enabled for the process metricset. #cgroups: false + # A list of regular expressions used to whitelist environment variables + # reported with the process metricset's events. Defaults to empty. + #process.env.whitelist: [] + # Configure reverse DNS lookup on remote IP addresses in the socket metricset. #socket.reverse_lookup.enabled: false #socket.reverse_lookup.success_ttl: 60s diff --git a/metricbeat/module/system/process/_meta/fields.yml b/metricbeat/module/system/process/_meta/fields.yml index 853627c8b7f..d3cb2fcece8 100644 --- a/metricbeat/module/system/process/_meta/fields.yml +++ b/metricbeat/module/system/process/_meta/fields.yml @@ -35,6 +35,12 @@ cannot be determined, the field will contain the user's numeric identifier (UID). On Windows, this field includes the user's domain and is formatted as `domain\username`. + - name: env + type: dict + dict-type: keyword + description: > + The environment variables used to start the process. The data is + available on FreeBSD, Linux, and OS X. - name: cpu type: group prefix: "[float]" diff --git a/metricbeat/module/system/process/helper.go b/metricbeat/module/system/process/helper.go index 96d11914302..adce7f353ff 100644 --- a/metricbeat/module/system/process/helper.go +++ b/metricbeat/module/system/process/helper.go @@ -31,18 +31,23 @@ type Process struct { Cpu sigar.ProcTime Ctime time.Time FD sigar.ProcFDUsage + Env common.MapStr } type ProcStats struct { - Procs []string - regexps []*regexp.Regexp - ProcsMap ProcsMap - CpuTicks bool -} + Procs []string + ProcsMap ProcsMap + CpuTicks bool + EnvWhitelist []string -// newProcess creates a new Process object based on the state information. -func newProcess(pid int) (*Process, error) { + procRegexps []*regexp.Regexp // List of regular expressions used to whitelist processes. + envRegexps []*regexp.Regexp // List of regular expressions used to whitelist env vars. +} +// newProcess creates a new Process object and initializes it with process +// state information. If the process's command line and environment variables +// are known they should be passed in to avoid re-fetching the information. +func newProcess(pid int, cmdline string, env common.MapStr) (*Process, error) { state := sigar.ProcState{} if err := state.Get(pid); err != nil { return nil, fmt.Errorf("error getting process state for pid=%d: %v", pid, err) @@ -53,17 +58,22 @@ func newProcess(pid int) (*Process, error) { Ppid: state.Ppid, Pgid: state.Pgid, Name: state.Name, - State: getProcState(byte(state.State)), Username: state.Username, + State: getProcState(byte(state.State)), + CmdLine: cmdline, Ctime: time.Now(), + Env: env, } return &proc, nil } -// getDetails fills in CPU, memory, FD usage, and command line details for the process. -func (proc *Process) getDetails(cmdline string) error { - +// getDetails fetches CPU, memory, FD usage, command line arguments, and +// environment variables for the process. The envPredicate parameter is an +// optional predicate function that should return true if an environment +// variable should be saved with the process. If the argument is nil then all +// environment variables are stored. +func (proc *Process) getDetails(envPredicate func(string) bool) error { proc.Mem = sigar.ProcMem{} if err := proc.Mem.Get(proc.Pid); err != nil { return fmt.Errorf("error getting process mem for pid=%d: %v", proc.Pid, err) @@ -74,14 +84,12 @@ func (proc *Process) getDetails(cmdline string) error { return fmt.Errorf("error getting process cpu time for pid=%d: %v", proc.Pid, err) } - if cmdline == "" { + if proc.CmdLine == "" { args := sigar.ProcArgs{} if err := args.Get(proc.Pid); err != nil && !sigar.IsNotImplemented(err) { return fmt.Errorf("error getting process arguments for pid=%d: %v", proc.Pid, err) } proc.CmdLine = strings.Join(args.List, " ") - } else { - proc.CmdLine = cmdline } if fd, err := getProcFDUsage(proc.Pid); err != nil { @@ -90,6 +98,13 @@ func (proc *Process) getDetails(cmdline string) error { proc.FD = *fd } + if proc.Env == nil { + proc.Env = common.MapStr{} + if err := getProcEnv(proc.Pid, proc.Env, envPredicate); err != nil { + return fmt.Errorf("error getting process environment variables for pid=%d: %v", proc.Pid, err) + } + } + return nil } @@ -120,6 +135,35 @@ func getProcFDUsage(pid int) (*sigar.ProcFDUsage, error) { return &fd, nil } +// getProcEnv gets the process's environment variables and writes them to the +// out parameter. It handles ErrNotImplemented and permission errors. Any other +// errors are returned. +// +// The filter function should return true if a given environment variable should +// be added to the out parameter. +// +// On Linux you must be root to read other processes' environment variables. +func getProcEnv(pid int, out common.MapStr, filter func(v string) bool) error { + env := &sigar.ProcEnv{} + if err := env.Get(pid); err != nil { + switch { + case sigar.IsNotImplemented(err): + return nil + case os.IsPermission(err): + return nil + default: + return err + } + } + + for k, v := range env.Vars { + if filter == nil || filter(k) { + out[k] = v + } + } + return nil +} + func GetProcMemPercentage(proc *Process, totalPhyMem uint64) float64 { // in unit tests, total_phymem is set to a value greater than zero @@ -186,6 +230,10 @@ func (procStats *ProcStats) GetProcessEvent(process *Process, last *Process) com proc["cmdline"] = process.CmdLine } + if len(process.Env) > 0 { + proc["env"] = process.Env + } + if procStats.CpuTicks { proc["cpu"] = common.MapStr{ "user": process.Cpu.User, @@ -233,7 +281,7 @@ func GetProcCpuPercentage(last *Process, current *Process) float64 { func (procStats *ProcStats) MatchProcess(name string) bool { - for _, reg := range procStats.regexps { + for _, reg := range procStats.procRegexps { if reg.MatchString(name) { return true } @@ -249,13 +297,22 @@ func (procStats *ProcStats) InitProcStats() error { return nil } - procStats.regexps = []*regexp.Regexp{} + procStats.procRegexps = []*regexp.Regexp{} for _, pattern := range procStats.Procs { reg, err := regexp.Compile(pattern) if err != nil { return fmt.Errorf("Failed to compile regexp [%s]: %v", pattern, err) } - procStats.regexps = append(procStats.regexps, reg) + procStats.procRegexps = append(procStats.procRegexps, reg) + } + + procStats.envRegexps = make([]*regexp.Regexp, 0, len(procStats.EnvWhitelist)) + for _, pattern := range procStats.EnvWhitelist { + reg, err := regexp.Compile(pattern) + if err != nil { + return fmt.Errorf("failed to compile env whitelist regexp [%v]: %v", pattern, err) + } + procStats.envRegexps = append(procStats.envRegexps, reg) } return nil @@ -278,18 +335,20 @@ func (procStats *ProcStats) GetProcStats() ([]common.MapStr, error) { for _, pid := range pids { var cmdline string + var env common.MapStr if previousProc := procStats.ProcsMap[pid]; previousProc != nil { cmdline = previousProc.CmdLine + env = previousProc.Env } - process, err := newProcess(pid) + process, err := newProcess(pid, cmdline, env) if err != nil { logp.Debug("metricbeat", "Skip process pid=%d: %v", pid, err) continue } if procStats.MatchProcess(process.Name) { - err = process.getDetails(cmdline) + err = process.getDetails(procStats.isWhitelistedEnvVar) if err != nil { logp.Err("Error getting process details. pid=%d: %v", process.Pid, err) continue @@ -297,7 +356,7 @@ func (procStats *ProcStats) GetProcStats() ([]common.MapStr, error) { newProcs[process.Pid] = process - last, _ := procStats.ProcsMap[process.Pid] + last := procStats.ProcsMap[process.Pid] proc := procStats.GetProcessEvent(process, last) processes = append(processes, proc) @@ -308,6 +367,21 @@ func (procStats *ProcStats) GetProcStats() ([]common.MapStr, error) { return processes, nil } +// isWhitelistedEnvVar returns true if the given variable name is a match for +// the whitelist. If the whitelist is empty it returns false. +func (p ProcStats) isWhitelistedEnvVar(varName string) bool { + if len(p.envRegexps) == 0 { + return false + } + + for _, p := range p.envRegexps { + if p.MatchString(varName) { + return true + } + } + return false +} + // unixTimeMsToTime converts a unix time given in milliseconds since Unix epoch // to a common.Time value. func unixTimeMsToTime(unixTimeMs uint64) common.Time { diff --git a/metricbeat/module/system/process/helper_test.go b/metricbeat/module/system/process/helper_test.go index c271580da57..b779277b8b6 100644 --- a/metricbeat/module/system/process/helper_test.go +++ b/metricbeat/module/system/process/helper_test.go @@ -4,9 +4,12 @@ package process import ( + "os" + "runtime" "testing" "time" + "github.com/elastic/beats/libbeat/common" "github.com/elastic/gosigar" "github.com/stretchr/testify/assert" ) @@ -22,47 +25,41 @@ func TestPids(t *testing.T) { } func TestGetProcess(t *testing.T) { - pids, err := Pids() - - assert.Nil(t, err) - - for _, pid := range pids { + process, err := newProcess(os.Getpid(), "", nil) + if err != nil { + t.Fatal(err) + } + if err = process.getDetails(nil); err != nil { + t.Fatal(err) + } - process, err := newProcess(pid) - if err != nil { - continue - } - err = process.getDetails("") - assert.NoError(t, err) - assert.NotNil(t, process) - - assert.True(t, (process.Pid > 0)) - assert.True(t, (process.Ppid >= 0)) - assert.True(t, (process.Pgid >= 0)) - assert.True(t, (len(process.Name) > 0)) - assert.True(t, (len(process.Username) > 0)) - assert.NotEqual(t, "unknown", process.State) - - // Memory Checks - assert.True(t, (process.Mem.Size >= 0)) - assert.True(t, (process.Mem.Resident >= 0)) - assert.True(t, (process.Mem.Share >= 0)) - - // CPU Checks - assert.True(t, (process.Cpu.StartTime > 0)) - assert.True(t, (process.Cpu.Total >= 0)) - assert.True(t, (process.Cpu.User >= 0)) - assert.True(t, (process.Cpu.Sys >= 0)) - - assert.True(t, (process.Ctime.Unix() <= time.Now().Unix())) - - // it's enough to get valid data for a single process - break + assert.True(t, (process.Pid > 0)) + assert.True(t, (process.Ppid >= 0)) + assert.True(t, (process.Pgid >= 0)) + assert.True(t, (len(process.Name) > 0)) + assert.True(t, (len(process.Username) > 0)) + assert.NotEqual(t, "unknown", process.State) + + // Memory Checks + assert.True(t, (process.Mem.Size >= 0)) + assert.True(t, (process.Mem.Resident >= 0)) + assert.True(t, (process.Mem.Share >= 0)) + + // CPU Checks + assert.True(t, (process.Cpu.StartTime > 0)) + assert.True(t, (process.Cpu.Total >= 0)) + assert.True(t, (process.Cpu.User >= 0)) + assert.True(t, (process.Cpu.Sys >= 0)) + + assert.True(t, (process.Ctime.Unix() <= time.Now().Unix())) + + switch runtime.GOOS { + case "darwin", "linux", "freebsd": + assert.True(t, len(process.Env) > 0, "empty environment") } } func TestProcState(t *testing.T) { - assert.Equal(t, getProcState('R'), "running") assert.Equal(t, getProcState('S'), "sleeping") assert.Equal(t, getProcState('s'), "unknown") @@ -72,7 +69,6 @@ func TestProcState(t *testing.T) { } func TestMatchProcs(t *testing.T) { - var procStats = ProcStats{} procStats.Procs = []string{".*"} @@ -94,7 +90,6 @@ func TestMatchProcs(t *testing.T) { } func TestProcMemPercentage(t *testing.T) { - procStats := ProcStats{} p := Process{ @@ -113,7 +108,6 @@ func TestProcMemPercentage(t *testing.T) { } func TestProcCpuPercentage(t *testing.T) { - procStats := ProcStats{} ctime := time.Now() @@ -146,7 +140,7 @@ func TestProcCpuPercentage(t *testing.T) { } // BenchmarkGetProcess runs a benchmark of the GetProcess method with caching -// of the command line arguments enabled. +// of the command line and environment variables. func BenchmarkGetProcess(b *testing.B) { pids, err := Pids() if err != nil { @@ -160,15 +154,17 @@ func BenchmarkGetProcess(b *testing.B) { pid := pids[i%nPids] var cmdline string + var env common.MapStr if p := procs[pid]; p != nil { cmdline = p.CmdLine + env = p.Env } - process, err := newProcess(pid) + process, err := newProcess(pid, cmdline, env) if err != nil { continue } - err = process.getDetails(cmdline) + err = process.getDetails(nil) assert.NoError(b, err) procs[pid] = process diff --git a/metricbeat/module/system/process/process.go b/metricbeat/module/system/process/process.go index fbcf95a0b55..ca3081e43f4 100644 --- a/metricbeat/module/system/process/process.go +++ b/metricbeat/module/system/process/process.go @@ -34,13 +34,13 @@ type MetricSet struct { // New creates and returns a new MetricSet. func New(base mb.BaseMetricSet) (mb.MetricSet, error) { config := struct { - Procs []string `config:"processes"` // collect all processes by default - Cgroups bool `config:"cgroups"` + Procs []string `config:"processes"` // collect all processes by default + Cgroups bool `config:"cgroups"` + EnvWhitelist []string `config:"process.env.whitelist"` }{ Procs: []string{".*"}, Cgroups: false, } - if err := base.Module().UnpackConfig(&config); err != nil { return nil, err } @@ -48,7 +48,8 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) { m := &MetricSet{ BaseMetricSet: base, stats: &ProcStats{ - Procs: config.Procs, + Procs: config.Procs, + EnvWhitelist: config.EnvWhitelist, }, } err := m.stats.InitProcStats() diff --git a/metricbeat/module/system/process/process_test.go b/metricbeat/module/system/process/process_test.go index d8e665efc21..432d9d95f6e 100644 --- a/metricbeat/module/system/process/process_test.go +++ b/metricbeat/module/system/process/process_test.go @@ -4,7 +4,6 @@ package process import ( "testing" - "time" mbtest "github.com/elastic/beats/metricbeat/mb/testing" diff --git a/metricbeat/tests/system/test_system.py b/metricbeat/tests/system/test_system.py index 25400d74044..d0f8856c1de 100644 --- a/metricbeat/tests/system/test_system.py +++ b/metricbeat/tests/system/test_system.py @@ -327,7 +327,10 @@ def test_process(self): self.render_config_template(modules=[{ "name": "system", "metricsets": ["process"], - "period": "5s" + "period": "5s", + "extras": { + "process.env.whitelist": ["PATH"] + } }]) proc = self.start_beat() self.wait_until(lambda: self.output_lines() > 0) @@ -341,16 +344,26 @@ def test_process(self): self.assertGreater(len(output), 0) found_cmdline = False + found_env = False found_fd = False for evt in output: - self.assert_fields_are_documented(evt) process = evt["system"]["process"] + + # Remove 'env' prior to checking documented fields because its keys are dynamic. + env = process.pop("env", None) + if env is not None: + found_env = True + + self.assert_fields_are_documented(evt) + + # Remove optional keys. cmdline = process.pop("cmdline", None) if cmdline is not None: found_cmdline = True fd = process.pop("fd", None) if fd is not None: found_fd = True + self.assertItemsEqual(SYSTEM_PROCESS_FIELDS, process.keys()) self.assertTrue(found_cmdline, "cmdline not found in any process events") @@ -358,6 +371,10 @@ def test_process(self): if sys.platform.startswith("linux") or sys.platform.startswith("freebsd"): self.assertTrue(found_fd, "fd not found in any process events") + if sys.platform.startswith("linux") or sys.platform.startswith("freebsd")\ + or sys.platform.startswith("darwin"): + self.assertTrue(found_env, "env not found in any process events") + @unittest.skipUnless(re.match("(?i)win|linux|darwin|freebsd", sys.platform), "os") def test_process_metricbeat(self): """ diff --git a/vendor/github.com/elastic/gosigar/CHANGELOG.md b/vendor/github.com/elastic/gosigar/CHANGELOG.md index 3cc18f0cd86..65b58896265 100644 --- a/vendor/github.com/elastic/gosigar/CHANGELOG.md +++ b/vendor/github.com/elastic/gosigar/CHANGELOG.md @@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Added support to `github.com/gosigar/sys/windows` for querying and enabling privileges in a process token. - Added utility code for interfacing with linux NETLINK_INET_DIAG. #60 +- Added `ProcEnv` for getting a process's environment variables. #61 ### Changed - Changed several `OpenProcess` calls on Windows to request the lowest possible diff --git a/vendor/github.com/elastic/gosigar/README.md b/vendor/github.com/elastic/gosigar/README.md index c701aad71d0..2482620a834 100644 --- a/vendor/github.com/elastic/gosigar/README.md +++ b/vendor/github.com/elastic/gosigar/README.md @@ -29,6 +29,7 @@ The features vary by operating system. | LoadAverage | X | X | | X | X | | Mem | X | X | X | X | X | | ProcArgs | X | X | X | | X | +| ProcEnv | X | X | | | X | | ProcExe | X | X | | | X | | ProcFDUsage | X | | | | X | | ProcList | X | X | X | | X | diff --git a/vendor/github.com/elastic/gosigar/sigar_darwin.go b/vendor/github.com/elastic/gosigar/sigar_darwin.go index 3a9921cf61c..d90c15eebc2 100644 --- a/vendor/github.com/elastic/gosigar/sigar_darwin.go +++ b/vendor/github.com/elastic/gosigar/sigar_darwin.go @@ -326,6 +326,18 @@ func (self *ProcArgs) Get(pid int) error { return err } +func (self *ProcEnv) Get(pid int) error { + if self.Vars == nil { + self.Vars = map[string]string{} + } + + env := func(k, v string) { + self.Vars[k] = v + } + + return kern_procargs(pid, nil, nil, env) +} + func (self *ProcExe) Get(pid int) error { exe := func(arg string) { self.Name = arg diff --git a/vendor/github.com/elastic/gosigar/sigar_interface.go b/vendor/github.com/elastic/gosigar/sigar_interface.go index 9f0e4d77127..6ab5afb37ba 100644 --- a/vendor/github.com/elastic/gosigar/sigar_interface.go +++ b/vendor/github.com/elastic/gosigar/sigar_interface.go @@ -160,6 +160,10 @@ type ProcArgs struct { List []string } +type ProcEnv struct { + Vars map[string]string +} + type ProcExe struct { Name string Cwd string diff --git a/vendor/github.com/elastic/gosigar/sigar_linux_common.go b/vendor/github.com/elastic/gosigar/sigar_linux_common.go index 24b096aebd2..7753a7e7904 100644 --- a/vendor/github.com/elastic/gosigar/sigar_linux_common.go +++ b/vendor/github.com/elastic/gosigar/sigar_linux_common.go @@ -305,6 +305,34 @@ func (self *ProcArgs) Get(pid int) error { return nil } +func (self *ProcEnv) Get(pid int) error { + contents, err := readProcFile(pid, "environ") + if err != nil { + return err + } + + if self.Vars == nil { + self.Vars = map[string]string{} + } + + pairs := bytes.Split(contents, []byte{0}) + for _, kv := range pairs { + parts := bytes.SplitN(kv, []byte{'='}, 2) + if len(parts) != 2 { + continue + } + + key := string(bytes.TrimSpace(parts[0])) + if key == "" { + continue + } + + self.Vars[key] = string(bytes.TrimSpace(parts[1])) + } + + return nil +} + func (self *ProcExe) Get(pid int) error { fields := map[string]*string{ "exe": &self.Name, diff --git a/vendor/github.com/elastic/gosigar/sigar_openbsd.go b/vendor/github.com/elastic/gosigar/sigar_openbsd.go index 68cbb61c750..847b6dfc678 100644 --- a/vendor/github.com/elastic/gosigar/sigar_openbsd.go +++ b/vendor/github.com/elastic/gosigar/sigar_openbsd.go @@ -357,6 +357,10 @@ func (self *ProcArgs) Get(pid int) error { return nil } +func (self *ProcEnv) Get(pid int) error { + return ErrNotImplemented{runtime.GOOS} +} + func (self *ProcState) Get(pid int) error { return nil } diff --git a/vendor/github.com/elastic/gosigar/sigar_windows.go b/vendor/github.com/elastic/gosigar/sigar_windows.go index d1d40bd52b0..c5f665d920d 100644 --- a/vendor/github.com/elastic/gosigar/sigar_windows.go +++ b/vendor/github.com/elastic/gosigar/sigar_windows.go @@ -67,6 +67,10 @@ func (self *FDUsage) Get() error { return ErrNotImplemented{runtime.GOOS} } +func (self *ProcEnv) Get(pid int) error { + return ErrNotImplemented{runtime.GOOS} +} + func (self *ProcExe) Get(pid int) error { return ErrNotImplemented{runtime.GOOS} }