feat(web): 仪表盘补系统资源/中国 IP 段/通知通道与公网穿透卡,修复 frpc 版本读取
仪表盘:修「服务」卡写死的「正常」——改为按 Fonu/Nginx/DDNS/证书四项实际状态聚合,异常时点名并降级徽标 仪表盘:新增「性能」面板(今日异常/平均响应,/api/status 早已返回但从未展示)与「流量 Top 5」(复用已有逐规则流量轮询);/api/status 每 15 秒刷新并在页面隐藏时暂停(连同流量轮询) 仪表盘:新增「公网穿透」统计卡;运行状态面板增加公网反代行,数据来自新增的 GET /api/frpmulti/routes/summary(逐台服务端聚合,单台失败不中断) 仪表盘:新增一行三卡「系统资源 / 中国 IP 段 / 通知通道」;系统资源为新增跨平台采集 internal/sysinfo(Linux /proc、Windows kernel32,无新依赖) FRP:修复 frpc 版本读不出来——RuntimeStatus.Version 带 omitempty,--version 探测失败时字段被省略,前端只能显示未检测;现回落按 <FrpDir>/bin/<版本>/ 目录名推导(与「FRP 应用配置」弹窗的 active_version 同源),并逐台服务端探测版本 清理:删除 DashboardView 死样式(.dashboard/.stat-highlight/.proxy-metric*)
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// Package sysinfo 采集主机资源占用(CPU / 内存 / 磁盘),供仪表盘展示。
|
||||
//
|
||||
// 注意:在容器(Docker)里运行时,/proc 与根文件系统通常反映的是宿主机的总量与占用
|
||||
// (除非挂载了 lxcfs 之类的视图隔离),因此这里的数值是「宿主机视角」,不是容器配额。
|
||||
package sysinfo
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CPUSample 是 CPU 累计计数器的一次快照;占用率需要两次采样求差值
|
||||
type CPUSample struct {
|
||||
Total uint64
|
||||
Idle uint64
|
||||
}
|
||||
|
||||
// Stats 是主机资源快照;当前平台采集不到的字段保持 0
|
||||
type Stats struct {
|
||||
CPUPercent float64
|
||||
MemUsedBytes uint64
|
||||
MemTotalBytes uint64
|
||||
DiskUsedBytes uint64
|
||||
DiskTotalBytes uint64
|
||||
}
|
||||
|
||||
// Read 采集一次主机资源。cpuPrev 为上次采样的 CPU 计数器(零值表示首次采集,
|
||||
// 此时 CPUPercent 为 0),返回的 sample 供下次调用传入。
|
||||
func Read(cpuPrev CPUSample) (Stats, CPUSample) {
|
||||
stats, sample := readPlatform()
|
||||
stats.CPUPercent = CPUPercent(cpuPrev, sample)
|
||||
return stats, sample
|
||||
}
|
||||
|
||||
// CPUPercent 由两次采样的差值计算占用率;首次采样或计数器未前进时返回 0
|
||||
func CPUPercent(prev, cur CPUSample) float64 {
|
||||
if prev.Total == 0 || cur.Total <= prev.Total {
|
||||
return 0
|
||||
}
|
||||
totalDelta := cur.Total - prev.Total
|
||||
var idleDelta uint64
|
||||
if cur.Idle > prev.Idle {
|
||||
idleDelta = cur.Idle - prev.Idle
|
||||
}
|
||||
if idleDelta >= totalDelta {
|
||||
return 0
|
||||
}
|
||||
return float64(totalDelta-idleDelta) / float64(totalDelta) * 100
|
||||
}
|
||||
|
||||
// parseProcStat 解析 /proc/stat 的 cpu 汇总行;第 4、5 列(idle、iowait)都计入空闲
|
||||
func parseProcStat(raw string) CPUSample {
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 5 || fields[0] != "cpu" {
|
||||
continue
|
||||
}
|
||||
var total, idle uint64
|
||||
for i, field := range fields[1:] {
|
||||
value, err := strconv.ParseUint(field, 10, 64)
|
||||
if err != nil {
|
||||
return CPUSample{}
|
||||
}
|
||||
total += value
|
||||
if i == 3 || i == 4 {
|
||||
idle += value
|
||||
}
|
||||
}
|
||||
return CPUSample{Total: total, Idle: idle}
|
||||
}
|
||||
return CPUSample{}
|
||||
}
|
||||
|
||||
// parseMemInfo 解析 /proc/meminfo(值以 kB 为单位),返回总内存与已用内存字节数。
|
||||
// 已用内存优先按 MemAvailable 计算,缺失时回落到 MemFree + Buffers + Cached。
|
||||
func parseMemInfo(raw string) (total, used uint64) {
|
||||
values := make(map[string]uint64, 16)
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
key, value, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(value)
|
||||
if len(fields) == 0 {
|
||||
continue
|
||||
}
|
||||
amount, err := strconv.ParseUint(fields[0], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
values[strings.TrimSpace(key)] = amount * 1024
|
||||
}
|
||||
total = values["MemTotal"]
|
||||
if total == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
available := values["MemAvailable"]
|
||||
if available == 0 {
|
||||
available = values["MemFree"] + values["Buffers"] + values["Cached"]
|
||||
}
|
||||
if available > total {
|
||||
return total, 0
|
||||
}
|
||||
return total, total - available
|
||||
}
|
||||
Reference in New Issue
Block a user