package main // Host/system sampling for the system-status page — mirrors // lib/sysstat.js. Memory-only ring history (240 samples), ~30s minimum // sampling interval, "today" traffic baseline kept in memory (a panel // restart restarts the day counter from traffic seen since boot). import ( "net" "os" "runtime" "strconv" "strings" "sync" "syscall" "time" ) const historyCap = 240 const sampleMinIntervalMs = 25000 type sysSample struct { At string `json:"at"` CPU *float64 `json:"cpu"` Mem *float64 `json:"mem"` UpBps float64 `json:"upBps"` DownBps float64 `json:"downBps"` Connections int `json:"connections"` } type sysCurrent struct { CPUPct *float64 MemTotal int64 MemUsed int64 DiskTotal int64 DiskUsed int64 UpBps float64 DownBps float64 Conns int TotalUp int64 TotalDown int64 } type SystemMonitor struct { mu sync.Mutex dataDir string startedAt time.Time prevIdle int64 prevTotal int64 hasPrev bool cpuPct *float64 history []sysSample lastSample time.Time dayKey string dayBaseUp int64 dayBaseDown int64 todayUp int64 todayDown int64 current *sysCurrent } func NewSystemMonitor(dataDir string) *SystemMonitor { return &SystemMonitor{dataDir: dataDir, startedAt: time.Now()} } func readCPUTimes() (idle, total int64, ok bool) { raw, err := os.ReadFile("/proc/stat") if err != nil { return 0, 0, false } line := strings.SplitN(string(raw), "\n", 2)[0] fields := strings.Fields(line) if len(fields) < 6 { return 0, 0, false } var values []int64 for _, f := range fields[1:] { n, err := strconv.ParseInt(f, 10, 64) if err != nil { return 0, 0, false } values = append(values, n) } idle = values[3] if len(values) > 4 { idle += values[4] } for _, v := range values { total += v } return idle, total, true } func readMemInfo() (total, used int64) { raw, err := os.ReadFile("/proc/meminfo") if err != nil { return 0, 0 } grab := func(key string) int64 { for _, line := range strings.Split(string(raw), "\n") { if strings.HasPrefix(line, key+":") { fields := strings.Fields(line) if len(fields) >= 2 { if n, err := strconv.ParseInt(fields[1], 10, 64); err == nil { return n * 1024 } } } } return 0 } total = grab("MemTotal") available := grab("MemAvailable") if total > 0 { used = total - available } return total, used } func isGlobalIPv4(ip net.IP) bool { v4 := ip.To4() if v4 == nil { return false } a, b := v4[0], v4[1] switch { case a == 0 || a == 10 || a == 127: return false case a == 172 && b >= 16 && b <= 31: return false case a == 192 && b == 168: return false case a == 100 && b >= 64 && b <= 127: return false case a == 169 && b == 254: return false case a >= 224: return false } return true } func isGlobalIPv6(ip net.IP) bool { if ip.To4() != nil { return false } if ip.IsLoopback() || ip.IsUnspecified() || ip.IsMulticast() { return false } b := ip.To16() if b == nil { return false } if b[0] == 0xfc || b[0] == 0xfd { // ULA fc00::/7 return false } if b[0] == 0xfe && b[1]&0xc0 == 0x80 { // link-local fe80::/10 return false } return true } // HostIPs picks the host's global interface addresses (no external // lookup of any kind), mirroring hostIPs in lib/sysstat.js. func HostIPs() (ipv4, ipv6 *string) { ifaces, err := net.Interfaces() if err != nil { return nil, nil } for _, iface := range ifaces { if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 { continue } addrs, err := iface.Addrs() if err != nil { continue } for _, addr := range addrs { var ip net.IP switch a := addr.(type) { case *net.IPNet: ip = a.IP case *net.IPAddr: ip = a.IP } if ip == nil { continue } if ipv4 == nil && isGlobalIPv4(ip) { s := ip.String() ipv4 = &s } if ipv6 == nil && isGlobalIPv6(ip) { s := ip.String() ipv6 = &s } } } return ipv4, ipv6 } func round1(v float64) float64 { return float64(int64(v*10+0.5)) / 10 } func (m *SystemMonitor) Sample(d *Data, force bool) *sysCurrent { m.mu.Lock() defer m.mu.Unlock() now := time.Now() if !force && m.current != nil && now.Sub(m.lastSample).Milliseconds() < sampleMinIntervalMs { return m.current } m.lastSample = now if idle, total, ok := readCPUTimes(); ok { if m.hasPrev { totalDelta := total - m.prevTotal idleDelta := idle - m.prevIdle if totalDelta > 0 { pct := (1 - float64(idleDelta)/float64(totalDelta)) * 100 if pct < 0 { pct = 0 } if pct > 100 { pct = 100 } m.cpuPct = &pct } } m.prevIdle, m.prevTotal, m.hasPrev = idle, total, true } memTotal, memUsed := readMemInfo() var diskTotal, diskUsed int64 var statfs syscall.Statfs_t if err := syscall.Statfs(m.dataDir, &statfs); err == nil { diskTotal = int64(statfs.Blocks) * int64(statfs.Bsize) diskUsed = (int64(statfs.Blocks) - int64(statfs.Bfree)) * int64(statfs.Bsize) } var totalUp, totalDown int64 var upBps, downBps float64 nowMs := now.UnixMilli() for _, node := range d.Nodes { if node.MeteredTraffic != nil { totalUp += node.MeteredTraffic.Uplink totalDown += node.MeteredTraffic.Downlink } up, down := NodeSpeedBps(node, nowMs) upBps += up downBps += down } dayKey := now.Format("2006-01-02") if m.dayKey != dayKey { m.dayKey = dayKey m.dayBaseUp = totalUp m.dayBaseDown = totalDown } m.todayUp = totalUp - m.dayBaseUp if m.todayUp < 0 { m.todayUp = 0 } m.todayDown = totalDown - m.dayBaseDown if m.todayDown < 0 { m.todayDown = 0 } conns := CountOnlineCustomers(d, now) var cpuVal, memVal *float64 if m.cpuPct != nil { v := round1(*m.cpuPct) cpuVal = &v } if memTotal > 0 { v := round1(float64(memUsed) / float64(memTotal) * 100) memVal = &v } m.history = append(m.history, sysSample{ At: ISOFromTime(now), CPU: cpuVal, Mem: memVal, UpBps: upBps, DownBps: downBps, Connections: conns, }) if len(m.history) > historyCap { m.history = m.history[len(m.history)-historyCap:] } m.current = &sysCurrent{ CPUPct: cpuVal, MemTotal: memTotal, MemUsed: memUsed, DiskTotal: diskTotal, DiskUsed: diskUsed, UpBps: upBps, DownBps: downBps, Conns: conns, TotalUp: totalUp, TotalDown: totalDown, } return m.current } func panelMemBytes() int64 { raw, err := os.ReadFile("/proc/self/status") if err != nil { return 0 } for _, line := range strings.Split(string(raw), "\n") { if strings.HasPrefix(line, "VmRSS:") { fields := strings.Fields(line) if len(fields) >= 2 { if n, err := strconv.ParseInt(fields[1], 10, 64); err == nil { return n * 1024 } } } } return 0 } func hostUptimeSec() int64 { raw, err := os.ReadFile("/proc/uptime") if err != nil { return 0 } fields := strings.Fields(string(raw)) if len(fields) == 0 { return 0 } if v, err := strconv.ParseFloat(fields[0], 64); err == nil { return int64(v) } return 0 } func (m *SystemMonitor) Status(d *Data, version string) map[string]any { current := m.Sample(d, false) m.mu.Lock() history := make([]sysSample, len(m.history)) copy(history, m.history) todayUp, todayDown := m.todayUp, m.todayDown startedAt := m.startedAt m.mu.Unlock() if current == nil { current = &sysCurrent{} } memTotal, memUsed := current.MemTotal, current.MemUsed var memPct, diskPct *float64 if memTotal > 0 { v := round1(float64(memUsed) / float64(memTotal) * 100) memPct = &v } if current.DiskTotal > 0 { v := round1(float64(current.DiskUsed) / float64(current.DiskTotal) * 100) diskPct = &v } onlineNodes := 0 for _, node := range d.Nodes { if node.Status == "active" { onlineNodes++ } } ipv4, ipv6 := HostIPs() load1 := float64(0) // load average from /proc/loadavg if raw, err := os.ReadFile("/proc/loadavg"); err == nil { fields := strings.Fields(string(raw)) if len(fields) > 0 { load1, _ = strconv.ParseFloat(fields[0], 64) } } return map[string]any{ "version": version, "startedAt": ISOFromTime(startedAt), "panelUptimeSec": int64(time.Since(startedAt).Seconds()), "hostUptimeSec": hostUptimeSec(), "panelMemBytes": panelMemBytes(), "host": map[string]any{ "cpuPercent": current.CPUPct, "cpuCount": runtime.NumCPU(), "load1": load1, "memTotal": memTotal, "memUsed": memUsed, "memPercent": memPct, "diskTotal": current.DiskTotal, "diskUsed": current.DiskUsed, "diskPercent": diskPct, }, "network": map[string]any{ "upBps": current.UpBps, "downBps": current.DownBps, "connections": current.Conns, "onlineCustomers": current.Conns, "onlineNodes": onlineNodes, "nodesTotal": len(d.Nodes), "customersTotal": len(d.Customers), }, "today": map[string]any{"upBytes": todayUp, "downBytes": todayDown}, "totals": map[string]any{"upBytes": current.TotalUp, "downBytes": current.TotalDown}, "ips": map[string]any{"ipv4": ipv4, "ipv6": ipv6}, "history": history, } }