...
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
17 var prevCPULoadTimeStatsLock sync.RWMutex
18 var prevCPULoadTimeStats cpu.TimesStat
19 var prevCPULoadTimestamp time.Time
20
21 func init() {
22
23 if probe, err := cpu.Times(false); err == nil {
24 prevCPULoadTimeStats = probe[0]
25 prevCPULoadTimestamp = time.Now()
26 }
27 }
28
29
30
31
32
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
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()
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 {
118 readBytes += float64(v.ReadBytes)
119
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) {
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