github.com/mongodb/grip@v0.0.0-20240213223901-f906268d82b9/message/process_info.go (about)

     1  package message
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"sync"
     7  
     8  	"github.com/mongodb/grip/level"
     9  	"github.com/shirou/gopsutil/v3/net"
    10  	"github.com/shirou/gopsutil/v3/process"
    11  )
    12  
    13  // ProcessInfo holds the data for per-process statistics (e.g. cpu,
    14  // memory, io). The Process info composers produce messages in this
    15  // form.
    16  type ProcessInfo struct {
    17  	Message        string                   `json:"message" bson:"message"`
    18  	Pid            int32                    `json:"pid" bson:"pid"`
    19  	Parent         int32                    `json:"parentPid" bson:"parentPid"`
    20  	Threads        int                      `json:"numThreads" bson:"numThreads"`
    21  	Command        string                   `json:"command" bson:"command"`
    22  	CPU            StatCPUTimes             `json:"cpu" bson:"cpu"`
    23  	IoStat         process.IOCountersStat   `json:"io" bson:"io"`
    24  	NetStat        []net.IOCountersStat     `json:"net" bson:"net"`
    25  	Memory         process.MemoryInfoStat   `json:"mem" bson:"mem"`
    26  	MemoryPlatform process.MemoryInfoExStat `json:"memExtra" bson:"memExtra"`
    27  	Errors         []string                 `json:"errors" bson:"errors"`
    28  	Base           `json:"metadata,omitempty" bson:"metadata,omitempty"`
    29  	loggable       bool
    30  	rendered       string
    31  }
    32  
    33  ///////////////////////////////////////////////////////////////////////////
    34  //
    35  // Constructors
    36  //
    37  ///////////////////////////////////////////////////////////////////////////
    38  
    39  // CollectProcessInfo returns a populated ProcessInfo message.Composer
    40  // instance for the specified pid.
    41  func CollectProcessInfo(pid int32) Composer {
    42  	return NewProcessInfo(level.Trace, pid, "")
    43  }
    44  
    45  // CollectProcessInfoSelf returns a populated ProcessInfo message.Composer
    46  // for the pid of the current process.
    47  func CollectProcessInfoSelf() Composer {
    48  	return NewProcessInfo(level.Trace, int32(os.Getpid()), "")
    49  }
    50  
    51  // CollectProcessInfoSelfWithChildren returns a slice of populated
    52  // ProcessInfo message.Composer instances for the current process and
    53  // all children processes.
    54  func CollectProcessInfoSelfWithChildren() []Composer {
    55  	return CollectProcessInfoWithChildren(int32(os.Getpid()))
    56  }
    57  
    58  // CollectProcessInfoWithChildren returns a slice of populated
    59  // ProcessInfo message.Composer instances for the process with the
    60  // specified pid and all children processes for that process.
    61  func CollectProcessInfoWithChildren(pid int32) []Composer {
    62  	var results []Composer
    63  	parent, err := process.NewProcess(pid)
    64  	if err != nil {
    65  		return results
    66  	}
    67  
    68  	parentMsg := &ProcessInfo{}
    69  	parentMsg.loggable = true
    70  	parentMsg.populate(parent)
    71  	results = append(results, parentMsg)
    72  
    73  	for _, child := range getChildrenRecursively(parent) {
    74  		cm := &ProcessInfo{}
    75  		cm.loggable = true
    76  		cm.populate(child)
    77  		results = append(results, cm)
    78  	}
    79  
    80  	return results
    81  }
    82  
    83  // CollectAllProcesses returns a slice of populated ProcessInfo
    84  // message.Composer interfaces for all processes currently running on
    85  // a system.
    86  func CollectAllProcesses() []Composer {
    87  	numThreads := 32
    88  	procs, err := process.Processes()
    89  	if err != nil {
    90  		return []Composer{}
    91  	}
    92  	if len(procs) < numThreads {
    93  		numThreads = len(procs)
    94  	}
    95  
    96  	results := []Composer{}
    97  	procChan := make(chan *process.Process, len(procs))
    98  	for _, p := range procs {
    99  		procChan <- p
   100  	}
   101  	close(procChan)
   102  	wg := sync.WaitGroup{}
   103  	wg.Add(numThreads)
   104  	infoChan := make(chan *ProcessInfo, len(procs))
   105  	for i := 0; i < numThreads; i++ {
   106  		go func() {
   107  			defer wg.Done()
   108  			for p := range procChan {
   109  				cm := &ProcessInfo{}
   110  				cm.loggable = true
   111  				cm.populate(p)
   112  				infoChan <- cm
   113  			}
   114  		}()
   115  	}
   116  	wg.Wait()
   117  	close(infoChan)
   118  	for p := range infoChan {
   119  		results = append(results, p)
   120  	}
   121  
   122  	return results
   123  }
   124  
   125  func getChildrenRecursively(proc *process.Process) []*process.Process {
   126  	var out []*process.Process
   127  
   128  	children, err := proc.Children()
   129  	if len(children) == 0 || err != nil {
   130  		return out
   131  	}
   132  
   133  	for _, p := range children {
   134  		out = append(out, p)
   135  		out = append(out, getChildrenRecursively(p)...)
   136  	}
   137  
   138  	return out
   139  }
   140  
   141  // NewProcessInfo constructs a fully configured and populated
   142  // Processinfo message.Composer instance for the specified process.
   143  func NewProcessInfo(priority level.Priority, pid int32, message string) Composer {
   144  	p := &ProcessInfo{
   145  		Message: message,
   146  		Pid:     pid,
   147  	}
   148  
   149  	if err := p.SetPriority(priority); err != nil {
   150  		p.saveError("priority", err)
   151  		return p
   152  	}
   153  
   154  	proc, err := process.NewProcess(pid)
   155  	p.saveError("process", err)
   156  	if err != nil {
   157  		return p
   158  	}
   159  
   160  	p.loggable = true
   161  	p.populate(proc)
   162  
   163  	return p
   164  }
   165  
   166  ///////////////////////////////////////////////////////////////////////////
   167  //
   168  // message.Composer implementation
   169  //
   170  ///////////////////////////////////////////////////////////////////////////
   171  
   172  // Loggable returns true when the Processinfo structure has been
   173  // populated.
   174  func (p *ProcessInfo) Loggable() bool { return p.loggable }
   175  
   176  // Raw always returns the ProcessInfo object, however it will call the
   177  // Collect method of the base operation first.
   178  func (p *ProcessInfo) Raw() interface{} { _ = p.Collect(true); return p }
   179  
   180  // String returns a string representation of the message, lazily
   181  // rendering the message, and caching it privately.
   182  func (p *ProcessInfo) String() string {
   183  	if p.rendered == "" {
   184  		p.rendered = renderStatsString(p.Message, p)
   185  	}
   186  
   187  	return p.rendered
   188  }
   189  
   190  ///////////////////////////////////////////////////////////////////////////
   191  //
   192  // Internal Methods for collecting data
   193  //
   194  ///////////////////////////////////////////////////////////////////////////
   195  
   196  func (p *ProcessInfo) populate(proc *process.Process) {
   197  	var err error
   198  
   199  	if p.Pid == 0 {
   200  		p.Pid = proc.Pid
   201  	}
   202  	parentPid, err := proc.Ppid()
   203  	p.saveError("parent_pid", err)
   204  	if err == nil {
   205  		p.Parent = parentPid
   206  	}
   207  
   208  	memInfo, err := proc.MemoryInfo()
   209  	p.saveError("meminfo", err)
   210  	if err == nil && memInfo != nil {
   211  		p.Memory = *memInfo
   212  	}
   213  
   214  	memInfoEx, err := proc.MemoryInfoEx()
   215  	p.saveError("meminfo_extended", err)
   216  	if err == nil && memInfoEx != nil {
   217  		p.MemoryPlatform = *memInfoEx
   218  	}
   219  
   220  	threads, err := proc.NumThreads()
   221  	p.Threads = int(threads)
   222  	p.saveError("num_threads", err)
   223  
   224  	// TODO: should this be removed entirely? process.NetIOCounters is not
   225  	// useful on Linux and is equivalent to the system-wide net.IOCounters,
   226  	// which is not per-process.
   227  	// Issue: https://github.com/shirou/gopsutil/issues/429
   228  	p.NetStat, err = net.IOCounters(false)
   229  	p.saveError("netstat", err)
   230  
   231  	p.Command, err = proc.Cmdline()
   232  	p.saveError("cmd args", err)
   233  
   234  	cpuTimes, err := proc.Times()
   235  	p.saveError("cpu_times", err)
   236  	if err == nil && cpuTimes != nil {
   237  		p.CPU = convertCPUTimes(*cpuTimes)
   238  	}
   239  
   240  	ioStat, err := proc.IOCounters()
   241  	p.saveError("iostat", err)
   242  	if err == nil && ioStat != nil {
   243  		p.IoStat = *ioStat
   244  	}
   245  }
   246  
   247  func (p *ProcessInfo) saveError(stat string, err error) {
   248  	if shouldSaveError(err) {
   249  		p.Errors = append(p.Errors, fmt.Sprintf("%s: %v", stat, err))
   250  	}
   251  }