[优化] 添加 Relay frps 连接和代理计数字段,重构相关逻辑以支持监控和可观测性

This commit is contained in:
ryan
2026-06-01 14:26:43 +08:00
parent 394e3c4855
commit 054dc1a8a8
22 changed files with 716 additions and 26 deletions
+2
View File
@@ -172,6 +172,8 @@ type NodeView struct {
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) {
+2
View File
@@ -355,6 +355,8 @@ func buildNodeView(node *model.Node) *NodeView {
view.RelayStatus = node.RelayStatus
view.RelayFrpVersion = node.RelayFrpVersion
view.RelayVersion = node.RelayVersion
view.RelayFrpsConnections = node.RelayFrpsConnections
view.RelayFrpsProxyCount = node.RelayFrpsProxyCount
return view
}
+55 -2
View File
@@ -27,6 +27,7 @@ type NodeObservabilityView struct {
HealthEvents []*model.NodeHealthEvent `json:"health_events"`
Analytics NodeObservabilityAnalytics `json:"analytics"`
Trends NodeObservabilityTrends `json:"trends"`
RelayDashboard *RelayDashboardSnapshot `json:"relay_dashboard,omitempty"`
}
type NodeObservabilityAnalytics struct {
@@ -47,6 +48,25 @@ type NodeHealthEventCleanupResult struct {
DeletedCount int64 `json:"deleted_count"`
}
type RelayDashboardSnapshot struct {
TotalProxies int `json:"total_proxies"`
OnlineProxies int `json:"online_proxies"`
OfflineProxies int `json:"offline_proxies"`
Proxies []RelayProxyStat `json:"proxies"`
TotalConnections int `json:"total_connections"`
ClientCounts int `json:"client_counts"`
}
type RelayProxyStat struct {
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
ClientVersion string `json:"client_version"`
LastStartTime string `json:"last_start_time"`
LastCloseTime string `json:"last_close_time"`
ClientAddr string `json:"client_addr"`
}
func GetNodeObservability(id uint, query NodeObservabilityQuery) (*NodeObservabilityView, error) {
now := time.Now()
node, err := model.GetNodeByID(id)
@@ -90,7 +110,7 @@ func GetNodeObservability(id uint, query NodeObservabilityQuery) (*NodeObservabi
return nil, err
}
return &NodeObservabilityView{
view := &NodeObservabilityView{
NodeID: node.NodeID,
Profile: profile,
MetricSnapshots: snapshots,
@@ -107,7 +127,40 @@ func GetNodeObservability(id uint, query NodeObservabilityQuery) (*NodeObservabi
Network24h: buildNetworkTrendPoints(now, trendSnapshots),
DiskIO24h: buildDiskIOTrendPoints(now, trendSnapshots),
},
}, nil
}
if node.NodeType == "tunnel_relay" {
view.RelayDashboard = buildRelayDashboardSnapshot(node)
}
return view, nil
}
func buildRelayDashboardSnapshot(node *model.Node) *RelayDashboardSnapshot {
if node == nil {
return nil
}
totalProxies := node.RelayFrpsProxyCount
if totalProxies < 0 {
totalProxies = 0
}
onlineProxies := totalProxies
if node.RelayStatus != "healthy" {
onlineProxies = 0
}
return &RelayDashboardSnapshot{
TotalProxies: totalProxies,
OnlineProxies: onlineProxies,
OfflineProxies: totalProxies - onlineProxies,
Proxies: []RelayProxyStat{},
TotalConnections: maxInt(node.RelayFrpsConnections, 0),
ClientCounts: 0,
}
}
func maxInt(a int, b int) int {
if a > b {
return a
}
return b
}
func CleanupNodeHealthEvents(id uint) (*NodeHealthEventCleanupResult, error) {
+39 -5
View File
@@ -11,11 +11,16 @@ import (
// RelayHeartbeatPayload is the payload sent by OpenFlareRelay in each heartbeat.
type RelayHeartbeatPayload struct {
RelayVersion string `json:"relay_version"`
FrpVersion string `json:"frp_version"`
RelayStatus string `json:"relay_status"`
FrpsConnCount int `json:"frps_connections"`
FrpsProxyCount int `json:"frps_proxy_count"`
RelayVersion string `json:"relay_version"`
FrpVersion string `json:"frp_version"`
RelayStatus string `json:"relay_status"`
FrpsConnCount int `json:"frps_connections"`
FrpsProxyCount int `json:"frps_proxy_count"`
Name string `json:"name"`
IP string `json:"ip"`
Profile *AgentNodeSystemProfile `json:"profile,omitempty"`
Snapshot *AgentNodeMetricSnapshot `json:"snapshot,omitempty"`
HealthEvents []AgentNodeHealthEvent `json:"health_events,omitempty"`
}
// RelayConfig is the frps configuration sent to the Relay.
@@ -48,6 +53,8 @@ func HeartbeatRelay(node *model.Node, payload RelayHeartbeatPayload) (*RelayHear
payload.RelayVersion = strings.TrimSpace(payload.RelayVersion)
payload.FrpVersion = strings.TrimSpace(payload.FrpVersion)
payload.RelayStatus = normalizeRelayStatus(payload.RelayStatus)
payload.Name = strings.TrimSpace(payload.Name)
payload.IP = strings.TrimSpace(payload.IP)
changes := make(map[string]any)
appendRelayChange := func(key string, before any, after any) {
@@ -59,6 +66,22 @@ func HeartbeatRelay(node *model.Node, payload RelayHeartbeatPayload) (*RelayHear
appendRelayChange("relay_version", node.RelayVersion, payload.RelayVersion)
appendRelayChange("relay_frp_version", node.RelayFrpVersion, payload.FrpVersion)
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
}
if payload.IP != "" && !node.IPManualOverride {
appendRelayChange("ip", node.IP, payload.IP)
node.IP = payload.IP
if !node.GeoManualOverride {
applyGeoInfoFromIP(node, node.IP)
changes["geo_name"] = node.GeoName
changes["geo_latitude"] = node.GeoLatitude
changes["geo_longitude"] = node.GeoLongitude
}
}
if !node.LastSeenAt.Equal(now) {
changes["last_seen_at"] = now
}
@@ -67,6 +90,8 @@ func HeartbeatRelay(node *model.Node, payload RelayHeartbeatPayload) (*RelayHear
node.RelayVersion = payload.RelayVersion
node.RelayFrpVersion = payload.FrpVersion
node.RelayStatus = payload.RelayStatus
node.RelayFrpsConnections = payload.FrpsConnCount
node.RelayFrpsProxyCount = payload.FrpsProxyCount
node.LastSeenAt = now
node.Status = NodeStatusOnline
@@ -76,6 +101,7 @@ func HeartbeatRelay(node *model.Node, payload RelayHeartbeatPayload) (*RelayHear
}
}
refreshAgentTokenCache(node)
persistRelayHeartbeatObservability(node.NodeID, payload, node.LastSeenAt)
return &RelayHeartbeatResponse{
RelayConfig: buildRelayConfig(node),
@@ -83,6 +109,14 @@ func HeartbeatRelay(node *model.Node, payload RelayHeartbeatPayload) (*RelayHear
}, nil
}
func persistRelayHeartbeatObservability(nodeID string, payload RelayHeartbeatPayload, reportedAt time.Time) {
persistHeartbeatObservability(nodeID, AgentNodePayload{
Profile: payload.Profile,
Snapshot: payload.Snapshot,
HealthEvents: payload.HealthEvents,
}, reportedAt)
}
func buildRelayConfig(node *model.Node) *RelayConfig {
if node == nil {
return nil
+98
View File
@@ -0,0 +1,98 @@
package service
import (
"openflare/model"
"testing"
"time"
)
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: "",
}
if err := node.Insert(); err != nil {
t.Fatalf("failed to seed relay node: %v", err)
}
now := time.Now().UTC()
_, err := HeartbeatRelay(node, RelayHeartbeatPayload{
RelayVersion: "v0.1.0",
FrpVersion: "0.61.0",
RelayStatus: "healthy",
FrpsConnCount: 7,
FrpsProxyCount: 3,
Name: "relay-runtime",
IP: "203.0.113.9",
Profile: &AgentNodeSystemProfile{
Hostname: "relay-runtime",
OSName: "Ubuntu",
OSVersion: "24.04",
Architecture: "amd64",
CPUCores: 4,
ReportedAtUnix: now.Unix(),
},
Snapshot: &AgentNodeMetricSnapshot{
CapturedAtUnix: now.Unix(),
CPUUsagePercent: 12.5,
NetworkRxBytes: 1024,
NetworkTxBytes: 2048,
},
HealthEvents: []AgentNodeHealthEvent{},
})
if err != nil {
t.Fatalf("HeartbeatRelay failed: %v", err)
}
updated, err := model.GetNodeByNodeID(node.NodeID)
if err != nil {
t.Fatalf("failed to reload node: %v", err)
}
if updated.Status != NodeStatusOnline || updated.RelayStatus != "healthy" {
t.Fatalf("unexpected relay status: %+v", updated)
}
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)
}
profile, err := model.GetNodeSystemProfile(node.NodeID)
if err != nil {
t.Fatalf("expected relay system profile: %v", err)
}
if profile.Hostname != "relay-runtime" || profile.OSName != "Ubuntu" {
t.Fatalf("unexpected relay profile: %+v", profile)
}
snapshots, err := model.ListNodeMetricSnapshots(node.NodeID, now.Add(-time.Minute), 10)
if err != nil {
t.Fatalf("failed to list relay snapshots: %v", err)
}
if len(snapshots) != 1 || snapshots[0].CPUUsagePercent != 12.5 {
t.Fatalf("unexpected relay snapshots: %+v", snapshots)
}
observability, err := GetNodeObservability(updated.ID, NodeObservabilityQuery{Hours: 1, Limit: 10})
if err != nil {
t.Fatalf("GetNodeObservability failed: %v", err)
}
if observability.RelayDashboard == nil {
t.Fatal("expected relay dashboard snapshot")
}
if observability.RelayDashboard.TotalConnections != 7 || observability.RelayDashboard.TotalProxies != 3 {
t.Fatalf("unexpected relay dashboard: %+v", observability.RelayDashboard)
}
}