383 lines
8.8 KiB
Go
383 lines
8.8 KiB
Go
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,
|
|
}
|
|
}
|