New internal/web/service/probe package keeps probe history in its own probe.db beside the panel DB (single connection, WAL), so history writes never contend with the panel write lock. - Minute rows per hub semantics: a held-reading rhythm books traffic against each reading own arrival; minute buckets merge (means, rounded int means, peaks) and the stored net rate derives from the kernel counters between row marks, falling back to agent rates at epoch breaks. - probe_traffic ledger: accumulate is a line port of hub traffic.rs (baseline alignment, same-boot shrink books 0, epoch change books 0, unseen-boot exception books the whole counters-since-boot measured against the master last-seen stamp, per-direction period resets, saturating adds); reads gate stale day/month stamps to zero. - probe_meta rows are ensured for every reporting agent (B3 edits them in place); probe_ping_* tables are created now, written by B4. - Hourly roll_up behind a watermark (hour-aligned, late grace, pre-retention hours skipped unfoldered) and watermark-guarded chunked prune (minute tier 7 days or the retention window when shorter; hourly tier to retention_days, default 7, cap 365). - Wiring: heartbeat files probe samples after the snapshot update (last-seen captured before touch); housekeeping hangs off the existing robfig/cron in web.startTask (@hourly + one pass a minute after startup, mirroring the hub boot pass); Server.stop books held readings. Every failure path logs and degrades - a broken probe.db never fails a heartbeat or the panel.
86 lines
2.3 KiB
Go
86 lines
2.3 KiB
Go
package probe
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
)
|
|
|
|
// RetentionDays reads the history retention window (days) from the
|
|
// panel settings (key retention_days, as in hub). Default 7 — the
|
|
// window the user settled on — clamped to [1, 365] (hub's cap).
|
|
func RetentionDays() int64 {
|
|
var row model.Setting
|
|
err := database.GetDB().Where("key = ?", "retention_days").First(&row).Error
|
|
if err != nil {
|
|
return DetailDays
|
|
}
|
|
n, perr := strconv.ParseInt(strings.TrimSpace(row.Value), 10, 64)
|
|
if perr != nil {
|
|
return DetailDays
|
|
}
|
|
if n < 1 {
|
|
return 1
|
|
}
|
|
if n > 365 {
|
|
return 365
|
|
}
|
|
return n
|
|
}
|
|
|
|
// Record files one heartbeat's probe sample into the default store.
|
|
// prevLastSeenMs is the agent row's last-seen stamp from before this
|
|
// heartbeat (milliseconds, as stored on agent_nodes). Best-effort: any
|
|
// failure is logged and swallowed so a heartbeat never fails over
|
|
// history.
|
|
func Record(agentID int64, smp Sample, prevLastSeenMs int64) {
|
|
st := Default()
|
|
if st == nil {
|
|
return
|
|
}
|
|
if err := st.Record(agentID, smp, time.Now(), prevLastSeenMs/1000); err != nil {
|
|
logger.Warningf("probe: record agent %d: %v", agentID, err)
|
|
}
|
|
}
|
|
|
|
// Maintain runs one housekeeping pass against the default store:
|
|
// roll_up first, then prune — prune runs even when roll_up fails, as
|
|
// it keeps minute rows until their hour is folded and skipping it
|
|
// would leave the database growing (hub housekeeping discipline).
|
|
func Maintain() {
|
|
st := Default()
|
|
if st == nil {
|
|
return
|
|
}
|
|
keep := RetentionDays()
|
|
if folded, err := st.RollUp(time.Now().Unix(), keep); err != nil {
|
|
logger.Warningf("probe: roll_up failed: %v", err)
|
|
} else if folded > 1 {
|
|
logger.Infof("probe: folded %d hours of history into the hourly tier", folded)
|
|
}
|
|
if _, err := st.Prune(keep); err != nil {
|
|
logger.Warningf("probe: prune failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// BookHeld books every node's held, not-yet-booked reading on the
|
|
// default store; called as the master shuts down.
|
|
func BookHeld() {
|
|
if st := Default(); st != nil {
|
|
st.BookHeld()
|
|
}
|
|
}
|
|
|
|
// TrafficSnapshot is the B3 read port: every agent's gated ledger.
|
|
func TrafficSnapshot() (map[int64]Traffic, error) {
|
|
st := Default()
|
|
if st == nil {
|
|
return map[int64]Traffic{}, nil
|
|
}
|
|
return st.AllTraffic()
|
|
}
|