mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 15:26:36 +08:00
feat: 添加节点可观测性查询功能,支持获取节点的系统配置、指标快照、流量报告和健康事件
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"atsflare/model"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultObservabilityWindow = 24 * time.Hour
|
||||
defaultObservabilityLimit = 120
|
||||
maxObservabilityLimit = 500
|
||||
)
|
||||
|
||||
type NodeObservabilityQuery struct {
|
||||
Hours int `json:"hours"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type NodeObservabilityView struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Profile *model.NodeSystemProfile `json:"profile"`
|
||||
MetricSnapshots []*model.NodeMetricSnapshot `json:"metric_snapshots"`
|
||||
TrafficReports []*model.NodeRequestReport `json:"traffic_reports"`
|
||||
HealthEvents []*model.NodeHealthEvent `json:"health_events"`
|
||||
}
|
||||
|
||||
func GetNodeObservability(id uint, query NodeObservabilityQuery) (*NodeObservabilityView, error) {
|
||||
node, err := model.GetNodeByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
limit := normalizeObservabilityLimit(query.Limit)
|
||||
since := time.Now().Add(-normalizeObservabilityWindow(query.Hours))
|
||||
|
||||
profile, err := model.GetNodeSystemProfile(node.NodeID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
profile = nil
|
||||
}
|
||||
|
||||
snapshots, err := model.ListNodeMetricSnapshots(node.NodeID, since, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reports, err := model.ListNodeRequestReports(node.NodeID, since, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events, err := model.ListNodeHealthEvents(node.NodeID, false, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &NodeObservabilityView{
|
||||
NodeID: node.NodeID,
|
||||
Profile: profile,
|
||||
MetricSnapshots: snapshots,
|
||||
TrafficReports: reports,
|
||||
HealthEvents: events,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeObservabilityLimit(limit int) int {
|
||||
if limit <= 0 {
|
||||
return defaultObservabilityLimit
|
||||
}
|
||||
if limit > maxObservabilityLimit {
|
||||
return maxObservabilityLimit
|
||||
}
|
||||
return limit
|
||||
}
|
||||
|
||||
func normalizeObservabilityWindow(hours int) time.Duration {
|
||||
if hours <= 0 {
|
||||
return defaultObservabilityWindow
|
||||
}
|
||||
return time.Duration(hours) * time.Hour
|
||||
}
|
||||
@@ -493,3 +493,100 @@ func TestHeartbeatNodeResolvesMissingHealthEvents(t *testing.T) {
|
||||
t.Fatalf("expected resolved health event record, got %+v", allEvents)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
if err := node.Insert(); err != nil {
|
||||
t.Fatalf("failed to insert node: %v", err)
|
||||
}
|
||||
if err := model.UpsertNodeSystemProfile(&model.NodeSystemProfile{
|
||||
NodeID: node.NodeID,
|
||||
Hostname: "query-edge",
|
||||
OSName: "Ubuntu",
|
||||
Architecture: "amd64",
|
||||
ReportedAt: time.Now(),
|
||||
}); err != nil {
|
||||
t.Fatalf("failed to insert node system profile: %v", err)
|
||||
}
|
||||
if err := (&model.NodeMetricSnapshot{
|
||||
NodeID: node.NodeID,
|
||||
CapturedAt: time.Now(),
|
||||
}).Insert(); err != nil {
|
||||
t.Fatalf("failed to insert node metric snapshot: %v", err)
|
||||
}
|
||||
if err := (&model.NodeRequestReport{
|
||||
NodeID: node.NodeID,
|
||||
WindowStartedAt: time.Now().Add(-time.Minute),
|
||||
WindowEndedAt: time.Now(),
|
||||
RequestCount: 123,
|
||||
}).Insert(); err != nil {
|
||||
t.Fatalf("failed to insert node request report: %v", err)
|
||||
}
|
||||
if err := model.DB.Create(&model.NodeHealthEvent{
|
||||
NodeID: node.NodeID,
|
||||
EventType: "sync_error",
|
||||
Severity: NodeHealthSeverityWarning,
|
||||
Status: NodeHealthEventStatusActive,
|
||||
Message: "checksum mismatch",
|
||||
FirstTriggeredAt: time.Now().Add(-time.Minute),
|
||||
LastTriggeredAt: time.Now(),
|
||||
ReportedAt: time.Now(),
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("failed to insert node health event: %v", err)
|
||||
}
|
||||
|
||||
view, err := GetNodeObservability(node.ID, NodeObservabilityQuery{Hours: 24, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("GetNodeObservability failed: %v", err)
|
||||
}
|
||||
if view.NodeID != node.NodeID {
|
||||
t.Fatalf("unexpected node id: %s", view.NodeID)
|
||||
}
|
||||
if view.Profile == nil || view.Profile.OSName != "Ubuntu" {
|
||||
t.Fatalf("unexpected profile: %+v", view.Profile)
|
||||
}
|
||||
if len(view.MetricSnapshots) != 1 {
|
||||
t.Fatalf("expected 1 metric snapshot, got %d", len(view.MetricSnapshots))
|
||||
}
|
||||
if len(view.TrafficReports) != 1 || view.TrafficReports[0].RequestCount != 123 {
|
||||
t.Fatalf("unexpected traffic reports: %+v", view.TrafficReports)
|
||||
}
|
||||
if len(view.HealthEvents) != 1 || view.HealthEvents[0].EventType != "sync_error" {
|
||||
t.Fatalf("unexpected health events: %+v", view.HealthEvents)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
if err := node.Insert(); err != nil {
|
||||
t.Fatalf("failed to insert node: %v", err)
|
||||
}
|
||||
|
||||
view, err := GetNodeObservability(node.ID, NodeObservabilityQuery{})
|
||||
if err != nil {
|
||||
t.Fatalf("GetNodeObservability failed: %v", err)
|
||||
}
|
||||
if view.Profile != nil {
|
||||
t.Fatalf("expected nil profile when profile not reported, got %+v", view.Profile)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user