mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-23 19:37:13 +00:00
feat(dashboard): more System History metrics, persistence & localized labels
- Sample swap %, TCP/UDP connection counts and disk-usage % on the host ticker - System History: Swap overlaid on the RAM tab, plus new Connections and Disk Usage tabs - Persist the host time-series across restarts: gob snapshot beside the DB, written on a timer and at shutdown, restored on boot - Live-refresh the open chart (2s for short ranges, 10s for longer) - Localize CPU/RAM/Swap and the new tab/chart titles across all 13 languages and route legend series names through i18n
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/config"
|
||||
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||
)
|
||||
|
||||
// MetricSample is one point of any time-series we keep in memory.
|
||||
@@ -59,6 +65,34 @@ func (h *metricHistory) drop(metric string) {
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// snapshot returns a deep copy of every series, safe to serialize without
|
||||
// holding the lock during disk I/O.
|
||||
func (h *metricHistory) snapshot() map[string][]MetricSample {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
out := make(map[string][]MetricSample, len(h.metrics))
|
||||
for k, v := range h.metrics {
|
||||
cp := make([]MetricSample, len(v))
|
||||
copy(cp, v)
|
||||
out[k] = cp
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// restore replaces the in-memory series with a previously persisted set,
|
||||
// re-applying the per-series capacity cap so a tampered or oversized file
|
||||
// can't grow the working set unbounded.
|
||||
func (h *metricHistory) restore(data map[string][]MetricSample) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
for k, v := range data {
|
||||
if len(v) > metricCapacityDefault {
|
||||
v = v[len(v)-metricCapacityDefault:]
|
||||
}
|
||||
h.metrics[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// aggregate returns up to maxPoints buckets of size bucketSeconds,
|
||||
// each bucket carrying the arithmetic mean of the underlying samples.
|
||||
// Bucket alignment is to absolute Unix-second boundaries so two
|
||||
@@ -137,7 +171,7 @@ var (
|
||||
// status sample. Exposed for documentation/test purposes; the
|
||||
// controller validates incoming names against an allow-list.
|
||||
var SystemMetricKeys = []string{
|
||||
"cpu", "mem", "netUp", "netDown", "pktUp", "pktDown", "diskRead", "diskWrite", "online", "load1", "load5", "load15",
|
||||
"cpu", "mem", "swap", "netUp", "netDown", "pktUp", "pktDown", "diskRead", "diskWrite", "diskUsage", "tcpCount", "udpCount", "online", "load1", "load5", "load15",
|
||||
}
|
||||
|
||||
// NodeMetricKeys lists the per-node metric names NodeHeartbeatJob writes.
|
||||
@@ -150,3 +184,54 @@ var NodeMetricKeys = []string{"cpu", "mem"}
|
||||
var XrayMetricKeys = []string{
|
||||
"xrAlloc", "xrSys", "xrHeapObjects", "xrNumGC", "xrPauseNs",
|
||||
}
|
||||
|
||||
// systemMetricsStorePath is where the host time-series is persisted between
|
||||
// restarts. It lives next to the database so a single volume mount carries
|
||||
// both. Only systemMetrics is persisted — node and xray series are cheap to
|
||||
// rebuild and tied to live connections.
|
||||
func systemMetricsStorePath() string {
|
||||
return filepath.Join(config.GetDBFolderPath(), "system_metrics.gob")
|
||||
}
|
||||
|
||||
// PersistSystemMetrics writes the host time-series to disk via a temp file +
|
||||
// rename so a crash mid-write can't corrupt the previous snapshot. Called on a
|
||||
// timer and at shutdown.
|
||||
func PersistSystemMetrics() error {
|
||||
path := systemMetricsStorePath()
|
||||
tmp := path + ".tmp"
|
||||
f, err := os.Create(tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gob.NewEncoder(f).Encode(systemMetrics.snapshot()); err != nil {
|
||||
f.Close()
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
// RestoreSystemMetrics loads a previously persisted host time-series on startup.
|
||||
// A missing file is not an error (first boot). Aggregation already windows by
|
||||
// time, so any gap from downtime is handled by the readers.
|
||||
func RestoreSystemMetrics() {
|
||||
path := systemMetricsStorePath()
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
logger.Warning("restore system metrics failed:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
var data map[string][]MetricSample
|
||||
if err := gob.NewDecoder(f).Decode(&data); err != nil {
|
||||
logger.Warning("decode system metrics failed:", err)
|
||||
return
|
||||
}
|
||||
systemMetrics.restore(data)
|
||||
}
|
||||
|
||||
@@ -565,12 +565,22 @@ func (s *ServerService) AppendStatusSample(t time.Time, status *Status) {
|
||||
if status.Mem.Total > 0 {
|
||||
systemMetrics.append("mem", t, float64(status.Mem.Current)*100.0/float64(status.Mem.Total))
|
||||
}
|
||||
if status.Swap.Total > 0 {
|
||||
systemMetrics.append("swap", t, float64(status.Swap.Current)*100.0/float64(status.Swap.Total))
|
||||
} else {
|
||||
systemMetrics.append("swap", t, 0)
|
||||
}
|
||||
systemMetrics.append("netUp", t, float64(status.NetIO.Up))
|
||||
systemMetrics.append("netDown", t, float64(status.NetIO.Down))
|
||||
systemMetrics.append("diskRead", t, float64(status.DiskIO.Read))
|
||||
systemMetrics.append("diskWrite", t, float64(status.DiskIO.Write))
|
||||
if status.Disk.Total > 0 {
|
||||
systemMetrics.append("diskUsage", t, float64(status.Disk.Current)*100.0/float64(status.Disk.Total))
|
||||
}
|
||||
systemMetrics.append("pktUp", t, float64(status.NetIO.PktUp))
|
||||
systemMetrics.append("pktDown", t, float64(status.NetIO.PktDown))
|
||||
systemMetrics.append("tcpCount", t, float64(status.TcpCount))
|
||||
systemMetrics.append("udpCount", t, float64(status.UdpCount))
|
||||
online := 0
|
||||
if p != nil && p.IsRunning() {
|
||||
online = len(p.GetOnlineClients())
|
||||
|
||||
Reference in New Issue
Block a user