feat(telemetry): agent 心跳遥测上报+节点列表覆写(第13条)
agent 每 60s 心跳捎带遥测: agentVersion/xrayVersion(按 mtime 缓存重读)/ uptime/最近一次主控请求 RTT/cpu%/mem%/公网 v4+v6(公网优先, ULA/私网兜底); 主控心跳处理按指针语义只存上报字段, 旧 agent 无遥测行为不变; agent_nodes 加法迁移存遥测, 节点列表对 agent 行覆写真值, panelVersion 显示 "agent <版本>", NodeView 新增 ipv4/ipv6。
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
// P13: the heartbeat optionally carries a telemetry object. Reported fields
|
||||
// are persisted onto the agent row; a heartbeat without telemetry (older
|
||||
// agents) must not clear previously stored values, and a partial report
|
||||
// only overwrites the fields it carries.
|
||||
func TestAgentHeartbeatTelemetry(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
dbDir := t.TempDir()
|
||||
t.Setenv("XUI_DB_FOLDER", dbDir)
|
||||
if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
|
||||
t.Fatalf("InitDB: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.CloseDB() })
|
||||
db := database.GetDB()
|
||||
|
||||
node := &model.Node{Name: "telemetry-node", Address: "10.0.9.1"}
|
||||
if err := db.Create(node).Error; err != nil {
|
||||
t.Fatalf("seed node: %v", err)
|
||||
}
|
||||
const token = "telemetry-test-token"
|
||||
agent := &model.AgentNode{
|
||||
TokenHash: hashAgentToken(token),
|
||||
TokenPrefix: "telemetry-",
|
||||
Name: "telemetry-node",
|
||||
NodeID: &node.Id,
|
||||
}
|
||||
if err := db.Create(agent).Error; err != nil {
|
||||
t.Fatalf("seed agent: %v", err)
|
||||
}
|
||||
|
||||
engine := gin.New()
|
||||
NewAgentController(engine.Group("/panel/api/agent"))
|
||||
|
||||
post := func(body string) int {
|
||||
req := httptest.NewRequest(http.MethodPost, "/panel/api/agent/heartbeat", bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
rec := httptest.NewRecorder()
|
||||
engine.ServeHTTP(rec, req)
|
||||
return rec.Code
|
||||
}
|
||||
load := func() model.AgentNode {
|
||||
var row model.AgentNode
|
||||
if err := db.First(&row, agent.Id).Error; err != nil {
|
||||
t.Fatalf("reload agent: %v", err)
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
full := `{"token":"` + token + `","timestamp":1,"telemetry":{` +
|
||||
`"agentVersion":"3.7.0-agent.1","xrayVersion":"26.6.27","uptimeSeconds":90061,` +
|
||||
`"latencyMs":12,"cpuPercent":3.5,"memPercent":41.2,` +
|
||||
`"ipv4":"203.0.113.7","ipv6":"2001:db8::7"}}`
|
||||
if code := post(full); code != http.StatusOK {
|
||||
t.Fatalf("heartbeat with telemetry: status %d", code)
|
||||
}
|
||||
row := load()
|
||||
if row.AgentVersion != "3.7.0-agent.1" || row.XrayVersion != "26.6.27" ||
|
||||
row.UptimeSecs != 90061 || row.LatencyMs != 12 ||
|
||||
row.CpuPct != 3.5 || row.MemPct != 41.2 ||
|
||||
row.IPv4 != "203.0.113.7" || row.IPv6 != "2001:db8::7" || row.TelemetryAt == 0 {
|
||||
t.Fatalf("telemetry not stored: %+v", row)
|
||||
}
|
||||
|
||||
// No telemetry object (old agent): stored values survive.
|
||||
if code := post(`{"token":"` + token + `","timestamp":2}`); code != http.StatusOK {
|
||||
t.Fatalf("bare heartbeat: status %d", code)
|
||||
}
|
||||
row = load()
|
||||
if row.XrayVersion != "26.6.27" || row.IPv4 != "203.0.113.7" || row.UptimeSecs != 90061 {
|
||||
t.Fatalf("bare heartbeat cleared telemetry: %+v", row)
|
||||
}
|
||||
|
||||
// Partial report: only the carried field changes.
|
||||
if code := post(`{"token":"` + token + `","timestamp":3,"telemetry":{"cpuPercent":9.25}}`); code != http.StatusOK {
|
||||
t.Fatalf("partial heartbeat: status %d", code)
|
||||
}
|
||||
row = load()
|
||||
if row.CpuPct != 9.25 || row.MemPct != 41.2 || row.LatencyMs != 12 {
|
||||
t.Fatalf("partial telemetry merged wrong: %+v", row)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user