package service import ( "hy2xs-admin/dao" "hy2xs-admin/model/entity" "hy2xs-admin/model/vo" "hy2xs-admin/util" "sync" "time" ) const hysteriaVersionCacheTTL = 15 * time.Minute 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 "-" } metricsStore.Lock() metricsStore.version = content metricsStore.versionAt = now metricsStore.Unlock() return content } 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 CleanupMetricsRetention() { cutoff := time.Now().Add(-72 * time.Hour).UnixMilli() _ = dao.CleanupMetricSample(cutoff) } func DashboardSnapshot() metricsSnapshot { metricsStore.RLock() defer metricsStore.RUnlock() return metricsStore.snapshot }