174 lines
4.7 KiB
Go
174 lines
4.7 KiB
Go
package service
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"hy2xs-admin/dao"
|
|
"hy2xs-admin/model/entity"
|
|
"hy2xs-admin/model/vo"
|
|
"hy2xs-admin/util"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const hysteriaVersionCacheTTL = 15 * time.Minute
|
|
const statsRetention = 7 * 24 * time.Hour
|
|
|
|
var ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*[A-Za-z]`)
|
|
var hysteriaVersionRe = regexp.MustCompile(`(?i)v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?`)
|
|
|
|
func normalizeHysteriaVersion(raw string) string {
|
|
cleaned := strings.TrimSpace(raw)
|
|
cleaned = ansiRe.ReplaceAllString(cleaned, "")
|
|
match := hysteriaVersionRe.FindString(cleaned)
|
|
if match == "" {
|
|
return "-"
|
|
}
|
|
return strings.TrimSpace(match)
|
|
}
|
|
|
|
type metricsSnapshot struct {
|
|
CollectedAt int64
|
|
System vo.DashboardSystemVo
|
|
Hysteria vo.DashboardHysteriaVo
|
|
OnlinePeers int64
|
|
OnlineDevices int64
|
|
CollectorState vo.DataHealthVo
|
|
HysteriaState vo.DataHealthVo
|
|
}
|
|
|
|
var metricsStore = struct {
|
|
sync.RWMutex
|
|
snapshot metricsSnapshot
|
|
lastSuccessAt int64
|
|
version string
|
|
versionAt time.Time
|
|
}{}
|
|
|
|
func getCachedHysteriaVersion(now time.Time) string {
|
|
metricsStore.RLock()
|
|
if metricsStore.version != "" && now.Sub(metricsStore.versionAt) < hysteriaVersionCacheTTL {
|
|
v := metricsStore.version
|
|
metricsStore.RUnlock()
|
|
return v
|
|
}
|
|
metricsStore.RUnlock()
|
|
|
|
content, err := util.Exec(util.GetHysteria2BinPath() + " version")
|
|
if err != nil {
|
|
metricsStore.RLock()
|
|
v := metricsStore.version
|
|
metricsStore.RUnlock()
|
|
if v != "" {
|
|
return v
|
|
}
|
|
return "-"
|
|
}
|
|
normalized := normalizeHysteriaVersion(content)
|
|
if normalized == "-" {
|
|
logrus.Debugf("hysteria version parse failed, raw output: %q", strings.TrimSpace(content))
|
|
}
|
|
|
|
metricsStore.Lock()
|
|
metricsStore.version = normalized
|
|
metricsStore.versionAt = now
|
|
metricsStore.Unlock()
|
|
return normalized
|
|
}
|
|
|
|
func CollectMetricsSnapshot() {
|
|
nowMs := time.Now().UnixMilli()
|
|
s := metricsSnapshot{
|
|
CollectedAt: nowMs,
|
|
CollectorState: vo.DataHealthVo{Status: "ok", LastSuccessAt: nowMs},
|
|
HysteriaState: vo.DataHealthVo{Status: "ok", LastSuccessAt: nowMs},
|
|
}
|
|
|
|
if cpuPercent, err := util.GetCpuPercent(); err == nil {
|
|
s.System.CpuPercent = cpuPercent
|
|
}
|
|
if memPercent, err := util.GetMemPercent(); err == nil {
|
|
s.System.MemPercent = memPercent
|
|
}
|
|
if memInfo, err := util.GetMemInfo(); err == nil {
|
|
s.System.MemUsedBytes = memInfo.Used
|
|
s.System.MemTotalBytes = memInfo.Total
|
|
}
|
|
if diskPercent, err := util.GetDiskPercent(); err == nil {
|
|
s.System.DiskPercent = diskPercent
|
|
}
|
|
if diskInfo, err := util.GetDiskInfo(); err == nil {
|
|
s.System.DiskUsedBytes = diskInfo.Used
|
|
s.System.DiskTotalBytes = diskInfo.Total
|
|
}
|
|
|
|
s.Hysteria.Running = Hysteria2IsRunning()
|
|
s.Hysteria.Version = getCachedHysteriaVersion(time.Now())
|
|
|
|
if onlineMap, err := Hysteria2Online(); err == nil {
|
|
s.Hysteria.ApiReachable = true
|
|
s.OnlinePeers = int64(len(onlineMap))
|
|
for _, c := range onlineMap {
|
|
s.OnlineDevices += c
|
|
}
|
|
} else {
|
|
s.Hysteria.ApiReachable = false
|
|
s.Hysteria.LastApiError = "dashboard.error.trafficApiUnavailable"
|
|
s.HysteriaState.Status = "error"
|
|
s.HysteriaState.MessageKey = "dashboard.error.trafficApiUnavailable"
|
|
}
|
|
|
|
metricsStore.Lock()
|
|
metricsStore.snapshot = s
|
|
metricsStore.lastSuccessAt = nowMs
|
|
metricsStore.Unlock()
|
|
|
|
running := int64(0)
|
|
if s.Hysteria.Running {
|
|
running = 1
|
|
}
|
|
diskPath := "/"
|
|
if configured := util.GetEnvDiskPath(); configured != "" {
|
|
diskPath = configured
|
|
}
|
|
metric := entity.MetricSample{
|
|
SampledAt: &nowMs,
|
|
CpuPercent: &s.System.CpuPercent,
|
|
Load1: func() *float64 { v := float64(0); return &v }(),
|
|
MemUsedBytes: func() *int64 { v := int64(s.System.MemUsedBytes); return &v }(),
|
|
MemTotalBytes: func() *int64 { v := int64(s.System.MemTotalBytes); return &v }(),
|
|
MemPercent: &s.System.MemPercent,
|
|
DiskPath: &diskPath,
|
|
DiskUsedBytes: func() *int64 { v := int64(s.System.DiskUsedBytes); return &v }(),
|
|
DiskTotalBytes: func() *int64 { v := int64(s.System.DiskTotalBytes); return &v }(),
|
|
DiskPercent: &s.System.DiskPercent,
|
|
HysteriaRunning: &running,
|
|
OnlinePeers: &s.OnlinePeers,
|
|
OnlineDevices: &s.OnlineDevices,
|
|
}
|
|
_ = dao.SaveMetricSample(metric)
|
|
}
|
|
|
|
func CleanupStatsRetention() {
|
|
now := time.Now()
|
|
cutoffMs := now.Add(-statsRetention).UnixMilli()
|
|
|
|
hourMs := int64(time.Hour / time.Millisecond)
|
|
dayMs := int64(24 * time.Hour / time.Millisecond)
|
|
cutoffHour := cutoffMs - (cutoffMs % hourMs)
|
|
cutoffDay := cutoffMs - (cutoffMs % dayMs)
|
|
|
|
_ = dao.CleanupMetricSample(cutoffMs)
|
|
_ = dao.CleanupTrafficSample(cutoffMs)
|
|
_ = dao.CleanupTrafficAggregateHourly(cutoffHour)
|
|
_ = dao.CleanupTrafficAggregateDaily(cutoffDay)
|
|
}
|
|
|
|
func DashboardSnapshot() metricsSnapshot {
|
|
metricsStore.RLock()
|
|
defer metricsStore.RUnlock()
|
|
return metricsStore.snapshot
|
|
}
|