package widgets import ( "fmt" "log" "sort" "strings" "time" "github.com/prometheus/client_golang/prometheus" psDisk "github.com/shirou/gopsutil/disk" ui "github.com/xxxserxxx/gotop/v3/termui" "github.com/xxxserxxx/gotop/v3/utils" ) type Partition struct { Device string MountPoint string BytesRead uint64 BytesWritten uint64 BytesReadRecently string BytesWrittenRecently string UsedPercent uint32 Free string } type DiskWidget struct { *ui.Table updateInterval time.Duration Partitions map[string]*Partition metric map[string]prometheus.Gauge } // TODO: Add filtering // TODO: Abstract out device from widget func NewDiskWidget() *DiskWidget { self := &DiskWidget{ Table: ui.NewTable(), updateInterval: time.Second, Partitions: make(map[string]*Partition), } self.Title = " Disk Usage " self.Header = []string{"Disk", "Mount", "Used", "Free", "R/s", "W/s"} self.ColGap = 2 self.ColResizer = func() { self.ColWidths = []int{ utils.MaxInt(4, (self.Inner.Dx()-29)/2), utils.MaxInt(5, (self.Inner.Dx()-29)/2), 4, 5, 5, 5, } } self.update() go func() { for range time.NewTicker(self.updateInterval).C { self.Lock() self.update() self.Unlock() } }() return self } func (self *DiskWidget) EnableMetric() { self.metric = make(map[string]prometheus.Gauge) for key, part := range self.Partitions { gauge := prometheus.NewGauge(prometheus.GaugeOpts{ Namespace: "gotop", Subsystem: "disk", Name: strings.ReplaceAll(key, "/", ":"), //Name: strings.Replace(strings.Replace(part.Device, "/dev/", "", -1), "mapper/", "", -1), }) gauge.Set(float64(part.UsedPercent) / 100.0) prometheus.MustRegister(gauge) self.metric[key] = gauge } } func (self *DiskWidget) update() { partitions, err := psDisk.Partitions(false) if err != nil { log.Printf("failed to get disk partitions from gopsutil: %v", err) return } // add partition if it's new for _, partition := range partitions { // don't show loop devices if strings.HasPrefix(partition.Device, "/dev/loop") { continue } // don't show docker container filesystems if strings.HasPrefix(partition.Mountpoint, "/var/lib/docker/") { continue } // check if partition doesn't already exist in our list if _, ok := self.Partitions[partition.Device]; !ok { self.Partitions[partition.Device] = &Partition{ Device: partition.Device, MountPoint: partition.Mountpoint, } } } // delete a partition if it no longer exists toDelete := []string{} for device := range self.Partitions { exists := false for _, partition := range partitions { if device == partition.Device { exists = true break } } if !exists { toDelete = append(toDelete, device) } } for _, device := range toDelete { delete(self.Partitions, device) } // updates partition info. We add 0.5 to all values to make sure the truncation rounds for _, partition := range self.Partitions { usage, err := psDisk.Usage(partition.MountPoint) if err != nil { log.Printf("failed to get partition usage statistics from gopsutil: %v. partition: %v", err, partition) continue } partition.UsedPercent = uint32(usage.UsedPercent + 0.5) bytesFree, magnitudeFree := utils.ConvertBytes(usage.Free) partition.Free = fmt.Sprintf("%3d%s", uint64(bytesFree+0.5), magnitudeFree) ioCounters, err := psDisk.IOCounters(partition.Device) if err != nil { log.Printf("failed to get partition read/write info from gopsutil: %v. partition: %v", err, partition) continue } ioCounter := ioCounters[strings.Replace(partition.Device, "/dev/", "", -1)] bytesRead, bytesWritten := ioCounter.ReadBytes, ioCounter.WriteBytes if partition.BytesRead != 0 { // if this isn't the first update bytesReadRecently := bytesRead - partition.BytesRead bytesWrittenRecently := bytesWritten - partition.BytesWritten readFloat, readMagnitude := utils.ConvertBytes(bytesReadRecently) writeFloat, writeMagnitude := utils.ConvertBytes(bytesWrittenRecently) bytesReadRecently, bytesWrittenRecently = uint64(readFloat+0.5), uint64(writeFloat+0.5) partition.BytesReadRecently = fmt.Sprintf("%d%s", bytesReadRecently, readMagnitude) partition.BytesWrittenRecently = fmt.Sprintf("%d%s", bytesWrittenRecently, writeMagnitude) } else { partition.BytesReadRecently = fmt.Sprintf("%d%s", 0, "B") partition.BytesWrittenRecently = fmt.Sprintf("%d%s", 0, "B") } partition.BytesRead, partition.BytesWritten = bytesRead, bytesWritten } // converts self.Partitions into self.Rows which is a [][]String sortedPartitions := []string{} for seriesName := range self.Partitions { sortedPartitions = append(sortedPartitions, seriesName) } sort.Strings(sortedPartitions) self.Rows = make([][]string, len(self.Partitions)) for i, key := range sortedPartitions { partition := self.Partitions[key] self.Rows[i] = make([]string, 6) self.Rows[i][0] = strings.Replace(strings.Replace(partition.Device, "/dev/", "", -1), "mapper/", "", -1) self.Rows[i][1] = partition.MountPoint self.Rows[i][2] = fmt.Sprintf("%d%%", partition.UsedPercent) self.Rows[i][3] = partition.Free self.Rows[i][4] = partition.BytesReadRecently self.Rows[i][5] = partition.BytesWrittenRecently if self.metric != nil { if self.metric[key] == nil { log.Printf("ERROR: missing metric %s", key) } else { self.metric[key].Set(float64(partition.UsedPercent) / 100.0) } } } }