...

Source file src/github.com/cybertec-postgresql/pgwatch/v5/internal/reaper/psutil.go

Documentation: github.com/cybertec-postgresql/pgwatch/v5/internal/reaper

     1  package reaper
     2  
     3  import (
     4  	"math"
     5  	"os"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/cybertec-postgresql/pgwatch/v5/internal/metrics"
    10  	"github.com/shirou/gopsutil/v4/cpu"
    11  	"github.com/shirou/gopsutil/v4/disk"
    12  	"github.com/shirou/gopsutil/v4/load"
    13  	"github.com/shirou/gopsutil/v4/mem"
    14  )
    15  
    16  // "cache" of last CPU utilization stats for GetGoPsutilCPU to get more exact results and not having to sleep
    17  var prevCPULoadTimeStatsLock sync.RWMutex
    18  var prevCPULoadTimeStats cpu.TimesStat
    19  var prevCPULoadTimestamp time.Time
    20  
    21  func init() {
    22  	// initialize the cache with current stats so the first call returns a meaningful delta
    23  	if probe, err := cpu.Times(false); err == nil {
    24  		prevCPULoadTimeStats = probe[0]
    25  		prevCPULoadTimestamp = time.Now()
    26  	}
    27  }
    28  
    29  // cpuTotal returns the total number of seconds across all CPU states.
    30  // Guest and GuestNice are intentionally excluded because on Linux they are
    31  // already counted within User and Nice respectively (/proc/stat semantics),
    32  // so including them would double-count and skew percentage calculations.
    33  func cpuTotal(c cpu.TimesStat) float64 {
    34  	return c.User + c.System + c.Idle + c.Nice + c.Iowait + c.Irq +
    35  		c.Softirq + c.Steal
    36  }
    37  
    38  func goPsutilCalcCPUUtilization(probe0, probe1 cpu.TimesStat) float64 {
    39  	return 100 - (100.0 * (probe1.Idle - probe0.Idle + probe1.Iowait - probe0.Iowait + probe1.Steal - probe0.Steal) / (cpuTotal(probe1) - cpuTotal(probe0)))
    40  }
    41  
    42  // GetGoPsutilCPU simulates "psutil" metric output. Assumes the result from last call as input
    43  func GetGoPsutilCPU(interval time.Duration) (metrics.Measurements, error) {
    44  	prevCPULoadTimeStatsLock.RLock()
    45  	prevTime := prevCPULoadTimestamp
    46  	prevTimeStat := prevCPULoadTimeStats
    47  	prevCPULoadTimeStatsLock.RUnlock()
    48  
    49  	curCallStats, err := cpu.Times(false)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	if time.Since(prevTime) >= interval {
    54  		prevCPULoadTimeStatsLock.Lock() // update the cache
    55  		prevCPULoadTimeStats = curCallStats[0]
    56  		prevCPULoadTimestamp = time.Now()
    57  		prevCPULoadTimeStatsLock.Unlock()
    58  	}
    59  
    60  	la, err := load.Avg()
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	cpus, err := cpu.Counts(true)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  
    70  	retMap := metrics.NewMeasurement(time.Now().UnixNano())
    71  	retMap["cpu_utilization"] = math.Round(100*goPsutilCalcCPUUtilization(prevTimeStat, curCallStats[0])) / 100
    72  	retMap["load_1m_norm"] = math.Round(100*la.Load1/float64(cpus)) / 100
    73  	retMap["load_1m"] = math.Round(100*la.Load1) / 100
    74  	retMap["load_5m_norm"] = math.Round(100*la.Load5/float64(cpus)) / 100
    75  	retMap["load_5m"] = math.Round(100*la.Load5) / 100
    76  	totalDiff := cpuTotal(curCallStats[0]) - cpuTotal(prevTimeStat)
    77  	retMap["user"] = math.Round(10000.0*(curCallStats[0].User-prevTimeStat.User)/totalDiff) / 100
    78  	retMap["system"] = math.Round(10000.0*(curCallStats[0].System-prevTimeStat.System)/totalDiff) / 100
    79  	retMap["idle"] = math.Round(10000.0*(curCallStats[0].Idle-prevTimeStat.Idle)/totalDiff) / 100
    80  	retMap["iowait"] = math.Round(10000.0*(curCallStats[0].Iowait-prevTimeStat.Iowait)/totalDiff) / 100
    81  	retMap["irqs"] = math.Round(10000.0*(curCallStats[0].Irq-prevTimeStat.Irq+curCallStats[0].Softirq-prevTimeStat.Softirq)/totalDiff) / 100
    82  	retMap["other"] = math.Round(10000.0*(curCallStats[0].Steal-prevTimeStat.Steal+curCallStats[0].Guest-prevTimeStat.Guest+curCallStats[0].GuestNice-prevTimeStat.GuestNice)/totalDiff) / 100
    83  
    84  	return metrics.Measurements{retMap}, nil
    85  }
    86  
    87  func GetGoPsutilMem() (metrics.Measurements, error) {
    88  	vm, err := mem.VirtualMemory()
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	retMap := metrics.NewMeasurement(time.Now().UnixNano())
    94  	retMap["total"] = int64(vm.Total)
    95  	retMap["used"] = int64(vm.Used)
    96  	retMap["free"] = int64(vm.Free)
    97  	retMap["buff_cache"] = int64(vm.Buffers)
    98  	retMap["available"] = int64(vm.Available)
    99  	retMap["percent"] = math.Round(100*vm.UsedPercent) / 100
   100  	retMap["swap_total"] = int64(vm.SwapTotal)
   101  	retMap["swap_used"] = int64(vm.SwapCached)
   102  	retMap["swap_free"] = int64(vm.SwapFree)
   103  	retMap["swap_percent"] = math.Round(100*float64(vm.SwapCached)/float64(vm.SwapTotal)) / 100
   104  
   105  	return metrics.Measurements{retMap}, nil
   106  }
   107  
   108  func GetGoPsutilDiskTotals() (metrics.Measurements, error) {
   109  	d, err := disk.IOCounters()
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	retMap := metrics.NewMeasurement(time.Now().UnixNano())
   115  	var readBytes, writeBytes, reads, writes float64
   116  
   117  	for _, v := range d { // summarize all disk devices
   118  		readBytes += float64(v.ReadBytes) // datatype float is just an oversight in the original psutil helper
   119  		// but can't change it without causing problems on storage level (InfluxDB)
   120  		writeBytes += float64(v.WriteBytes)
   121  		reads += float64(v.ReadCount)
   122  		writes += float64(v.WriteCount)
   123  	}
   124  	retMap["read_bytes"] = readBytes
   125  	retMap["write_bytes"] = writeBytes
   126  	retMap["read_count"] = reads
   127  	retMap["write_count"] = writes
   128  
   129  	return metrics.Measurements{retMap}, nil
   130  }
   131  
   132  func GetLoadAvgLocal() (metrics.Measurements, error) {
   133  	la, err := load.Avg()
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	row := metrics.NewMeasurement(time.Now().UnixNano())
   139  	row["load_1min"] = la.Load1
   140  	row["load_5min"] = la.Load5
   141  	row["load_15min"] = la.Load15
   142  
   143  	return metrics.Measurements{row}, nil
   144  }
   145  
   146  func CheckFolderExistsAndReadable(path string) bool {
   147  	if path == "" {
   148  		return false
   149  	}
   150  	_, err := os.ReadDir(path)
   151  	return err == nil
   152  }
   153  
   154  func GetGoPsutilDiskPG(pgDirs metrics.Measurements) (metrics.Measurements, error) {
   155  	usageCache := make(map[uint64]*disk.UsageStat)
   156  	retRows := make(metrics.Measurements, 0)
   157  	epochNs := time.Now().UnixNano()
   158  
   159  	for _, row := range pgDirs {
   160  		path := row["path"].(string)
   161  		name := row["name"].(string)
   162  
   163  		if !CheckFolderExistsAndReadable(path) { // syslog etc considered out of scope
   164  			continue
   165  		}
   166  
   167  		devID, err := GetPathUnderlyingDeviceID(path)
   168  		if err != nil {
   169  			return nil, err
   170  		}
   171  
   172  		usage, ok := usageCache[devID]
   173  		if !ok {
   174  			usage, err = disk.Usage(path)
   175  			if err != nil {
   176  				return nil, err
   177  			}
   178  			usageCache[devID] = usage
   179  		}
   180  
   181  		m := metrics.NewMeasurement(epochNs)
   182  		m["tag_dir_or_tablespace"] = name
   183  		m["tag_path"] = path
   184  		m["total"] = float64(usage.Total)
   185  		m["used"] = float64(usage.Used)
   186  		m["free"] = float64(usage.Free)
   187  		m["percent"] = math.Round(100*usage.UsedPercent) / 100
   188  		retRows = append(retRows, m)
   189  	}
   190  
   191  	return retRows, nil
   192  }
   193