P7: restore agent node type, agent heartbeat expiry, build-version injection, remove log/history/xray-metrics modules, optional sub port
- nodes: port node-type (traditional/agent) picker back from web-src into the embedded frontend; backend annotates NodeView.nodeType from agent_nodes bindings; fix agent-node update path (skip reachability probe and apiToken requirement for agent-managed nodes); restore i18n keys in all 13 locales; install command now uses the Gitea mirror - jobs: expire agent-bound nodes whose agent_nodes.last_seen is older than max(3x heartbeat interval, 120s) = 180s; last_seen=0 (never registered) is left untouched; transitions reuse UpdateHeartbeat + node event broadcast - config: GetBaseVersion honors ldflags-injected buildVersion; release builds inject 3.7.0-agent.1 so the panel no longer reports a phantom update against the Gitea release tag - logger/server: remove the three log viewers (panel/xray/amneziawg), their API endpoints and the 10240-entry in-memory log buffer; file logging via lumberjack unchanged; tests assert via a new optional logger hook instead of the removed buffer - server: remove system history and xray metrics modules end to end (endpoints, sampling appends in RefreshStatus, per-minute persist cron, gob snapshot/restore, XrayMetricsService); realtime status and per-node history are untouched - settings: subPort may be 0/unset - subscription links are generated without a port while the subscription service keeps binding the default 2096; the settings form declares the cleared state via onNumber's new onClear option instead of snapping back to 1
This commit is contained in:
@@ -1,15 +1,9 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/config"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
)
|
||||
|
||||
// MetricSample is one point of any time-series we keep in memory.
|
||||
@@ -118,8 +112,7 @@ func (s *series) pickTier(spanSeconds int64) *tierBuf {
|
||||
}
|
||||
|
||||
// metricHistory is a thread-safe, in-memory store of tiered series keyed by
|
||||
// arbitrary strings. Three singletons live below: system-wide host metrics,
|
||||
// per-node metrics, and xray expvar metrics.
|
||||
// arbitrary strings. One singleton lives below: per-node metrics.
|
||||
type metricHistory struct {
|
||||
mu sync.Mutex
|
||||
series map[string]*series
|
||||
@@ -215,156 +208,12 @@ func (h *metricHistory) aggregate(metric string, bucketSeconds int, maxPoints in
|
||||
return out
|
||||
}
|
||||
|
||||
// persistedTier and persistedSeries are the on-disk shape of a series. Tiers are
|
||||
// matched back by resolution on restore, so changing the ladder degrades
|
||||
// gracefully (unmatched layers are dropped) instead of corrupting state.
|
||||
type persistedTier struct {
|
||||
Resolution int
|
||||
Samples []MetricSample
|
||||
}
|
||||
|
||||
type persistedSeries struct {
|
||||
Tiers []persistedTier
|
||||
}
|
||||
|
||||
// snapshot returns a deep copy of every series' closed buckets, safe to
|
||||
// serialize without holding the lock during disk I/O.
|
||||
func (h *metricHistory) snapshot() map[string]persistedSeries {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
out := make(map[string]persistedSeries, len(h.series))
|
||||
for k, s := range h.series {
|
||||
ps := persistedSeries{Tiers: make([]persistedTier, len(s.tiers))}
|
||||
for i, tb := range s.tiers {
|
||||
cp := make([]MetricSample, len(tb.samples))
|
||||
copy(cp, tb.samples)
|
||||
ps.Tiers[i] = persistedTier{Resolution: tb.resolution, Samples: cp}
|
||||
}
|
||||
out[k] = ps
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// restore replaces the in-memory series with a previously persisted set,
|
||||
// re-applying each tier's capacity cap so a tampered or oversized file can't grow
|
||||
// the working set unbounded.
|
||||
func (h *metricHistory) restore(data map[string]persistedSeries) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
for k, ps := range data {
|
||||
s := newSeries()
|
||||
for _, pt := range ps.Tiers {
|
||||
for _, tb := range s.tiers {
|
||||
if tb.resolution != pt.Resolution {
|
||||
continue
|
||||
}
|
||||
samples := pt.Samples
|
||||
if len(samples) > tb.capacity {
|
||||
samples = samples[len(samples)-tb.capacity:]
|
||||
}
|
||||
tb.samples = samples
|
||||
break
|
||||
}
|
||||
}
|
||||
h.series[k] = s
|
||||
}
|
||||
}
|
||||
|
||||
// systemMetrics holds whole-host time series (cpu, mem, netUp, etc.) fed by
|
||||
// ServerService.RefreshStatus every 2s. nodeMetrics holds per-node CPU/Mem fed
|
||||
// by NodeHeartbeatJob. xrayMetrics holds xray expvar series. Only systemMetrics
|
||||
// is persisted; the others rebuild from live connections.
|
||||
var (
|
||||
systemMetrics = newMetricHistory()
|
||||
nodeMetrics = newMetricHistory()
|
||||
xrayMetrics = newMetricHistory()
|
||||
)
|
||||
|
||||
// SystemMetricKeys lists the metric names ServerService writes on every status
|
||||
// sample. Exposed for documentation/test purposes; the controller validates
|
||||
// incoming names against an allow-list.
|
||||
var SystemMetricKeys = []string{
|
||||
"cpu", "mem", "swap", "netUp", "netDown", "pktUp", "pktDown", "diskRead", "diskWrite", "diskUsage", "tcpCount", "udpCount", "online", "load1", "load5", "load15",
|
||||
}
|
||||
// nodeMetrics is the remaining singleton: per-node CPU/Mem/net series fed by
|
||||
// NodeHeartbeatJob (via UpdateHeartbeat) and read by the node history panel.
|
||||
// The whole-host system series and the xray expvar series that used to live
|
||||
// here were removed with the system-history and xray-metrics features,
|
||||
// together with the gob persistence of the host series.
|
||||
var nodeMetrics = newMetricHistory()
|
||||
|
||||
// NodeMetricKeys lists the per-node metric names NodeHeartbeatJob writes.
|
||||
var NodeMetricKeys = []string{"cpu", "mem", "netUp", "netDown"}
|
||||
|
||||
// XrayMetricKeys lists series sourced from xray's /debug/vars expvar endpoint.
|
||||
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.
|
||||
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). A pre-tier flat snapshot is
|
||||
// migrated by replaying its samples through the rollup.
|
||||
func RestoreSystemMetrics() {
|
||||
path := systemMetricsStorePath()
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
logger.Warning("restore system metrics failed:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
var data map[string]persistedSeries
|
||||
decErr := gob.NewDecoder(f).Decode(&data)
|
||||
f.Close()
|
||||
if decErr == nil {
|
||||
systemMetrics.restore(data)
|
||||
return
|
||||
}
|
||||
if migrateLegacySystemMetrics(path) {
|
||||
return
|
||||
}
|
||||
logger.Warning("decode system metrics failed:", decErr)
|
||||
}
|
||||
|
||||
// migrateLegacySystemMetrics loads a pre-tier flat snapshot
|
||||
// (map[string][]MetricSample) and replays it through append so the new tiers are
|
||||
// seeded from the existing history instead of starting empty.
|
||||
func migrateLegacySystemMetrics(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
var legacy map[string][]MetricSample
|
||||
if err := gob.NewDecoder(f).Decode(&legacy); err != nil {
|
||||
return false
|
||||
}
|
||||
for metric, samples := range legacy {
|
||||
for _, p := range samples {
|
||||
systemMetrics.append(metric, time.Unix(p.T, 0), p.V)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user