package sysinfo import ( "bufio" "fmt" "os" "runtime" "strconv" "strings" "sync" "syscall" "time" ) var netMu sync.Mutex var lastNet = struct { rx, tx uint64 at time.Time }{} var cpuMu sync.Mutex var lastCPU = struct { idle, total uint64 at time.Time usage int }{} type Profile struct { Role string `json:"role,omitempty"` CPUCores int `json:"cpu_cores"` CPUUsagePercent int `json:"cpu_usage_percent"` Load1M float64 `json:"load_1m"` Load5M float64 `json:"load_5m"` Load15M float64 `json:"load_15m"` NetRXRate float64 `json:"net_rx_rate"` NetTXRate float64 `json:"net_tx_rate"` NetRXTotal uint64 `json:"net_rx_total"` NetTXTotal uint64 `json:"net_tx_total"` UptimeSeconds float64 `json:"uptime_seconds"` BootTime int64 `json:"boot_time"` MemPercent int `json:"mem_percent"` MemAvailable string `json:"mem_available"` DiskPercent int `json:"disk_percent"` DiskFree string `json:"disk_free"` IOPSI float64 `json:"io_psi"` Docker bool `json:"docker"` UpdatedAt int64 `json:"updated_at"` } func Collect(defaultRole string) Profile { role := os.Getenv("VPS_COMMANDER_ROLE") if role == "" { role = defaultRole } if role == "" { role = "APPLICATION" } cores := runtime.NumCPU() cpuPercent := getCPUUsage() load1m, load5m, load15m := getLoads() netRX, netTX, netRXT, netTXT := getNetwork() uptime, boot := getUptime() memPct, memAvl := getMemInfo() diskPct, diskFree := getDiskInfo() psi := getIOPSI() docker := hasDocker() return Profile{ Role: role, CPUCores: cores, CPUUsagePercent: cpuPercent, Load1M: load1m, Load5M: load5m, Load15M: load15m, NetRXRate: netRX, NetTXRate: netTX, NetRXTotal: netRXT, NetTXTotal: netTXT, UptimeSeconds: uptime, BootTime: boot, MemPercent: memPct, MemAvailable: memAvl, DiskPercent: diskPct, DiskFree: diskFree, IOPSI: psi, Docker: docker, UpdatedAt: time.Now().Unix(), } } func getLoads() (float64, float64, float64) { data, err := os.ReadFile("/proc/loadavg") if err != nil { return 0, 0, 0 } f := strings.Fields(string(data)) if len(f) < 3 { return 0, 0, 0 } v := func(i int) float64 { x, _ := strconv.ParseFloat(f[i], 64); return x } return v(0), v(1), v(2) } func getNetwork() (float64, float64, uint64, uint64) { data, err := os.ReadFile("/proc/net/dev") if err != nil { return 0, 0, 0, 0 } var rx, tx uint64 for _, line := range strings.Split(string(data), "\n") { parts := strings.SplitN(line, ":", 2) if len(parts) != 2 || strings.TrimSpace(parts[0]) == "lo" { continue } f := strings.Fields(parts[1]) if len(f) < 9 { continue } r, e1 := strconv.ParseUint(f[0], 10, 64) t, e2 := strconv.ParseUint(f[8], 10, 64) if e1 == nil && e2 == nil { rx += r tx += t } } now := time.Now() netMu.Lock() defer netMu.Unlock() var rr, tr float64 if !lastNet.at.IsZero() && now.After(lastNet.at) && rx >= lastNet.rx && tx >= lastNet.tx { d := now.Sub(lastNet.at).Seconds() if d > 0 { rr = float64(rx-lastNet.rx) / d tr = float64(tx-lastNet.tx) / d } } lastNet.rx, lastNet.tx, lastNet.at = rx, tx, now return rr, tr, rx, tx } func getUptime() (float64, int64) { data, err := os.ReadFile("/proc/uptime") if err != nil { return 0, time.Now().Unix() } f := strings.Fields(string(data)) if len(f) == 0 { return 0, time.Now().Unix() } u, err := strconv.ParseFloat(f[0], 64) if err != nil { return 0, time.Now().Unix() } return u, time.Now().Add(-time.Duration(u * float64(time.Second))).Unix() } func getCPUUsage() int { pct := sampleCPUStat() if pct >= 0 { return pct } if data, err := os.ReadFile("/proc/loadavg"); err == nil { fields := strings.Fields(string(data)) if len(fields) > 0 { if l1, err := strconv.ParseFloat(fields[0], 64); err == nil { cores := float64(runtime.NumCPU()) if cores < 1 { cores = 1 } val := int((l1 / cores) * 100) if val > 100 { val = 100 } return val } } } return 0 } func sampleCPUStat() int { readStat := func() (idle, total uint64, ok bool) { f, err := os.Open("/proc/stat") if err != nil { return 0, 0, false } defer f.Close() scanner := bufio.NewScanner(f) if scanner.Scan() { fields := strings.Fields(scanner.Text()) if len(fields) >= 5 && fields[0] == "cpu" { var sum uint64 for i := 1; i < len(fields); i++ { v, _ := strconv.ParseUint(fields[i], 10, 64) sum += v } idleVal, _ := strconv.ParseUint(fields[4], 10, 64) return idleVal, sum, true } } return 0, 0, false } idleCurr, totalCurr, ok := readStat() if !ok { return -1 } now := time.Now() cpuMu.Lock() defer cpuMu.Unlock() if lastCPU.total > 0 && totalCurr > lastCPU.total && now.Sub(lastCPU.at) >= 500*time.Millisecond { deltaTotal := totalCurr - lastCPU.total deltaIdle := idleCurr - lastCPU.idle if deltaTotal > 0 && deltaTotal >= deltaIdle { busy := float64(deltaTotal-deltaIdle) / float64(deltaTotal) * 100 if busy < 0 { busy = 0 } if busy > 100 { busy = 100 } lastCPU.usage = int(busy) lastCPU.idle = idleCurr lastCPU.total = totalCurr lastCPU.at = now return lastCPU.usage } } if lastCPU.total == 0 { // 初次初始化基线 lastCPU.idle = idleCurr lastCPU.total = totalCurr lastCPU.at = now // 用 200ms 短采样作为初始值 time.Sleep(200 * time.Millisecond) if idle2, total2, ok2 := readStat(); ok2 && total2 > totalCurr { dTotal := total2 - totalCurr dIdle := idle2 - idleCurr if dTotal > 0 && dTotal >= dIdle { busy := float64(dTotal-dIdle) / float64(dTotal) * 100 lastCPU.usage = int(busy) lastCPU.idle = idle2 lastCPU.total = total2 lastCPU.at = time.Now() } } } return lastCPU.usage } func getMemInfo() (memPct int, memAvl string) { f, err := os.Open("/proc/meminfo") if err != nil { return 0, "N/A" } defer f.Close() var total, free, avail, buffers, cached uint64 scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() parts := strings.SplitN(line, ":", 2) if len(parts) != 2 { continue } key := strings.TrimSpace(parts[0]) valFields := strings.Fields(parts[1]) if len(valFields) == 0 { continue } v, _ := strconv.ParseUint(valFields[0], 10, 64) switch key { case "MemTotal": total = v case "MemFree": free = v case "MemAvailable": avail = v case "Buffers": buffers = v case "Cached": cached = v } } if avail == 0 { avail = free + buffers + cached } if total > 0 { used := total - avail memPct = int(float64(used) / float64(total) * 100) if avail >= 1024*1024 { memAvl = fmt.Sprintf("%.1fG", float64(avail)/(1024*1024)) } else { memAvl = fmt.Sprintf("%dM", avail/1024) } } else { memAvl = "N/A" } return memPct, memAvl } func getDiskInfo() (diskPct int, diskFree string) { var stat syscall.Statfs_t if err := syscall.Statfs("/", &stat); err != nil { return 0, "N/A" } total := stat.Blocks * uint64(stat.Bsize) free := stat.Bavail * uint64(stat.Bsize) if total == 0 { return 0, "N/A" } used := total - free diskPct = int(float64(used) / float64(total) * 100) if diskPct > 100 { diskPct = 100 } if free >= 1024*1024*1024 { diskFree = fmt.Sprintf("%.1fG", float64(free)/(1024*1024*1024)) } else { diskFree = fmt.Sprintf("%dM", free/(1024*1024)) } return diskPct, diskFree } func getIOPSI() float64 { data, err := os.ReadFile("/proc/pressure/io") if err == nil { lines := strings.Split(string(data), "\n") for _, line := range lines { if strings.HasPrefix(line, "some") { fields := strings.Fields(line) for _, f := range fields { if strings.HasPrefix(f, "avg10=") { val, err := strconv.ParseFloat(strings.TrimPrefix(f, "avg10="), 64) if err == nil { return val } } } } } } if data, err := os.ReadFile("/proc/loadavg"); err == nil { fields := strings.Fields(string(data)) if len(fields) > 0 { if val, err := strconv.ParseFloat(fields[0], 64); err == nil { return val } } } return 0.0 } func hasDocker() bool { if _, err := os.Stat("/var/run/docker.sock"); err == nil { return true } return false }