github.com/mongodb/grip@v0.0.0-20240213223901-f906268d82b9/message/system_info.go (about) 1 package message 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "runtime" 7 8 "github.com/mongodb/grip/level" 9 "github.com/shirou/gopsutil/v3/cpu" 10 "github.com/shirou/gopsutil/v3/disk" 11 "github.com/shirou/gopsutil/v3/mem" 12 "github.com/shirou/gopsutil/v3/net" 13 ) 14 15 // SystemInfo is a type that implements message.Composer but also 16 // collects system-wide resource utilization statistics about memory, 17 // CPU, and network use, along with an optional message. 18 type SystemInfo struct { 19 Message string `json:"message" bson:"message"` 20 CPU StatCPUTimes `json:"cpu" bson:"cpu"` 21 CPUPercent float64 `json:"cpu_percent" bson:"cpu_percent"` 22 NumCPU int `json:"num_cpus" bson:"num_cpus"` 23 VMStat mem.VirtualMemoryStat `json:"vmstat" bson:"vmstat"` 24 NetStat net.IOCountersStat `json:"netstat" bson:"netstat"` 25 Partitions []disk.PartitionStat `json:"partitions" bson:"partitions"` 26 Usage []disk.UsageStat `json:"usage" bson:"usage"` 27 IOStat []disk.IOCountersStat `json:"iostat" bson:"iostat"` 28 Errors []string `json:"errors" bson:"errors"` 29 Base `json:"metadata,omitempty" bson:"metadata,omitempty"` 30 loggable bool 31 rendered string 32 } 33 34 // StatCPUTimes provides a mirror of gopsutil/cpu.TimesStat with 35 // integers rather than floats. 36 type StatCPUTimes struct { 37 User int64 `json:"user" bson:"user"` 38 System int64 `json:"system" bson:"system"` 39 Idle int64 `json:"idle" bson:"idle"` 40 Nice int64 `json:"nice" bson:"nice"` 41 Iowait int64 `json:"iowait" bson:"iowait"` 42 Irq int64 `json:"irq" bson:"irq"` 43 Softirq int64 `json:"softirq" bson:"softirq"` 44 Steal int64 `json:"steal" bson:"steal"` 45 Guest int64 `json:"guest" bson:"guest"` 46 GuestNice int64 `json:"guestNice" bson:"guestNice"` 47 } 48 49 func convertCPUTimes(in cpu.TimesStat) StatCPUTimes { 50 return StatCPUTimes{ 51 User: int64(in.User * cpuTicks), 52 System: int64(in.System * cpuTicks), 53 Idle: int64(in.Idle * cpuTicks), 54 Nice: int64(in.Nice * cpuTicks), 55 Iowait: int64(in.Iowait * cpuTicks), 56 Irq: int64(in.Irq * cpuTicks), 57 Softirq: int64(in.Softirq * cpuTicks), 58 Steal: int64(in.Steal * cpuTicks), 59 Guest: int64(in.Guest * cpuTicks), 60 GuestNice: int64(in.GuestNice * cpuTicks), 61 } 62 } 63 64 // CollectSystemInfo returns a populated SystemInfo object, 65 // without a message. 66 func CollectSystemInfo() Composer { 67 return NewSystemInfo(level.Trace, "") 68 } 69 70 // MakeSystemInfo builds a populated SystemInfo object with the 71 // specified message. 72 func MakeSystemInfo(message string) Composer { 73 return NewSystemInfo(level.Info, message) 74 } 75 76 // NewSystemInfo returns a fully configured and populated SystemInfo 77 // object. 78 func NewSystemInfo(priority level.Priority, message string) Composer { 79 var err error 80 s := &SystemInfo{ 81 Message: message, 82 NumCPU: runtime.NumCPU(), 83 } 84 85 if err = s.SetPriority(priority); err != nil { 86 s.Errors = append(s.Errors, err.Error()) 87 return s 88 } 89 90 s.loggable = true 91 92 times, err := cpu.Times(false) 93 s.saveError("cpu_times", err) 94 if err == nil && len(times) > 0 { 95 // since we're not storing per-core information, 96 // there's only one thing we care about in this struct 97 s.CPU = convertCPUTimes(times[0]) 98 } 99 percent, err := cpu.Percent(0, false) 100 if err != nil { 101 s.saveError("cpu_times", err) 102 } else { 103 s.CPUPercent = percent[0] 104 } 105 106 vmstat, err := mem.VirtualMemory() 107 s.saveError("vmstat", err) 108 if err == nil && vmstat != nil { 109 s.VMStat = *vmstat 110 s.VMStat.UsedPercent = 0.0 111 } 112 113 netstat, err := net.IOCounters(false) 114 s.saveError("netstat", err) 115 if err == nil && len(netstat) > 0 { 116 s.NetStat = netstat[0] 117 } 118 119 partitions, err := disk.Partitions(true) 120 s.saveError("disk_part", err) 121 122 if err == nil { 123 var u *disk.UsageStat 124 for _, p := range partitions { 125 u, err = disk.Usage(p.Mountpoint) 126 s.saveError("partition", err) 127 if err != nil { 128 continue 129 } 130 u.UsedPercent = 0.0 131 u.InodesUsedPercent = 0.0 132 133 s.Usage = append(s.Usage, *u) 134 } 135 136 s.Partitions = partitions 137 } 138 139 iostatMap, err := disk.IOCounters() 140 s.saveError("iostat", err) 141 for _, stat := range iostatMap { 142 s.IOStat = append(s.IOStat, stat) 143 } 144 145 return s 146 } 147 148 // Loggable returns true when the Processinfo structure has been 149 // populated. 150 func (s *SystemInfo) Loggable() bool { return s.loggable } 151 152 // Raw always returns the SystemInfo object. 153 func (s *SystemInfo) Raw() interface{} { return s } 154 155 // String returns a string representation of the message, lazily 156 // rendering the message, and caching it privately. 157 func (s *SystemInfo) String() string { 158 if s.rendered == "" { 159 s.rendered = renderStatsString(s.Message, s) 160 } 161 162 return s.rendered 163 } 164 165 func (s *SystemInfo) saveError(stat string, err error) { 166 if shouldSaveError(err) { 167 s.Errors = append(s.Errors, fmt.Sprintf("%s: %v", stat, err)) 168 } 169 } 170 171 // helper function 172 func shouldSaveError(err error) bool { 173 return err != nil && err.Error() != "not implemented yet" 174 } 175 176 func renderStatsString(msg string, data interface{}) string { 177 out, err := json.Marshal(data) 178 if err != nil { 179 return msg 180 } 181 182 if msg == "" { 183 return string(out) 184 } 185 186 return fmt.Sprintf("%s:\n%s", msg, string(out)) 187 }