Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add environment variables to the system process metricset #3337

Merged
merged 2 commits into from
Jan 12, 2017
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Add environment variables to the system process metricset
This PR adds the environment variables that were used to start the process to the data reported in the system process metricset. The data is added as a dictionary under the `system.process.env` key. Environment variables must be whitelisted using an array of regular expressions specified using `process.env.whitelist: []` in the module config.

This feature implemented for FreeBSD, Linux, and OS X.
  • Loading branch information
andrewkroh committed Jan 12, 2017
commit 6e238466ff6aad856eb0925f671cfdea0f9428c1
4 changes: 4 additions & 0 deletions metricbeat/_meta/beat.full.yml
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions metricbeat/docs/fields.asciidoc
Original file line number Diff line number Diff line change
@@ -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

4 changes: 4 additions & 0 deletions metricbeat/metricbeat.full.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions metricbeat/module/system/_meta/config.full.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions metricbeat/module/system/process/_meta/fields.yml
Original file line number Diff line number Diff line change
@@ -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]"
114 changes: 94 additions & 20 deletions metricbeat/module/system/process/helper.go
Original file line number Diff line number Diff line change
@@ -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,26 +335,28 @@ 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
}

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 {
Loading