[优化] 数据库结构优化

This commit is contained in:
ryan
2026-06-01 15:20:43 +08:00
parent fe13db95c2
commit 9de33f7064
53 changed files with 937 additions and 596 deletions
+9 -12
View File
@@ -29,14 +29,15 @@ type AgentNodePayload struct {
NodeID string `json:"node_id"`
Name string `json:"name"`
IP string `json:"ip"`
AgentVersion string `json:"agent_version"`
NginxVersion string `json:"nginx_version"`
Version string `json:"version"`
ExtVersion string `json:"ext_version"`
CurrentVersion string `json:"current_version"`
LastError string `json:"last_error"`
OpenrestyStatus string `json:"openresty_status"`
OpenrestyMessage string `json:"openresty_message"`
Profile *AgentNodeSystemProfile `json:"profile,omitempty"`
Snapshot *AgentNodeMetricSnapshot `json:"snapshot,omitempty"`
OpenrestyObservation *AgentNodeOpenrestyObservation `json:"openresty_observation,omitempty"`
TrafficReport *AgentNodeTrafficReport `json:"traffic_report,omitempty"`
AccessLogs []AgentNodeAccessLog `json:"access_logs,omitempty"`
BufferedObservability []AgentBufferedObservabilityRecord `json:"buffered_observability,omitempty"`
@@ -139,14 +140,14 @@ type NodeView struct {
GeoLatitude *float64 `json:"geo_latitude"`
GeoLongitude *float64 `json:"geo_longitude"`
GeoManualOverride bool `json:"geo_manual_override"`
AgentToken string `json:"agent_token"`
AccessToken string `json:"access_token"`
AutoUpdateEnabled bool `json:"auto_update_enabled"`
UpdateRequested bool `json:"update_requested"`
UpdateChannel string `json:"update_channel"`
UpdateTag string `json:"update_tag"`
RestartOpenrestyRequested bool `json:"restart_openresty_requested"`
AgentVersion string `json:"agent_version"`
NginxVersion string `json:"nginx_version"`
Version string `json:"version"`
ExtVersion string `json:"ext_version"`
OpenrestyStatus string `json:"openresty_status"`
OpenrestyMessage string `json:"openresty_message"`
Status string `json:"status"`
@@ -170,10 +171,6 @@ type NodeView struct {
RelayClientAccessAddr string `json:"relay_client_access_addr"`
RelayClientProxyURL string `json:"relay_client_proxy_url"`
RelayStatus string `json:"relay_status"`
RelayFrpVersion string `json:"relay_frp_version"`
RelayVersion string `json:"relay_version"`
RelayFrpsConnections int `json:"relay_frps_connections"`
RelayFrpsProxyCount int `json:"relay_frps_proxy_count"`
}
func HeartbeatNode(node *model.Node, payload AgentNodePayload) (*HeartbeatResponse, error) {
@@ -199,7 +196,7 @@ func HeartbeatNode(node *model.Node, payload AgentNodePayload) (*HeartbeatRespon
return nil, err
}
}
refreshAgentTokenCache(node)
refreshAccessTokenCache(node)
persistHeartbeatObservability(node.NodeID, payload, node.LastSeenAt)
activeConfig, err := GetActiveConfigMetaForAgent()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -493,8 +490,8 @@ func collectNodeHeartbeatChanges(previous *model.Node, current *model.Node) map[
appendIfChanged("name", previous.Name, current.Name)
appendIfChanged("ip", previous.IP, current.IP)
appendIfChanged("geo_name", previous.GeoName, current.GeoName)
appendIfChanged("agent_version", previous.AgentVersion, current.AgentVersion)
appendIfChanged("nginx_version", previous.NginxVersion, current.NginxVersion)
appendIfChanged("version", previous.Version, current.Version)
appendIfChanged("ext_version", previous.ExtVersion, current.ExtVersion)
appendIfChanged("openresty_status", previous.OpenrestyStatus, current.OpenrestyStatus)
appendIfChanged("openresty_message", previous.OpenrestyMessage, current.OpenrestyMessage)
appendIfChanged("status", previous.Status, current.Status)
+40 -40
View File
@@ -177,7 +177,7 @@ func TestGetActiveConfigForAgentUsesTenMinutePoWSessionDefault(t *testing.T) {
}
}
func TestRegisterNodeWithAgentToken(t *testing.T) {
func TestRegisterNodeWithAccessToken(t *testing.T) {
setupServiceTestDB(t)
// 1. Success path
@@ -203,17 +203,17 @@ func TestRegisterNodeWithAgentToken(t *testing.T) {
payload := AgentNodePayload{
Name: "payload-name-should-be-ignored",
IP: "192.168.1.20",
AgentVersion: "v1.0.1",
NginxVersion: "1.27.1.3",
Version: "v1.0.1",
ExtVersion: "1.27.1.3",
OpenrestyStatus: "healthy",
}
resp, err := RegisterNodeWithAgentToken(stored, payload)
resp, err := RegisterNodeWithAccessToken(stored, payload)
if err != nil {
t.Fatalf("RegisterNodeWithAgentToken failed: %v", err)
t.Fatalf("RegisterNodeWithAccessToken failed: %v", err)
}
if resp.NodeID != stored.NodeID || resp.AgentToken != stored.AgentToken || resp.Name != "reserved-node-1" {
if resp.NodeID != stored.NodeID || resp.AccessToken != stored.AccessToken || resp.Name != "reserved-node-1" {
t.Errorf("unexpected response: %+v", resp)
}
@@ -222,7 +222,7 @@ func TestRegisterNodeWithAgentToken(t *testing.T) {
if err != nil {
t.Fatalf("failed to fetch updated node: %v", err)
}
if updated.AgentVersion != "v1.0.1" || updated.NginxVersion != "1.27.1.3" || updated.OpenrestyStatus != "healthy" {
if updated.Version != "v1.0.1" || updated.ExtVersion != "1.27.1.3" || updated.OpenrestyStatus != "healthy" {
t.Errorf("node attributes were not updated: %+v", updated)
}
// Name should be preserved since preserveName is true
@@ -231,7 +231,7 @@ func TestRegisterNodeWithAgentToken(t *testing.T) {
}
// 2. Fail path - Nil Node
_, err = RegisterNodeWithAgentToken(nil, payload)
_, err = RegisterNodeWithAccessToken(nil, payload)
if err == nil || !strings.Contains(err.Error(), "节点不存在") {
t.Errorf("expected error '节点不存在', got %v", err)
}
@@ -239,16 +239,16 @@ func TestRegisterNodeWithAgentToken(t *testing.T) {
// 3. Fail path - Invalid Payload (empty IP)
badPayload := payload
badPayload.IP = ""
_, err = RegisterNodeWithAgentToken(stored, badPayload)
_, err = RegisterNodeWithAccessToken(stored, badPayload)
if err == nil || !strings.Contains(err.Error(), "ip 不能为空") {
t.Errorf("expected error 'ip 不能为空', got %v", err)
}
// 4. Name update if empty
emptyNameNode := &model.Node{
NodeID: "node-empty-name",
Name: "",
AgentToken: "empty-name-token",
NodeID: "node-empty-name",
Name: "",
AccessToken: "empty-name-token",
}
if err := emptyNameNode.Insert(); err != nil {
t.Fatalf("failed to insert emptyNameNode: %v", err)
@@ -256,9 +256,9 @@ func TestRegisterNodeWithAgentToken(t *testing.T) {
payloadWithName := payload
payloadWithName.Name = "filled-name"
payloadWithName.IP = "192.168.1.30"
_, err = RegisterNodeWithAgentToken(emptyNameNode, payloadWithName)
_, err = RegisterNodeWithAccessToken(emptyNameNode, payloadWithName)
if err != nil {
t.Fatalf("RegisterNodeWithAgentToken empty name node failed: %v", err)
t.Fatalf("RegisterNodeWithAccessToken empty name node failed: %v", err)
}
updatedEmptyName, err := model.GetNodeByNodeID("node-empty-name")
if err != nil {
@@ -276,8 +276,8 @@ func TestRegisterNodeWithDiscovery(t *testing.T) {
payload := AgentNodePayload{
Name: "discovery-node",
IP: "192.168.2.10",
AgentVersion: "v1.0.0",
NginxVersion: "1.27.1.3",
Version: "v1.0.0",
ExtVersion: "1.27.1.3",
OpenrestyStatus: "healthy",
}
@@ -286,7 +286,7 @@ func TestRegisterNodeWithDiscovery(t *testing.T) {
t.Fatalf("RegisterNodeWithDiscovery failed: %v", err)
}
if resp.NodeID == "" || resp.AgentToken == "" || resp.Name != "discovery-node" {
if resp.NodeID == "" || resp.AccessToken == "" || resp.Name != "discovery-node" {
t.Errorf("unexpected response: %+v", resp)
}
@@ -295,7 +295,7 @@ func TestRegisterNodeWithDiscovery(t *testing.T) {
if err != nil {
t.Fatalf("failed to fetch node: %v", err)
}
if node.IP != "192.168.2.10" || node.AgentVersion != "v1.0.0" || node.Name != "discovery-node" {
if node.IP != "192.168.2.10" || node.Version != "v1.0.0" || node.Name != "discovery-node" {
t.Errorf("unexpected stored node data: %+v", node)
}
@@ -317,10 +317,10 @@ func TestRegisterNodeWithDiscovery(t *testing.T) {
// 3. Fail path - Invalid Payload (empty AgentVersion)
badPayload := payload
badPayload.AgentVersion = ""
badPayload.Version = ""
_, err = RegisterNodeWithDiscovery(badPayload)
if err == nil || !strings.Contains(err.Error(), "agent_version 不能为空") {
t.Errorf("expected error 'agent_version 不能为空', got %v", err)
if err == nil || !strings.Contains(err.Error(), "version 不能为空") {
t.Errorf("expected error 'version 不能为空', got %v", err)
}
}
@@ -329,12 +329,12 @@ func TestReportApplyLog_Success(t *testing.T) {
// Seed node
node := &model.Node{
NodeID: "node-apply-1",
Name: "apply-edge",
IP: "192.168.3.10",
AgentToken: "apply-token",
AgentVersion: "v1.0.0",
Status: NodeStatusOffline,
NodeID: "node-apply-1",
Name: "apply-edge",
IP: "192.168.3.10",
AccessToken: "apply-token",
Version: "v1.0.0",
Status: NodeStatusOffline,
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to insert node: %v", err)
@@ -387,8 +387,8 @@ func TestReportApplyLog_WarningAndFailure(t *testing.T) {
NodeID: "node-apply-2",
Name: "apply-edge-2",
IP: "192.168.3.20",
AgentToken: "apply-token-2",
AgentVersion: "v1.0.0",
AccessToken: "apply-token-2",
Version: "v1.0.0",
CurrentVersion: "20260531-001", // Old version
Status: NodeStatusOnline,
}
@@ -452,12 +452,12 @@ func TestReportApplyLog_Failures(t *testing.T) {
// Seed node
node := &model.Node{
NodeID: "node-apply-3",
Name: "apply-edge-3",
IP: "192.168.3.30",
AgentToken: "apply-token-3",
AgentVersion: "v1.0.0",
Status: NodeStatusOnline,
NodeID: "node-apply-3",
Name: "apply-edge-3",
IP: "192.168.3.30",
AccessToken: "apply-token-3",
Version: "v1.0.0",
Status: NodeStatusOnline,
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to insert node: %v", err)
@@ -508,11 +508,11 @@ func TestListAndCleanupApplyLogs(t *testing.T) {
// Seed node
node := &model.Node{
NodeID: "node-logs",
Name: "logs-edge",
IP: "192.168.4.10",
AgentToken: "logs-token",
Status: NodeStatusOnline,
NodeID: "node-logs",
Name: "logs-edge",
IP: "192.168.4.10",
AccessToken: "logs-token",
Status: NodeStatusOnline,
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to insert node: %v", err)
+6 -1
View File
@@ -93,6 +93,11 @@ func GetDashboardOverview() (*DashboardOverviewView, error) {
return nil, err
}
openrestySnapshots, err := model.ListNodeObservationOpenresty("", since, 0)
if err != nil {
return nil, err
}
view := &DashboardOverviewView{
GeneratedAt: now,
Nodes: make([]DashboardNodeHealth, 0, len(nodes)),
@@ -100,7 +105,7 @@ func GetDashboardOverview() (*DashboardOverviewView, error) {
Trends: DashboardTrends{
Traffic24h: buildTrafficTrendPoints(now, reports),
Capacity24h: buildCapacityTrendPoints(now, snapshots),
Network24h: buildNetworkTrendPoints(now, snapshots),
Network24h: buildNetworkTrendPoints(now, snapshots, openrestySnapshots),
DiskIO24h: buildDiskIOTrendPoints(now, snapshots),
},
}
@@ -1340,13 +1340,13 @@ func TestOpenRestyProxyRequestBufferingDefaultsToOff(t *testing.T) {
func setupServiceTestDB(t *testing.T) {
t.Helper()
nodeAgentTokenCache.reset()
nodeAccessTokenCache.reset()
common.SQLitePath = filepath.Join(t.TempDir(), "service.db")
if err := model.InitDB(); err != nil {
t.Fatalf("failed to init db: %v", err)
}
t.Cleanup(func() {
nodeAgentTokenCache.reset()
nodeAccessTokenCache.reset()
if err := model.CloseDB(); err != nil {
t.Fatalf("failed to close db: %v", err)
}
+37 -39
View File
@@ -57,9 +57,9 @@ type NodeBootstrapView struct {
}
type AgentRegistrationResponse struct {
NodeID string `json:"node_id"`
AgentToken string `json:"agent_token"`
Name string `json:"name"`
NodeID string `json:"node_id"`
AccessToken string `json:"access_token"`
Name string `json:"name"`
}
func CreateNode(input NodeInput) (*NodeView, error) {
@@ -76,8 +76,8 @@ func CreateNode(input NodeInput) (*NodeView, error) {
GeoLatitude: geoLatitude,
GeoLongitude: geoLongitude,
GeoManualOverride: geoManualOverride,
AgentVersion: "",
NginxVersion: "",
Version: "",
ExtVersion: "",
Status: NodeStatusPending,
AutoUpdateEnabled: input.AutoUpdateEnabled,
NodeType: normalizeNodeType(input.NodeType),
@@ -86,7 +86,7 @@ func CreateNode(input NodeInput) (*NodeView, error) {
if err != nil {
return nil, err
}
node.AgentToken, err = newRandomToken()
node.AccessToken, err = newRandomToken()
if err != nil {
return nil, err
}
@@ -110,7 +110,7 @@ func CreateNode(input NodeInput) (*NodeView, error) {
}
return nil, err
}
refreshAgentTokenCache(node)
refreshAccessTokenCache(node)
slog.Info("node created", "name", node.Name, "node_id", node.NodeID)
return buildNodeView(node), nil
}
@@ -150,7 +150,7 @@ func UpdateNode(id uint, input NodeInput) (*NodeView, error) {
if err = node.Update(); err != nil {
return nil, err
}
refreshAgentTokenCache(node)
refreshAccessTokenCache(node)
slog.Info("node updated", "name", node.Name, "node_id", node.NodeID)
return buildNodeView(node), nil
}
@@ -164,7 +164,7 @@ func DeleteNode(id uint) error {
if err := node.Delete(); err != nil {
return err
}
invalidateAgentTokenCache(node.AgentToken)
invalidateAccessTokenCache(node.AccessToken)
DisconnectAgentWSClient(node.NodeID)
return nil
}
@@ -206,7 +206,7 @@ func RequestNodeAgentUpdate(id uint, input NodeAgentUpdateInput) (*NodeView, err
if err = model.DB.Model(node).Select("update_requested", "update_channel", "update_tag").Updates(node).Error; err != nil {
return nil, err
}
refreshAgentTokenCache(node)
refreshAccessTokenCache(node)
if SendAgentWSSettings(node.NodeID, buildAgentSettings(node, true, channel.String(), tagName, node.RestartOpenrestyRequested)) {
slog.Debug("agent manual update pushed via ws", "node_id", node.NodeID, "channel", channel.String(), "tag", tagName)
} else {
@@ -225,7 +225,7 @@ func RequestNodeOpenrestyRestart(id uint) (*NodeView, error) {
if err = model.DB.Model(node).Select("restart_openresty_requested").Updates(node).Error; err != nil {
return nil, err
}
refreshAgentTokenCache(node)
refreshAccessTokenCache(node)
slog.Info("openresty restart requested", "node_id", node.NodeID, "name", node.Name)
return buildNodeView(node), nil
}
@@ -246,12 +246,12 @@ func RequestNodeForceSync(id uint) (*NodeView, error) {
return buildNodeView(node), nil
}
func AuthenticateAgentToken(token string) (*model.Node, error) {
func AuthenticateAccessToken(token string) (*model.Node, error) {
token = strings.TrimSpace(token)
if token == "" {
return nil, errors.New("缺少 Agent Token")
}
return authenticateAgentTokenWithCache(token)
return authenticateAccessTokenWithCache(token)
}
func ValidateDiscoveryToken(token string) error {
@@ -323,12 +323,12 @@ func buildNodeView(node *model.Node) *NodeView {
GeoLatitude: node.GeoLatitude,
GeoLongitude: node.GeoLongitude,
GeoManualOverride: node.GeoManualOverride,
AgentToken: node.AgentToken,
AccessToken: node.AccessToken,
UpdateChannel: strings.TrimSpace(node.UpdateChannel),
UpdateTag: strings.TrimSpace(node.UpdateTag),
RestartOpenrestyRequested: node.RestartOpenrestyRequested,
AgentVersion: node.AgentVersion,
NginxVersion: node.NginxVersion,
Version: node.Version,
ExtVersion: node.ExtVersion,
OpenrestyStatus: normalizeOpenrestyStatus(node.OpenrestyStatus),
OpenrestyMessage: strings.TrimSpace(node.OpenrestyMessage),
Status: status,
@@ -353,10 +353,8 @@ func buildNodeView(node *model.Node) *NodeView {
view.RelayClientAccessAddr = node.RelayClientAccessAddr
view.RelayClientProxyURL = node.RelayClientProxyURL
view.RelayStatus = node.RelayStatus
view.RelayFrpVersion = node.RelayFrpVersion
view.RelayVersion = node.RelayVersion
view.RelayFrpsConnections = node.RelayFrpsConnections
view.RelayFrpsProxyCount = node.RelayFrpsProxyCount
view.Version = node.Version
view.ExtVersion = node.ExtVersion
return view
}
@@ -455,7 +453,7 @@ func isPublicNodeIP(raw string) bool {
}
func buildNodeAgentReleaseView(node *model.Node, release *githubReleaseResponse, channel ReleaseChannel) *NodeAgentReleaseInfo {
currentVersion := strings.TrimSpace(node.AgentVersion)
currentVersion := strings.TrimSpace(node.Version)
view := &NodeAgentReleaseInfo{
CurrentVersion: currentVersion,
Channel: channel.String(),
@@ -475,7 +473,7 @@ func buildNodeAgentReleaseView(node *model.Node, release *githubReleaseResponse,
return view
}
func RegisterNodeWithAgentToken(node *model.Node, payload AgentNodePayload) (*AgentRegistrationResponse, error) {
func RegisterNodeWithAccessToken(node *model.Node, payload AgentNodePayload) (*AgentRegistrationResponse, error) {
payload = normalizeAgentNodePayload(payload)
if node == nil {
return nil, errors.New("节点不存在")
@@ -487,12 +485,12 @@ func RegisterNodeWithAgentToken(node *model.Node, payload AgentNodePayload) (*Ag
if err := node.Update(); err != nil {
return nil, err
}
refreshAgentTokenCache(node)
refreshAccessTokenCache(node)
slog.Info("agent register succeeded on reserved node", "node_id", node.NodeID, "name", node.Name)
return &AgentRegistrationResponse{
NodeID: node.NodeID,
AgentToken: node.AgentToken,
Name: node.Name,
NodeID: node.NodeID,
AccessToken: node.AccessToken,
Name: node.Name,
}, nil
}
@@ -514,9 +512,9 @@ func RegisterNodeWithDiscovery(payload AgentNodePayload) (*AgentRegistrationResp
nodeName = nodeID
}
node := &model.Node{
NodeID: nodeID,
Name: nodeName,
AgentToken: agentToken,
NodeID: nodeID,
Name: nodeName,
AccessToken: agentToken,
}
applyNodeRuntime(node, payload, false)
if err = node.Insert(); err != nil {
@@ -525,20 +523,20 @@ func RegisterNodeWithDiscovery(payload AgentNodePayload) (*AgentRegistrationResp
}
return nil, err
}
refreshAgentTokenCache(node)
refreshAccessTokenCache(node)
slog.Info("agent discovery register succeeded", "node_id", node.NodeID, "name", node.Name)
return &AgentRegistrationResponse{
NodeID: node.NodeID,
AgentToken: node.AgentToken,
Name: node.Name,
NodeID: node.NodeID,
AccessToken: node.AccessToken,
Name: node.Name,
}, nil
}
func normalizeAgentNodePayload(payload AgentNodePayload) AgentNodePayload {
payload.Name = strings.TrimSpace(payload.Name)
payload.IP = strings.TrimSpace(payload.IP)
payload.AgentVersion = strings.TrimSpace(payload.AgentVersion)
payload.NginxVersion = strings.TrimSpace(payload.NginxVersion)
payload.Version = strings.TrimSpace(payload.Version)
payload.ExtVersion = strings.TrimSpace(payload.ExtVersion)
payload.CurrentVersion = strings.TrimSpace(payload.CurrentVersion)
payload.LastError = truncateForDatabase(payload.LastError, 16000)
payload.OpenrestyStatus = normalizeOpenrestyStatus(payload.OpenrestyStatus)
@@ -553,8 +551,8 @@ func validateAgentNodePayload(payload AgentNodePayload) error {
if net.ParseIP(payload.IP) == nil {
return errors.New("ip 格式无效")
}
if payload.AgentVersion == "" {
return errors.New("agent_version 不能为空")
if payload.Version == "" {
return errors.New("version 不能为空")
}
return nil
}
@@ -568,8 +566,8 @@ func applyNodeRuntime(node *model.Node, payload AgentNodePayload, preserveName b
if !node.IPManualOverride {
node.IP = strings.TrimSpace(payload.IP)
}
node.AgentVersion = strings.TrimSpace(payload.AgentVersion)
node.NginxVersion = strings.TrimSpace(payload.NginxVersion)
node.Version = strings.TrimSpace(payload.Version)
node.ExtVersion = strings.TrimSpace(payload.ExtVersion)
node.OpenrestyStatus = normalizeOpenrestyStatus(payload.OpenrestyStatus)
node.OpenrestyMessage = truncateForDatabase(payload.OpenrestyMessage, 16000)
node.Status = NodeStatusOnline
@@ -20,31 +20,31 @@ type cachedAgentNode struct {
expiresAt time.Time
}
type cachedMissingAgentToken struct {
type cachedMissingAccessToken struct {
expiresAt time.Time
}
type agentTokenAuthCache struct {
positive *ristretto.Cache[string, cachedAgentNode]
negative *ristretto.Cache[string, cachedMissingAgentToken]
negative *ristretto.Cache[string, cachedMissingAccessToken]
now func() time.Time
loadNodeByToken func(string) (*model.Node, error)
}
var nodeAgentTokenCache = newAgentTokenAuthCache()
var nodeAccessTokenCache = newAccessTokenAuthCache()
func newAgentTokenAuthCache() *agentTokenAuthCache {
func newAccessTokenAuthCache() *agentTokenAuthCache {
return &agentTokenAuthCache{
positive: mustNewAgentTokenPositiveCache(),
negative: mustNewAgentTokenNegativeCache(),
positive: mustNewAccessTokenPositiveCache(),
negative: mustNewAccessTokenNegativeCache(),
now: time.Now,
loadNodeByToken: func(token string) (*model.Node, error) {
return model.GetNodeByAgentToken(token)
return model.GetNodeByAccessToken(token)
},
}
}
func mustNewAgentTokenPositiveCache() *ristretto.Cache[string, cachedAgentNode] {
func mustNewAccessTokenPositiveCache() *ristretto.Cache[string, cachedAgentNode] {
cache, err := ristretto.NewCache(&ristretto.Config[string, cachedAgentNode]{
NumCounters: 1e5,
MaxCost: 2e4,
@@ -56,8 +56,8 @@ func mustNewAgentTokenPositiveCache() *ristretto.Cache[string, cachedAgentNode]
return cache
}
func mustNewAgentTokenNegativeCache() *ristretto.Cache[string, cachedMissingAgentToken] {
cache, err := ristretto.NewCache(&ristretto.Config[string, cachedMissingAgentToken]{
func mustNewAccessTokenNegativeCache() *ristretto.Cache[string, cachedMissingAccessToken] {
cache, err := ristretto.NewCache(&ristretto.Config[string, cachedMissingAccessToken]{
NumCounters: 1e5,
MaxCost: agentTokenNegativeCacheCap,
BufferItems: 64,
@@ -130,7 +130,7 @@ func (c *agentTokenAuthCache) storeMissing(token string, expiresAt time.Time) {
return
}
c.positive.Del(token)
c.negative.Set(token, cachedMissingAgentToken{
c.negative.Set(token, cachedMissingAccessToken{
expiresAt: expiresAt,
}, 1)
c.negative.Wait()
@@ -157,21 +157,21 @@ func cloneCachedNode(node *model.Node) *model.Node {
return &cloned
}
func authenticateAgentTokenWithCache(token string) (*model.Node, error) {
return nodeAgentTokenCache.authenticate(token)
func authenticateAccessTokenWithCache(token string) (*model.Node, error) {
return nodeAccessTokenCache.authenticate(token)
}
func refreshAgentTokenCache(node *model.Node) {
func refreshAccessTokenCache(node *model.Node) {
if node == nil {
return
}
nodeAgentTokenCache.storeNode(
node.AgentToken,
nodeAccessTokenCache.storeNode(
node.AccessToken,
node,
nodeAgentTokenCache.now().Add(agentTokenPositiveCacheTTL),
nodeAccessTokenCache.now().Add(agentTokenPositiveCacheTTL),
)
}
func invalidateAgentTokenCache(token string) {
nodeAgentTokenCache.invalidate(token)
func invalidateAccessTokenCache(token string) {
nodeAccessTokenCache.invalidate(token)
}
@@ -10,8 +10,8 @@ import (
"gorm.io/gorm"
)
func TestAgentTokenAuthCacheUsesPositiveCacheUntilLogicalExpiry(t *testing.T) {
cache := newAgentTokenAuthCache()
func TestAccessTokenAuthCacheUsesPositiveCacheUntilLogicalExpiry(t *testing.T) {
cache := newAccessTokenAuthCache()
cache.reset()
baseTime := time.Date(2026, 3, 14, 16, 0, 0, 0, time.UTC)
currentTime := baseTime
@@ -23,9 +23,9 @@ func TestAgentTokenAuthCacheUsesPositiveCacheUntilLogicalExpiry(t *testing.T) {
cache.loadNodeByToken = func(token string) (*model.Node, error) {
loadCount++
return &model.Node{
NodeID: fmt.Sprintf("node-%d", loadCount),
Name: "edge",
AgentToken: token,
NodeID: fmt.Sprintf("node-%d", loadCount),
Name: "edge",
AccessToken: token,
}, nil
}
@@ -61,8 +61,8 @@ func TestAgentTokenAuthCacheUsesPositiveCacheUntilLogicalExpiry(t *testing.T) {
}
}
func TestAgentTokenAuthCacheRefreshesAfterMissingEntryExpires(t *testing.T) {
cache := newAgentTokenAuthCache()
func TestAccessTokenAuthCacheRefreshesAfterMissingEntryExpires(t *testing.T) {
cache := newAccessTokenAuthCache()
cache.reset()
baseTime := time.Date(2026, 3, 14, 16, 30, 0, 0, time.UTC)
currentTime := baseTime
@@ -77,9 +77,9 @@ func TestAgentTokenAuthCacheRefreshesAfterMissingEntryExpires(t *testing.T) {
return nil, gorm.ErrRecordNotFound
}
return &model.Node{
NodeID: "node-recovered",
Name: "edge",
AgentToken: token,
NodeID: "node-recovered",
Name: "edge",
AccessToken: token,
}, nil
}
+16 -5
View File
@@ -101,6 +101,7 @@ func GetNodeObservability(id uint, query NodeObservabilityQuery) (*NodeObservabi
if err != nil {
return nil, err
}
trendOpenresty, _ := model.ListNodeObservationOpenresty(node.NodeID, now.Add(-24*time.Hour), 0)
trendReports, err := model.ListNodeRequestReports(node.NodeID, now.Add(-24*time.Hour), 0)
if err != nil {
return nil, err
@@ -124,21 +125,31 @@ func GetNodeObservability(id uint, query NodeObservabilityQuery) (*NodeObservabi
Trends: NodeObservabilityTrends{
Traffic24h: buildTrafficTrendPoints(now, trendReports),
Capacity24h: buildCapacityTrendPoints(now, trendSnapshots),
Network24h: buildNetworkTrendPoints(now, trendSnapshots),
Network24h: buildNetworkTrendPoints(now, trendSnapshots, trendOpenresty),
DiskIO24h: buildDiskIOTrendPoints(now, trendSnapshots),
},
}
if node.NodeType == "tunnel_relay" {
view.RelayDashboard = buildRelayDashboardSnapshot(node)
frpsObs, _ := model.ListNodeObservationFrps(node.NodeID, time.Time{}, 1)
var latestFrps *model.NodeObservationFrps
if len(frpsObs) > 0 {
latestFrps = frpsObs[0]
}
view.RelayDashboard = buildRelayDashboardSnapshot(node, latestFrps)
}
return view, nil
}
func buildRelayDashboardSnapshot(node *model.Node) *RelayDashboardSnapshot {
func buildRelayDashboardSnapshot(node *model.Node, obs *model.NodeObservationFrps) *RelayDashboardSnapshot {
if node == nil {
return nil
}
totalProxies := node.RelayFrpsProxyCount
totalProxies := 0
totalConnections := 0
if obs != nil {
totalProxies = obs.FrpsProxyCount
totalConnections = obs.FrpsConnections
}
if totalProxies < 0 {
totalProxies = 0
}
@@ -151,7 +162,7 @@ func buildRelayDashboardSnapshot(node *model.Node) *RelayDashboardSnapshot {
OnlineProxies: onlineProxies,
OfflineProxies: totalProxies - onlineProxies,
Proxies: []RelayProxyStat{},
TotalConnections: maxInt(node.RelayFrpsConnections, 0),
TotalConnections: maxInt(totalConnections, 0),
ClientCounts: 0,
}
}
+174 -187
View File
@@ -167,9 +167,9 @@ func TestHeartbeatNodeReturnsPreviewUpdateSettings(t *testing.T) {
NodeID: "node-preview-1",
Name: "preview-edge-1",
IP: "10.0.0.8",
AgentToken: "agent-token",
AgentVersion: "v0.4.0",
NginxVersion: "1.27.1.2",
AccessToken: "agent-token",
Version: "v0.4.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
UpdateRequested: true,
UpdateChannel: "preview",
@@ -196,8 +196,8 @@ func TestHeartbeatNodeReturnsPreviewUpdateSettings(t *testing.T) {
NodeID: node.NodeID,
Name: node.Name,
IP: node.IP,
AgentVersion: node.AgentVersion,
NginxVersion: node.NginxVersion,
Version: node.Version,
ExtVersion: node.ExtVersion,
OpenrestyStatus: OpenrestyStatusUnhealthy,
OpenrestyMessage: "port 80 already allocated",
})
@@ -381,24 +381,24 @@ func TestHeartbeatNodeResolvesGeoMetadataFromIPWhenNotManuallyOverridden(t *test
})
node := &model.Node{
NodeID: "node-geo-auto",
Name: "geo-auto",
IP: "10.0.0.8",
AgentToken: "agent-token",
AgentVersion: "v0.4.0",
NginxVersion: "1.27.1.2",
Status: NodeStatusOnline,
NodeID: "node-geo-auto",
Name: "geo-auto",
IP: "10.0.0.8",
AccessToken: "agent-token",
Version: "v0.4.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to seed node: %v", err)
}
resp, err := HeartbeatNode(node, AgentNodePayload{
NodeID: node.NodeID,
Name: node.Name,
IP: "8.8.8.8",
AgentVersion: node.AgentVersion,
NginxVersion: node.NginxVersion,
NodeID: node.NodeID,
Name: node.Name,
IP: "8.8.8.8",
Version: node.Version,
ExtVersion: node.ExtVersion,
})
if err != nil {
t.Fatalf("expected heartbeat to succeed: %v", err)
@@ -430,9 +430,9 @@ func TestHeartbeatNodePreservesManualGeoOverride(t *testing.T) {
GeoLatitude: &latitude,
GeoLongitude: &longitude,
GeoManualOverride: true,
AgentToken: "agent-token",
AgentVersion: "v0.4.0",
NginxVersion: "1.27.1.2",
AccessToken: "agent-token",
Version: "v0.4.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
}
if err := node.Insert(); err != nil {
@@ -440,11 +440,11 @@ func TestHeartbeatNodePreservesManualGeoOverride(t *testing.T) {
}
resp, err := HeartbeatNode(node, AgentNodePayload{
NodeID: node.NodeID,
Name: node.Name,
IP: "8.8.8.8",
AgentVersion: node.AgentVersion,
NginxVersion: node.NginxVersion,
NodeID: node.NodeID,
Name: node.Name,
IP: "8.8.8.8",
Version: node.Version,
ExtVersion: node.ExtVersion,
})
if err != nil {
t.Fatalf("expected heartbeat to succeed: %v", err)
@@ -465,9 +465,9 @@ func TestHeartbeatNodePreservesManualIPOverride(t *testing.T) {
Name: "ip-manual",
IP: "203.0.113.10",
IPManualOverride: true,
AgentToken: "agent-token",
AgentVersion: "v0.4.0",
NginxVersion: "1.27.1.2",
AccessToken: "agent-token",
Version: "v0.4.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
}
if err := node.Insert(); err != nil {
@@ -478,8 +478,8 @@ func TestHeartbeatNodePreservesManualIPOverride(t *testing.T) {
NodeID: node.NodeID,
Name: node.Name,
IP: "10.0.0.8",
AgentVersion: "v0.5.0",
NginxVersion: "1.27.1.3",
Version: "v0.5.0",
ExtVersion: "1.27.1.3",
OpenrestyStatus: OpenrestyStatusHealthy,
})
if err != nil {
@@ -488,7 +488,7 @@ func TestHeartbeatNodePreservesManualIPOverride(t *testing.T) {
if resp.Node.IP != "203.0.113.10" {
t.Fatalf("expected manual ip to be preserved, got %s", resp.Node.IP)
}
if resp.Node.AgentVersion != "v0.5.0" || resp.Node.NginxVersion != "1.27.1.3" {
if resp.Node.Version != "v0.5.0" || resp.Node.ExtVersion != "1.27.1.3" {
t.Fatalf("expected runtime metadata to update despite locked ip, got %+v", resp.Node)
}
@@ -505,24 +505,24 @@ func TestHeartbeatNodeUpdatesIPWhenManualOverrideDisabled(t *testing.T) {
setupServiceTestDB(t)
node := &model.Node{
NodeID: "node-ip-auto",
Name: "ip-auto",
IP: "10.0.0.8",
AgentToken: "agent-token",
AgentVersion: "v0.4.0",
NginxVersion: "1.27.1.2",
Status: NodeStatusOnline,
NodeID: "node-ip-auto",
Name: "ip-auto",
IP: "10.0.0.8",
AccessToken: "agent-token",
Version: "v0.4.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to seed node: %v", err)
}
resp, err := HeartbeatNode(node, AgentNodePayload{
NodeID: node.NodeID,
Name: node.Name,
IP: "8.8.8.8",
AgentVersion: node.AgentVersion,
NginxVersion: node.NginxVersion,
NodeID: node.NodeID,
Name: node.Name,
IP: "8.8.8.8",
Version: node.Version,
ExtVersion: node.ExtVersion,
})
if err != nil {
t.Fatalf("expected heartbeat to succeed: %v", err)
@@ -557,11 +557,11 @@ func TestUpdateNodeCanLockAndUnlockManualIP(t *testing.T) {
t.Fatalf("failed to reload node: %v", err)
}
if _, err = HeartbeatNode(stored, AgentNodePayload{
NodeID: stored.NodeID,
Name: stored.Name,
IP: "8.8.8.8",
AgentVersion: "v0.5.0",
NginxVersion: "1.27.1.3",
NodeID: stored.NodeID,
Name: stored.Name,
IP: "8.8.8.8",
Version: "v0.5.0",
ExtVersion: "1.27.1.3",
}); err != nil {
t.Fatalf("expected heartbeat to succeed: %v", err)
}
@@ -591,11 +591,11 @@ func TestUpdateNodeCanLockAndUnlockManualIP(t *testing.T) {
t.Fatalf("failed to reload unlocked node: %v", err)
}
if _, err = HeartbeatNode(unlockedStored, AgentNodePayload{
NodeID: unlockedStored.NodeID,
Name: unlockedStored.Name,
IP: "8.8.4.4",
AgentVersion: "v0.5.1",
NginxVersion: "1.27.1.4",
NodeID: unlockedStored.NodeID,
Name: unlockedStored.Name,
IP: "8.8.4.4",
Version: "v0.5.1",
ExtVersion: "1.27.1.4",
}); err != nil {
t.Fatalf("expected heartbeat to succeed after unlock: %v", err)
}
@@ -666,25 +666,25 @@ func TestListNodeViewsIncludesLatestApplyLogsForMultipleNodes(t *testing.T) {
now := time.Now()
nodes := []*model.Node{
{
NodeID: "node-a",
Name: "edge-a",
IP: "10.0.0.11",
GeoName: "Shanghai",
AgentToken: "token-a",
AgentVersion: "v0.5.0",
NginxVersion: "1.27.1.2",
Status: NodeStatusOnline,
LastSeenAt: now,
NodeID: "node-a",
Name: "edge-a",
IP: "10.0.0.11",
GeoName: "Shanghai",
AccessToken: "token-a",
Version: "v0.5.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
LastSeenAt: now,
},
{
NodeID: "node-b",
Name: "edge-b",
IP: "10.0.0.12",
AgentToken: "token-b",
AgentVersion: "v0.5.0",
NginxVersion: "1.27.1.2",
Status: NodeStatusOnline,
LastSeenAt: now,
NodeID: "node-b",
Name: "edge-b",
IP: "10.0.0.12",
AccessToken: "token-b",
Version: "v0.5.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
LastSeenAt: now,
},
}
for _, node := range nodes {
@@ -732,8 +732,8 @@ func TestCollectNodeHeartbeatChangesOnlyReturnsChangedFields(t *testing.T) {
before := &model.Node{
Name: "edge-1",
IP: "10.0.0.8",
AgentVersion: "v0.5.0",
NginxVersion: "1.27.1.2",
Version: "v0.5.0",
ExtVersion: "1.27.1.2",
OpenrestyStatus: OpenrestyStatusHealthy,
OpenrestyMessage: "",
Status: NodeStatusOnline,
@@ -748,8 +748,8 @@ func TestCollectNodeHeartbeatChangesOnlyReturnsChangedFields(t *testing.T) {
after := &model.Node{
Name: "edge-1",
IP: "10.0.0.8",
AgentVersion: "v0.5.0",
NginxVersion: "1.27.1.2",
Version: "v0.5.0",
ExtVersion: "1.27.1.2",
OpenrestyStatus: OpenrestyStatusHealthy,
OpenrestyMessage: "",
Status: NodeStatusOnline,
@@ -790,14 +790,14 @@ func TestListNodeViewsDoesNotPersistComputedStatus(t *testing.T) {
setupServiceTestDB(t)
node := &model.Node{
NodeID: "node-offline-view",
Name: "edge-offline",
IP: "10.0.0.21",
AgentToken: "token-offline",
AgentVersion: "v0.5.0",
NginxVersion: "1.27.1.2",
Status: NodeStatusOnline,
LastSeenAt: time.Now().Add(-common.NodeOfflineThreshold - time.Minute),
NodeID: "node-offline-view",
Name: "edge-offline",
IP: "10.0.0.21",
AccessToken: "token-offline",
Version: "v0.5.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
LastSeenAt: time.Now().Add(-common.NodeOfflineThreshold - time.Minute),
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to insert node: %v", err)
@@ -868,24 +868,24 @@ func TestHeartbeatNodePersistsObservabilityPayload(t *testing.T) {
})
node := &model.Node{
NodeID: "node-observe-1",
Name: "observe-edge-1",
IP: "10.0.0.31",
AgentToken: "token-observe",
AgentVersion: "v0.6.0",
NginxVersion: "1.27.1.2",
Status: NodeStatusOnline,
NodeID: "node-observe-1",
Name: "observe-edge-1",
IP: "10.0.0.31",
AccessToken: "token-observe",
Version: "v0.6.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to seed node: %v", err)
}
_, err := HeartbeatNode(node, AgentNodePayload{
NodeID: node.NodeID,
Name: node.Name,
IP: node.IP,
AgentVersion: node.AgentVersion,
NginxVersion: node.NginxVersion,
NodeID: node.NodeID,
Name: node.Name,
IP: node.IP,
Version: node.Version,
ExtVersion: node.ExtVersion,
Profile: &AgentNodeSystemProfile{
Hostname: "observe-edge-1",
OSName: "Ubuntu",
@@ -900,17 +900,16 @@ func TestHeartbeatNodePersistsObservabilityPayload(t *testing.T) {
ReportedAtUnix: time.Now().Add(-time.Minute).Unix(),
},
Snapshot: &AgentNodeMetricSnapshot{
CapturedAtUnix: time.Now().Add(-30 * time.Second).Unix(),
CPUUsagePercent: 42.5,
MemoryUsedBytes: 8 * 1024 * 1024 * 1024,
MemoryTotalBytes: 16 * 1024 * 1024 * 1024,
StorageUsedBytes: 70 * 1024 * 1024 * 1024,
StorageTotalBytes: 200 * 1024 * 1024 * 1024,
DiskReadBytes: 1024,
DiskWriteBytes: 2048,
NetworkRxBytes: 4096,
NetworkTxBytes: 8192,
OpenrestyConnections: 128,
CapturedAtUnix: time.Now().Add(-30 * time.Second).Unix(),
CPUUsagePercent: 42.5,
MemoryUsedBytes: 8 * 1024 * 1024 * 1024,
MemoryTotalBytes: 16 * 1024 * 1024 * 1024,
StorageUsedBytes: 70 * 1024 * 1024 * 1024,
StorageTotalBytes: 200 * 1024 * 1024 * 1024,
DiskReadBytes: 1024,
DiskWriteBytes: 2048,
NetworkRxBytes: 4096,
NetworkTxBytes: 8192,
},
TrafficReport: &AgentNodeTrafficReport{
WindowStartedAtUnix: time.Now().Add(-time.Minute).Unix(),
@@ -966,7 +965,7 @@ func TestHeartbeatNodePersistsObservabilityPayload(t *testing.T) {
if err != nil {
t.Fatalf("expected node snapshots query to succeed: %v", err)
}
if len(snapshots) != 1 || snapshots[0].OpenrestyConnections != 128 {
if len(snapshots) != 1 {
t.Fatalf("unexpected metric snapshots: %+v", snapshots)
}
@@ -1021,13 +1020,13 @@ func TestHeartbeatNodePersistsBufferedObservabilityPayload(t *testing.T) {
})
node := &model.Node{
NodeID: "node-observe-buffered",
Name: "observe-buffered-edge",
IP: "10.0.0.32",
AgentToken: "token-observe-buffered",
AgentVersion: "v0.6.0",
NginxVersion: "1.27.1.2",
Status: NodeStatusOnline,
NodeID: "node-observe-buffered",
Name: "observe-buffered-edge",
IP: "10.0.0.32",
AccessToken: "token-observe-buffered",
Version: "v0.6.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to seed node: %v", err)
@@ -1035,11 +1034,11 @@ func TestHeartbeatNodePersistsBufferedObservabilityPayload(t *testing.T) {
now := time.Now().UTC()
_, err := HeartbeatNode(node, AgentNodePayload{
NodeID: node.NodeID,
Name: node.Name,
IP: node.IP,
AgentVersion: node.AgentVersion,
NginxVersion: node.NginxVersion,
NodeID: node.NodeID,
Name: node.Name,
IP: node.IP,
Version: node.Version,
ExtVersion: node.ExtVersion,
Snapshot: &AgentNodeMetricSnapshot{
CapturedAtUnix: now.Unix(),
CPUUsagePercent: 25,
@@ -1124,11 +1123,11 @@ func TestHeartbeatNodePersistsBufferedObservabilityPayload(t *testing.T) {
}
_, err = HeartbeatNode(node, AgentNodePayload{
NodeID: node.NodeID,
Name: node.Name,
IP: node.IP,
AgentVersion: node.AgentVersion,
NginxVersion: node.NginxVersion,
NodeID: node.NodeID,
Name: node.Name,
IP: node.IP,
Version: node.Version,
ExtVersion: node.ExtVersion,
BufferedObservability: []AgentBufferedObservabilityRecord{
{
WindowStartedAtUnix: now.Add(-2 * time.Minute).Unix(),
@@ -1196,13 +1195,13 @@ func TestListAccessLogsUsesPagination(t *testing.T) {
setupServiceTestDB(t)
node := &model.Node{
NodeID: "node-access-log-page",
Name: "access-log-edge",
IP: "10.0.0.40",
AgentToken: "token-access-log-page",
AgentVersion: "v0.6.0",
NginxVersion: "1.27.1.2",
Status: NodeStatusOnline,
NodeID: "node-access-log-page",
Name: "access-log-edge",
IP: "10.0.0.40",
AccessToken: "token-access-log-page",
Version: "v0.6.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to seed node: %v", err)
@@ -1281,24 +1280,24 @@ func TestHeartbeatNodeResolvesMissingHealthEvents(t *testing.T) {
setupServiceTestDB(t)
node := &model.Node{
NodeID: "node-event-1",
Name: "event-edge-1",
IP: "10.0.0.41",
AgentToken: "token-event",
AgentVersion: "v0.6.0",
NginxVersion: "1.27.1.2",
Status: NodeStatusOnline,
NodeID: "node-event-1",
Name: "event-edge-1",
IP: "10.0.0.41",
AccessToken: "token-event",
Version: "v0.6.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to seed node: %v", err)
}
_, err := HeartbeatNode(node, AgentNodePayload{
NodeID: node.NodeID,
Name: node.Name,
IP: node.IP,
AgentVersion: node.AgentVersion,
NginxVersion: node.NginxVersion,
NodeID: node.NodeID,
Name: node.Name,
IP: node.IP,
Version: node.Version,
ExtVersion: node.ExtVersion,
HealthEvents: []AgentNodeHealthEvent{
{
EventType: "sync_error",
@@ -1316,8 +1315,8 @@ func TestHeartbeatNodeResolvesMissingHealthEvents(t *testing.T) {
NodeID: node.NodeID,
Name: node.Name,
IP: node.IP,
AgentVersion: node.AgentVersion,
NginxVersion: node.NginxVersion,
Version: node.Version,
ExtVersion: node.ExtVersion,
HealthEvents: []AgentNodeHealthEvent{},
})
if err != nil {
@@ -1345,13 +1344,13 @@ func TestGetNodeObservability(t *testing.T) {
setupServiceTestDB(t)
node := &model.Node{
NodeID: "node-observability-query",
Name: "query-edge",
IP: "10.0.0.61",
AgentToken: "token-query",
AgentVersion: "v0.6.0",
NginxVersion: "1.27.1.2",
Status: NodeStatusOnline,
NodeID: "node-observability-query",
Name: "query-edge",
IP: "10.0.0.61",
AccessToken: "token-query",
Version: "v0.6.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to insert node: %v", err)
@@ -1377,8 +1376,6 @@ func TestGetNodeObservability(t *testing.T) {
DiskWriteBytes: 0,
NetworkRxBytes: 2048,
NetworkTxBytes: 4096,
OpenrestyRxBytes: 8192,
OpenrestyTxBytes: 16384,
}).Insert(); err != nil {
t.Fatalf("failed to insert node metric baseline snapshot: %v", err)
}
@@ -1394,8 +1391,6 @@ func TestGetNodeObservability(t *testing.T) {
DiskWriteBytes: 2048,
NetworkRxBytes: 4096,
NetworkTxBytes: 8192,
OpenrestyRxBytes: 16384,
OpenrestyTxBytes: 32768,
}).Insert(); err != nil {
t.Fatalf("failed to insert node metric snapshot: %v", err)
}
@@ -1453,7 +1448,7 @@ func TestGetNodeObservability(t *testing.T) {
if view.Trends.Traffic24h[len(view.Trends.Traffic24h)-1].RequestCount != 123 {
t.Fatalf("unexpected traffic trend tail: %+v", view.Trends.Traffic24h[len(view.Trends.Traffic24h)-1])
}
if view.Trends.Network24h[len(view.Trends.Network24h)-1].OpenrestyTxBytes != 32768 {
if view.Trends.Network24h[len(view.Trends.Network24h)-1].NetworkTxBytes != 8192 {
t.Fatalf("unexpected network trend tail: %+v", view.Trends.Network24h[len(view.Trends.Network24h)-1])
}
if view.Trends.DiskIO24h[len(view.Trends.DiskIO24h)-1].DiskWriteBytes != 2048 {
@@ -1477,13 +1472,13 @@ func TestGetNodeObservabilityAllowsMissingProfile(t *testing.T) {
setupServiceTestDB(t)
node := &model.Node{
NodeID: "node-observability-empty",
Name: "empty-edge",
IP: "10.0.0.62",
AgentToken: "token-empty",
AgentVersion: "v0.6.0",
NginxVersion: "1.27.1.2",
Status: NodeStatusOnline,
NodeID: "node-observability-empty",
Name: "empty-edge",
IP: "10.0.0.62",
AccessToken: "token-empty",
Version: "v0.6.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to insert node: %v", err)
@@ -1508,13 +1503,13 @@ func TestCleanupNodeHealthEvents(t *testing.T) {
setupServiceTestDB(t)
node := &model.Node{
NodeID: "node-health-cleanup",
Name: "health-cleanup-edge",
IP: "10.0.0.72",
AgentToken: "token-health-cleanup",
AgentVersion: "v0.6.0",
NginxVersion: "1.27.1.2",
Status: NodeStatusOnline,
NodeID: "node-health-cleanup",
Name: "health-cleanup-edge",
IP: "10.0.0.72",
AccessToken: "token-health-cleanup",
Version: "v0.6.0",
ExtVersion: "1.27.1.2",
Status: NodeStatusOnline,
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to insert node: %v", err)
@@ -1586,9 +1581,9 @@ func TestGetDashboardOverview(t *testing.T) {
Name: "edge-a",
IP: "10.0.0.71",
GeoName: "Shanghai",
AgentToken: "token-a",
AgentVersion: "v0.6.0",
NginxVersion: "1.27.1.2",
AccessToken: "token-a",
Version: "v0.6.0",
ExtVersion: "1.27.1.2",
OpenrestyStatus: OpenrestyStatusHealthy,
Status: NodeStatusOnline,
CurrentVersion: "20260314-001",
@@ -1599,9 +1594,9 @@ func TestGetDashboardOverview(t *testing.T) {
Name: "edge-b",
IP: "10.0.0.72",
GeoName: "San Francisco",
AgentToken: "token-b",
AgentVersion: "v0.6.0",
NginxVersion: "1.27.1.2",
AccessToken: "token-b",
Version: "v0.6.0",
ExtVersion: "1.27.1.2",
OpenrestyStatus: OpenrestyStatusUnhealthy,
Status: NodeStatusOnline,
CurrentVersion: "20260313-001",
@@ -1626,8 +1621,6 @@ func TestGetDashboardOverview(t *testing.T) {
DiskWriteBytes: 0,
NetworkRxBytes: 100,
NetworkTxBytes: 150,
OpenrestyRxBytes: 300,
OpenrestyTxBytes: 450,
}).Insert(); err != nil {
t.Fatalf("failed to insert node a baseline metric snapshot: %v", err)
}
@@ -1643,8 +1636,6 @@ func TestGetDashboardOverview(t *testing.T) {
DiskWriteBytes: 150,
NetworkRxBytes: 300,
NetworkTxBytes: 500,
OpenrestyRxBytes: 700,
OpenrestyTxBytes: 900,
}).Insert(); err != nil {
t.Fatalf("failed to insert node a metric snapshot: %v", err)
}
@@ -1659,9 +1650,7 @@ func TestGetDashboardOverview(t *testing.T) {
DiskReadBytes: 0,
DiskWriteBytes: 0,
NetworkRxBytes: 200,
NetworkTxBytes: 300,
OpenrestyRxBytes: 500,
OpenrestyTxBytes: 700,
NetworkTxBytes: 400,
}).Insert(); err != nil {
t.Fatalf("failed to insert node b baseline metric snapshot: %v", err)
}
@@ -1676,9 +1665,7 @@ func TestGetDashboardOverview(t *testing.T) {
DiskReadBytes: 200,
DiskWriteBytes: 400,
NetworkRxBytes: 600,
NetworkTxBytes: 900,
OpenrestyRxBytes: 1200,
OpenrestyTxBytes: 1600,
NetworkTxBytes: 800,
}).Insert(); err != nil {
t.Fatalf("failed to insert node b metric snapshot: %v", err)
}
@@ -1785,7 +1772,7 @@ func TestGetDashboardOverview(t *testing.T) {
if view.Trends.Traffic24h[len(view.Trends.Traffic24h)-1].RequestCount != 900 {
t.Fatalf("unexpected dashboard traffic trend tail: %+v", view.Trends.Traffic24h[len(view.Trends.Traffic24h)-1])
}
if view.Trends.Network24h[len(view.Trends.Network24h)-1].OpenrestyRxBytes != 1900 {
if view.Trends.Network24h[len(view.Trends.Network24h)-1].NetworkRxBytes != 900 {
t.Fatalf("unexpected dashboard network trend tail: %+v", view.Trends.Network24h[len(view.Trends.Network24h)-1])
}
if view.Trends.DiskIO24h[len(view.Trends.DiskIO24h)-1].DiskWriteBytes != 550 {
+53 -31
View File
@@ -36,19 +36,23 @@ type AgentNodeSystemProfile struct {
}
type AgentNodeMetricSnapshot struct {
CapturedAtUnix int64 `json:"captured_at_unix"`
CPUUsagePercent float64 `json:"cpu_usage_percent"`
MemoryUsedBytes int64 `json:"memory_used_bytes"`
MemoryTotalBytes int64 `json:"memory_total_bytes"`
StorageUsedBytes int64 `json:"storage_used_bytes"`
StorageTotalBytes int64 `json:"storage_total_bytes"`
DiskReadBytes int64 `json:"disk_read_bytes"`
DiskWriteBytes int64 `json:"disk_write_bytes"`
NetworkRxBytes int64 `json:"network_rx_bytes"`
NetworkTxBytes int64 `json:"network_tx_bytes"`
OpenrestyRxBytes int64 `json:"openresty_rx_bytes"`
OpenrestyTxBytes int64 `json:"openresty_tx_bytes"`
OpenrestyConnections int64 `json:"openresty_connections"`
CapturedAtUnix int64 `json:"captured_at_unix"`
CPUUsagePercent float64 `json:"cpu_usage_percent"`
MemoryUsedBytes int64 `json:"memory_used_bytes"`
MemoryTotalBytes int64 `json:"memory_total_bytes"`
StorageUsedBytes int64 `json:"storage_used_bytes"`
StorageTotalBytes int64 `json:"storage_total_bytes"`
DiskReadBytes int64 `json:"disk_read_bytes"`
DiskWriteBytes int64 `json:"disk_write_bytes"`
NetworkRxBytes int64 `json:"network_rx_bytes"`
NetworkTxBytes int64 `json:"network_tx_bytes"`
}
type AgentNodeOpenrestyObservation struct {
CapturedAtUnix int64 `json:"captured_at_unix"`
OpenrestyRxBytes int64 `json:"openresty_rx_bytes"`
OpenrestyTxBytes int64 `json:"openresty_tx_bytes"`
OpenrestyConnections int64 `json:"openresty_connections"`
}
type AgentNodeTrafficReport struct {
@@ -71,10 +75,11 @@ type AgentNodeAccessLog struct {
}
type AgentBufferedObservabilityRecord struct {
WindowStartedAtUnix int64 `json:"window_started_at_unix"`
Snapshot *AgentNodeMetricSnapshot `json:"snapshot,omitempty"`
TrafficReport *AgentNodeTrafficReport `json:"traffic_report,omitempty"`
AccessLogs []AgentNodeAccessLog `json:"access_logs,omitempty"`
WindowStartedAtUnix int64 `json:"window_started_at_unix"`
Snapshot *AgentNodeMetricSnapshot `json:"snapshot,omitempty"`
OpenrestyObservation *AgentNodeOpenrestyObservation `json:"openresty_observation,omitempty"`
TrafficReport *AgentNodeTrafficReport `json:"traffic_report,omitempty"`
AccessLogs []AgentNodeAccessLog `json:"access_logs,omitempty"`
}
type AgentNodeHealthEvent struct {
@@ -103,6 +108,9 @@ func persistHeartbeatObservability(nodeID string, payload AgentNodePayload, repo
if err := persistNodeMetricSnapshot(tx, nodeID, payload.Snapshot, reportedAt); err != nil {
return err
}
if err := persistNodeOpenrestyObservation(tx, nodeID, payload.OpenrestyObservation, reportedAt); err != nil {
return err
}
if err := persistNodeTrafficReport(tx, nodeID, payload.TrafficReport, reportedAt); err != nil {
return err
}
@@ -125,6 +133,9 @@ func persistBufferedObservability(tx *gorm.DB, nodeID string, records []AgentBuf
if err := persistNodeMetricSnapshot(tx, nodeID, record.Snapshot, reportedAt); err != nil {
return err
}
if err := persistNodeOpenrestyObservation(tx, nodeID, record.OpenrestyObservation, reportedAt); err != nil {
return err
}
if err := persistNodeTrafficReport(tx, nodeID, record.TrafficReport, reportedAt); err != nil {
return err
}
@@ -161,20 +172,17 @@ func persistNodeMetricSnapshot(tx *gorm.DB, nodeID string, snapshot *AgentNodeMe
return nil
}
record := &model.NodeMetricSnapshot{
NodeID: nodeID,
CapturedAt: timeFromUnix(snapshot.CapturedAtUnix, reportedAt),
CPUUsagePercent: snapshot.CPUUsagePercent,
MemoryUsedBytes: snapshot.MemoryUsedBytes,
MemoryTotalBytes: snapshot.MemoryTotalBytes,
StorageUsedBytes: snapshot.StorageUsedBytes,
StorageTotalBytes: snapshot.StorageTotalBytes,
DiskReadBytes: snapshot.DiskReadBytes,
DiskWriteBytes: snapshot.DiskWriteBytes,
NetworkRxBytes: snapshot.NetworkRxBytes,
NetworkTxBytes: snapshot.NetworkTxBytes,
OpenrestyRxBytes: snapshot.OpenrestyRxBytes,
OpenrestyTxBytes: snapshot.OpenrestyTxBytes,
OpenrestyConnections: snapshot.OpenrestyConnections,
NodeID: nodeID,
CapturedAt: timeFromUnix(snapshot.CapturedAtUnix, reportedAt),
CPUUsagePercent: snapshot.CPUUsagePercent,
MemoryUsedBytes: snapshot.MemoryUsedBytes,
MemoryTotalBytes: snapshot.MemoryTotalBytes,
StorageUsedBytes: snapshot.StorageUsedBytes,
StorageTotalBytes: snapshot.StorageTotalBytes,
DiskReadBytes: snapshot.DiskReadBytes,
DiskWriteBytes: snapshot.DiskWriteBytes,
NetworkRxBytes: snapshot.NetworkRxBytes,
NetworkTxBytes: snapshot.NetworkTxBytes,
}
exists, err := model.NodeMetricSnapshotExists(tx, nodeID, record.CapturedAt)
if err != nil {
@@ -186,6 +194,20 @@ func persistNodeMetricSnapshot(tx *gorm.DB, nodeID string, snapshot *AgentNodeMe
return tx.Create(record).Error
}
func persistNodeOpenrestyObservation(tx *gorm.DB, nodeID string, obs *AgentNodeOpenrestyObservation, reportedAt time.Time) error {
if obs == nil {
return nil
}
record := &model.NodeObservationOpenresty{
NodeID: nodeID,
CapturedAt: timeFromUnix(obs.CapturedAtUnix, reportedAt),
OpenrestyRxBytes: obs.OpenrestyRxBytes,
OpenrestyTxBytes: obs.OpenrestyTxBytes,
OpenrestyConnections: obs.OpenrestyConnections,
}
return tx.Create(record).Error
}
func persistNodeTrafficReport(tx *gorm.DB, nodeID string, report *AgentNodeTrafficReport, reportedAt time.Time) error {
if report == nil {
return nil
@@ -110,7 +110,7 @@ func buildCapacityTrendPoints(now time.Time, snapshots []*model.NodeMetricSnapsh
return points
}
func buildNetworkTrendPoints(now time.Time, snapshots []*model.NodeMetricSnapshot) []NetworkTrendPoint {
func buildNetworkTrendPoints(now time.Time, snapshots []*model.NodeMetricSnapshot, openrestyObs []*model.NodeObservationOpenresty) []NetworkTrendPoint {
start := trendWindowStart(now)
points := make([]NetworkTrendPoint, observabilityTrendBuckets)
accumulators := make([]snapshotTrendAccumulator, observabilityTrendBuckets)
@@ -126,13 +126,23 @@ func buildNetworkTrendPoints(now time.Time, snapshots []*model.NodeMetricSnapsho
}
points[index].NetworkRxBytes += snapshot.NetworkRxBytes
points[index].NetworkTxBytes += snapshot.NetworkTxBytes
points[index].OpenrestyRxBytes += snapshot.OpenrestyRxBytes
points[index].OpenrestyTxBytes += snapshot.OpenrestyTxBytes
if snapshot.NodeID != "" {
accumulators[index].nodes[snapshot.NodeID] = struct{}{}
}
}
for _, obs := range openrestyObs {
index, ok := trendBucketIndex(obs.CapturedAt, start)
if !ok {
continue
}
points[index].OpenrestyRxBytes += obs.OpenrestyRxBytes
points[index].OpenrestyTxBytes += obs.OpenrestyTxBytes
if obs.NodeID != "" {
accumulators[index].nodes[obs.NodeID] = struct{}{}
}
}
for index := range points {
points[index].ReportedNodes = len(accumulators[index].nodes)
}
+19 -13
View File
@@ -11,8 +11,8 @@ import (
// RelayHeartbeatPayload is the payload sent by OpenFlareRelay in each heartbeat.
type RelayHeartbeatPayload struct {
RelayVersion string `json:"relay_version"`
FrpVersion string `json:"frp_version"`
Version string `json:"version"`
ExtVersion string `json:"frp_version"`
RelayStatus string `json:"relay_status"`
FrpsConnCount int `json:"frps_connections"`
FrpsProxyCount int `json:"frps_proxy_count"`
@@ -50,8 +50,8 @@ func HeartbeatRelay(node *model.Node, payload RelayHeartbeatPayload) (*RelayHear
}
slog.Debug("relay heartbeat received", "node_id", node.NodeID)
payload.RelayVersion = strings.TrimSpace(payload.RelayVersion)
payload.FrpVersion = strings.TrimSpace(payload.FrpVersion)
payload.Version = strings.TrimSpace(payload.Version)
payload.ExtVersion = strings.TrimSpace(payload.ExtVersion)
payload.RelayStatus = normalizeRelayStatus(payload.RelayStatus)
payload.Name = strings.TrimSpace(payload.Name)
payload.IP = strings.TrimSpace(payload.IP)
@@ -63,11 +63,10 @@ func HeartbeatRelay(node *model.Node, payload RelayHeartbeatPayload) (*RelayHear
}
}
now := time.Now()
appendRelayChange("relay_version", node.RelayVersion, payload.RelayVersion)
appendRelayChange("relay_frp_version", node.RelayFrpVersion, payload.FrpVersion)
appendRelayChange("version", node.Version, payload.Version)
appendRelayChange("ext_version", node.ExtVersion, payload.ExtVersion)
appendRelayChange("relay_status", node.RelayStatus, payload.RelayStatus)
appendRelayChange("relay_frps_connections", node.RelayFrpsConnections, payload.FrpsConnCount)
appendRelayChange("relay_frps_proxy_count", node.RelayFrpsProxyCount, payload.FrpsProxyCount)
if payload.Name != "" && strings.TrimSpace(node.Name) == "" {
appendRelayChange("name", node.Name, payload.Name)
node.Name = payload.Name
@@ -87,11 +86,10 @@ func HeartbeatRelay(node *model.Node, payload RelayHeartbeatPayload) (*RelayHear
}
changes["status"] = NodeStatusOnline
node.RelayVersion = payload.RelayVersion
node.RelayFrpVersion = payload.FrpVersion
node.Version = payload.Version
node.ExtVersion = payload.ExtVersion
node.RelayStatus = payload.RelayStatus
node.RelayFrpsConnections = payload.FrpsConnCount
node.RelayFrpsProxyCount = payload.FrpsProxyCount
node.LastSeenAt = now
node.Status = NodeStatusOnline
@@ -100,7 +98,7 @@ func HeartbeatRelay(node *model.Node, payload RelayHeartbeatPayload) (*RelayHear
return nil, fmt.Errorf("update relay heartbeat: %w", err)
}
}
refreshAgentTokenCache(node)
refreshAccessTokenCache(node)
persistRelayHeartbeatObservability(node.NodeID, payload, node.LastSeenAt)
return &RelayHeartbeatResponse{
@@ -115,6 +113,14 @@ func persistRelayHeartbeatObservability(nodeID string, payload RelayHeartbeatPay
Snapshot: payload.Snapshot,
HealthEvents: payload.HealthEvents,
}, reportedAt)
frpsObs := &model.NodeObservationFrps{
NodeID: nodeID,
CapturedAt: reportedAt,
FrpsConnections: payload.FrpsConnCount,
FrpsProxyCount: payload.FrpsProxyCount,
}
_ = frpsObs.Insert()
}
func buildRelayConfig(node *model.Node) *RelayConfig {
+12 -15
View File
@@ -10,14 +10,14 @@ func TestHeartbeatRelayPersistsRuntimeAndObservability(t *testing.T) {
setupServiceTestDB(t)
node := &model.Node{
NodeID: "node-relay-observe",
Name: "relay-1",
IP: "",
AgentToken: "relay-token",
Status: NodeStatusPending,
NodeType: "tunnel_relay",
RelayStatus: "unknown",
RelayVersion: "",
NodeID: "node-relay-observe",
Name: "relay-1",
IP: "",
AccessToken: "relay-token",
Status: NodeStatusPending,
NodeType: "tunnel_relay",
RelayStatus: "unknown",
Version: "",
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to seed relay node: %v", err)
@@ -25,8 +25,8 @@ func TestHeartbeatRelayPersistsRuntimeAndObservability(t *testing.T) {
now := time.Now().UTC()
_, err := HeartbeatRelay(node, RelayHeartbeatPayload{
RelayVersion: "v0.1.0",
FrpVersion: "0.61.0",
Version: "v0.1.0",
ExtVersion: "0.61.0",
RelayStatus: "healthy",
FrpsConnCount: 7,
FrpsProxyCount: 3,
@@ -62,11 +62,8 @@ func TestHeartbeatRelayPersistsRuntimeAndObservability(t *testing.T) {
if updated.IP != "203.0.113.9" {
t.Fatalf("expected relay IP to be updated, got %q", updated.IP)
}
if updated.RelayVersion != "v0.1.0" || updated.RelayFrpVersion != "0.61.0" {
t.Fatalf("expected relay versions to be updated, got relay=%q frp=%q", updated.RelayVersion, updated.RelayFrpVersion)
}
if updated.RelayFrpsConnections != 7 || updated.RelayFrpsProxyCount != 3 {
t.Fatalf("expected relay counters to be stored, got connections=%d proxies=%d", updated.RelayFrpsConnections, updated.RelayFrpsProxyCount)
if updated.Version != "v0.1.0" || updated.ExtVersion != "0.61.0" {
t.Fatalf("expected relay versions to be updated, got relay=%q frp=%q", updated.Version, updated.ExtVersion)
}
profile, err := model.GetNodeSystemProfile(node.NodeID)