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 }