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  }