mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 14:26:36 +08:00
fc3db065db
- Extend DashboardNodeHealth and NodeItem interfaces to include geo_name, geo_latitude, and geo_longitude. - Update NodeDetailPage and NodesPage components to handle geo metadata in forms and payloads. - Implement validation for geo fields in NodesPage using Zod. - Enhance dashboard overview tests to include geo metadata for nodes. - Create WorldStage component to visualize node health and geo locations on a world map. - Update design and development documentation to reflect the addition of geo metadata for nodes.
436 lines
14 KiB
Go
436 lines
14 KiB
Go
package service
|
|
|
|
import (
|
|
"atsflare/model"
|
|
"errors"
|
|
"sort"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type DashboardOverviewView struct {
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
Summary DashboardSummary `json:"summary"`
|
|
Traffic DashboardTraffic `json:"traffic"`
|
|
Capacity DashboardCapacity `json:"capacity"`
|
|
Config DashboardConfig `json:"config"`
|
|
Risk DashboardRiskSummary `json:"risk"`
|
|
Peaks DashboardPeakSummary `json:"peaks"`
|
|
Distributions TrafficDistributions `json:"distributions"`
|
|
Trends DashboardTrends `json:"trends"`
|
|
Nodes []DashboardNodeHealth `json:"nodes"`
|
|
ActiveAlerts []DashboardAlert `json:"active_alerts"`
|
|
}
|
|
|
|
type DashboardSummary struct {
|
|
TotalNodes int `json:"total_nodes"`
|
|
OnlineNodes int `json:"online_nodes"`
|
|
OfflineNodes int `json:"offline_nodes"`
|
|
PendingNodes int `json:"pending_nodes"`
|
|
UnhealthyNodes int `json:"unhealthy_nodes"`
|
|
ActiveAlerts int `json:"active_alerts"`
|
|
LaggingNodes int `json:"lagging_nodes"`
|
|
}
|
|
|
|
type DashboardTraffic struct {
|
|
RequestCount int64 `json:"request_count"`
|
|
UniqueVisitors int64 `json:"unique_visitors"`
|
|
ErrorCount int64 `json:"error_count"`
|
|
EstimatedQPS float64 `json:"estimated_qps"`
|
|
ReportedNodes int `json:"reported_nodes"`
|
|
}
|
|
|
|
type DashboardCapacity struct {
|
|
AverageCPUUsagePercent float64 `json:"average_cpu_usage_percent"`
|
|
AverageMemoryUsagePercent float64 `json:"average_memory_usage_percent"`
|
|
HighCPUNodes int `json:"high_cpu_nodes"`
|
|
HighMemoryNodes int `json:"high_memory_nodes"`
|
|
HighStorageNodes int `json:"high_storage_nodes"`
|
|
}
|
|
|
|
type DashboardConfig struct {
|
|
ActiveVersion string `json:"active_version"`
|
|
LaggingNodes int `json:"lagging_nodes"`
|
|
PendingNodes int `json:"pending_nodes"`
|
|
}
|
|
|
|
type DashboardRiskSummary struct {
|
|
CriticalAlerts int `json:"critical_alerts"`
|
|
WarningAlerts int `json:"warning_alerts"`
|
|
InfoAlerts int `json:"info_alerts"`
|
|
OfflineNodes int `json:"offline_nodes"`
|
|
UnhealthyNodes int `json:"unhealthy_nodes"`
|
|
LaggingNodes int `json:"lagging_nodes"`
|
|
HighCPUNodes int `json:"high_cpu_nodes"`
|
|
HighMemoryNodes int `json:"high_memory_nodes"`
|
|
HighStorageNodes int `json:"high_storage_nodes"`
|
|
}
|
|
|
|
type DashboardPeakSummary struct {
|
|
PeakRequestHour DashboardPeakHour `json:"peak_request_hour"`
|
|
PeakErrorHour DashboardPeakHour `json:"peak_error_hour"`
|
|
BusiestNode *DashboardPeakNode `json:"busiest_node"`
|
|
RiskiestNode *DashboardPeakNode `json:"riskiest_node"`
|
|
}
|
|
|
|
type DashboardPeakHour struct {
|
|
BucketStartedAt time.Time `json:"bucket_started_at"`
|
|
RequestCount int64 `json:"request_count"`
|
|
ErrorCount int64 `json:"error_count"`
|
|
}
|
|
|
|
type DashboardPeakNode struct {
|
|
NodeID string `json:"node_id"`
|
|
NodeName string `json:"node_name"`
|
|
RequestCount int64 `json:"request_count"`
|
|
ErrorCount int64 `json:"error_count"`
|
|
CPUUsagePercent float64 `json:"cpu_usage_percent"`
|
|
ActiveEventCount int `json:"active_event_count"`
|
|
OpenrestyStatus string `json:"openresty_status"`
|
|
StorageUsagePercent float64 `json:"storage_usage_percent"`
|
|
}
|
|
|
|
type DashboardTrends struct {
|
|
Traffic24h []TrafficTrendPoint `json:"traffic_24h"`
|
|
Capacity24h []CapacityTrendPoint `json:"capacity_24h"`
|
|
Network24h []NetworkTrendPoint `json:"network_24h"`
|
|
DiskIO24h []DiskIOTrendPoint `json:"disk_io_24h"`
|
|
}
|
|
|
|
type DashboardNodeHealth struct {
|
|
ID uint `json:"id"`
|
|
NodeID string `json:"node_id"`
|
|
Name string `json:"name"`
|
|
GeoName string `json:"geo_name"`
|
|
GeoLatitude *float64 `json:"geo_latitude"`
|
|
GeoLongitude *float64 `json:"geo_longitude"`
|
|
Status string `json:"status"`
|
|
OpenrestyStatus string `json:"openresty_status"`
|
|
CurrentVersion string `json:"current_version"`
|
|
LastSeenAt time.Time `json:"last_seen_at"`
|
|
ActiveEventCount int `json:"active_event_count"`
|
|
CPUUsagePercent float64 `json:"cpu_usage_percent"`
|
|
MemoryUsagePercent float64 `json:"memory_usage_percent"`
|
|
StorageUsagePercent float64 `json:"storage_usage_percent"`
|
|
RequestCount int64 `json:"request_count"`
|
|
ErrorCount int64 `json:"error_count"`
|
|
UniqueVisitorCount int64 `json:"unique_visitor_count"`
|
|
}
|
|
|
|
type DashboardAlert struct {
|
|
NodeID string `json:"node_id"`
|
|
NodeName string `json:"node_name"`
|
|
EventType string `json:"event_type"`
|
|
Severity string `json:"severity"`
|
|
Message string `json:"message"`
|
|
LastTriggeredAt time.Time `json:"last_triggered_at"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func GetDashboardOverview() (*DashboardOverviewView, error) {
|
|
now := time.Now()
|
|
since := now.Add(-24 * time.Hour)
|
|
|
|
nodes, err := model.ListNodes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
activeVersion := ""
|
|
if version, versionErr := model.GetActiveConfigVersion(); versionErr == nil {
|
|
activeVersion = version.Version
|
|
} else if !errors.Is(versionErr, gorm.ErrRecordNotFound) {
|
|
return nil, versionErr
|
|
}
|
|
|
|
snapshots, err := model.ListMetricSnapshotsSince(since)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reports, err := model.ListRequestReportsSince(since)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
activeEvents, err := model.ListActiveNodeHealthEvents()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
view := &DashboardOverviewView{
|
|
GeneratedAt: now,
|
|
Nodes: make([]DashboardNodeHealth, 0, len(nodes)),
|
|
Distributions: buildTrafficDistributions(reports, 8),
|
|
Trends: DashboardTrends{
|
|
Traffic24h: buildTrafficTrendPoints(now, reports),
|
|
Capacity24h: buildCapacityTrendPoints(now, snapshots),
|
|
Network24h: buildNetworkTrendPoints(now, snapshots),
|
|
DiskIO24h: buildDiskIOTrendPoints(now, snapshots),
|
|
},
|
|
}
|
|
|
|
var cpuNodeCount int
|
|
var memoryNodeCount int
|
|
latestSnapshots := latestMetricSnapshotsByNode(snapshots)
|
|
latestTrafficReports := latestTrafficReportsByNode(reports)
|
|
activeEventsByNode := activeHealthEventsByNode(activeEvents)
|
|
|
|
for _, node := range nodes {
|
|
computedStatus := computeNodeStatus(node)
|
|
switch computedStatus {
|
|
case NodeStatusOnline:
|
|
view.Summary.OnlineNodes++
|
|
case NodeStatusOffline:
|
|
view.Summary.OfflineNodes++
|
|
view.Risk.OfflineNodes++
|
|
case NodeStatusPending:
|
|
view.Summary.PendingNodes++
|
|
}
|
|
if node.OpenrestyStatus == OpenrestyStatusUnhealthy {
|
|
view.Summary.UnhealthyNodes++
|
|
view.Risk.UnhealthyNodes++
|
|
}
|
|
if activeVersion != "" && node.CurrentVersion != "" && node.CurrentVersion != activeVersion {
|
|
view.Summary.LaggingNodes++
|
|
view.Risk.LaggingNodes++
|
|
}
|
|
if activeVersion != "" && node.CurrentVersion == "" && computedStatus != NodeStatusPending {
|
|
view.Summary.LaggingNodes++
|
|
view.Risk.LaggingNodes++
|
|
}
|
|
|
|
latestSnapshot := latestSnapshots[node.NodeID]
|
|
latestTraffic := latestTrafficReports[node.NodeID]
|
|
nodeActiveEvents := activeEventsByNode[node.NodeID]
|
|
|
|
nodeHealth := DashboardNodeHealth{
|
|
ID: node.ID,
|
|
NodeID: node.NodeID,
|
|
Name: node.Name,
|
|
GeoName: node.GeoName,
|
|
GeoLatitude: node.GeoLatitude,
|
|
GeoLongitude: node.GeoLongitude,
|
|
Status: computedStatus,
|
|
OpenrestyStatus: node.OpenrestyStatus,
|
|
CurrentVersion: node.CurrentVersion,
|
|
LastSeenAt: node.LastSeenAt,
|
|
ActiveEventCount: len(nodeActiveEvents),
|
|
}
|
|
|
|
for _, event := range nodeActiveEvents {
|
|
view.ActiveAlerts = append(view.ActiveAlerts, DashboardAlert{
|
|
NodeID: node.NodeID,
|
|
NodeName: node.Name,
|
|
EventType: event.EventType,
|
|
Severity: event.Severity,
|
|
Message: event.Message,
|
|
LastTriggeredAt: event.LastTriggeredAt,
|
|
Status: event.Status,
|
|
})
|
|
switch event.Severity {
|
|
case NodeHealthSeverityCritical:
|
|
view.Risk.CriticalAlerts++
|
|
case NodeHealthSeverityWarning:
|
|
view.Risk.WarningAlerts++
|
|
default:
|
|
view.Risk.InfoAlerts++
|
|
}
|
|
}
|
|
|
|
if latestSnapshot != nil {
|
|
nodeHealth.CPUUsagePercent = latestSnapshot.CPUUsagePercent
|
|
nodeHealth.MemoryUsagePercent = percentage(latestSnapshot.MemoryUsedBytes, latestSnapshot.MemoryTotalBytes)
|
|
nodeHealth.StorageUsagePercent = percentage(latestSnapshot.StorageUsedBytes, latestSnapshot.StorageTotalBytes)
|
|
if latestSnapshot.CPUUsagePercent > 0 {
|
|
view.Capacity.AverageCPUUsagePercent += latestSnapshot.CPUUsagePercent
|
|
cpuNodeCount++
|
|
}
|
|
if nodeHealth.MemoryUsagePercent > 0 {
|
|
view.Capacity.AverageMemoryUsagePercent += nodeHealth.MemoryUsagePercent
|
|
memoryNodeCount++
|
|
}
|
|
if latestSnapshot.CPUUsagePercent >= 80 {
|
|
view.Capacity.HighCPUNodes++
|
|
view.Risk.HighCPUNodes++
|
|
}
|
|
if nodeHealth.MemoryUsagePercent >= 85 {
|
|
view.Capacity.HighMemoryNodes++
|
|
view.Risk.HighMemoryNodes++
|
|
}
|
|
if nodeHealth.StorageUsagePercent >= 85 {
|
|
view.Capacity.HighStorageNodes++
|
|
view.Risk.HighStorageNodes++
|
|
}
|
|
}
|
|
|
|
if latestTraffic != nil {
|
|
nodeHealth.RequestCount = latestTraffic.RequestCount
|
|
nodeHealth.ErrorCount = latestTraffic.ErrorCount
|
|
nodeHealth.UniqueVisitorCount = latestTraffic.UniqueVisitorCount
|
|
view.Traffic.RequestCount += latestTraffic.RequestCount
|
|
view.Traffic.UniqueVisitors += latestTraffic.UniqueVisitorCount
|
|
view.Traffic.ErrorCount += latestTraffic.ErrorCount
|
|
if duration := latestTraffic.WindowEndedAt.Sub(latestTraffic.WindowStartedAt).Seconds(); duration > 0 {
|
|
view.Traffic.EstimatedQPS += float64(latestTraffic.RequestCount) / duration
|
|
}
|
|
view.Traffic.ReportedNodes++
|
|
}
|
|
|
|
view.Summary.ActiveAlerts += len(nodeActiveEvents)
|
|
view.Nodes = append(view.Nodes, nodeHealth)
|
|
}
|
|
|
|
view.Summary.TotalNodes = len(nodes)
|
|
view.Config.ActiveVersion = activeVersion
|
|
view.Config.LaggingNodes = view.Summary.LaggingNodes
|
|
view.Config.PendingNodes = view.Summary.PendingNodes
|
|
|
|
if cpuNodeCount > 0 {
|
|
view.Capacity.AverageCPUUsagePercent /= float64(cpuNodeCount)
|
|
}
|
|
if memoryNodeCount > 0 {
|
|
view.Capacity.AverageMemoryUsagePercent /= float64(memoryNodeCount)
|
|
}
|
|
|
|
sort.Slice(view.ActiveAlerts, func(i int, j int) bool {
|
|
if severityWeight(view.ActiveAlerts[i].Severity) == severityWeight(view.ActiveAlerts[j].Severity) {
|
|
return view.ActiveAlerts[i].LastTriggeredAt.After(view.ActiveAlerts[j].LastTriggeredAt)
|
|
}
|
|
return severityWeight(view.ActiveAlerts[i].Severity) > severityWeight(view.ActiveAlerts[j].Severity)
|
|
})
|
|
|
|
sort.Slice(view.Nodes, func(i int, j int) bool {
|
|
if view.Nodes[i].ActiveEventCount == view.Nodes[j].ActiveEventCount {
|
|
return view.Nodes[i].CPUUsagePercent > view.Nodes[j].CPUUsagePercent
|
|
}
|
|
return view.Nodes[i].ActiveEventCount > view.Nodes[j].ActiveEventCount
|
|
})
|
|
|
|
if len(view.ActiveAlerts) > 8 {
|
|
view.ActiveAlerts = view.ActiveAlerts[:8]
|
|
}
|
|
|
|
view.Peaks.PeakRequestHour = peakTrafficHour(view.Trends.Traffic24h, func(point TrafficTrendPoint) int64 {
|
|
return point.RequestCount
|
|
})
|
|
view.Peaks.PeakErrorHour = peakTrafficHour(view.Trends.Traffic24h, func(point TrafficTrendPoint) int64 {
|
|
return point.ErrorCount
|
|
})
|
|
view.Peaks.BusiestNode = busiestDashboardNode(view.Nodes)
|
|
view.Peaks.RiskiestNode = riskiestDashboardNode(view.Nodes)
|
|
|
|
return view, nil
|
|
}
|
|
|
|
func percentage(used int64, total int64) float64 {
|
|
if used <= 0 || total <= 0 {
|
|
return 0
|
|
}
|
|
return (float64(used) / float64(total)) * 100
|
|
}
|
|
|
|
func latestMetricSnapshotsByNode(snapshots []*model.NodeMetricSnapshot) map[string]*model.NodeMetricSnapshot {
|
|
result := make(map[string]*model.NodeMetricSnapshot, len(snapshots))
|
|
for _, snapshot := range snapshots {
|
|
if snapshot == nil || snapshot.NodeID == "" {
|
|
continue
|
|
}
|
|
if existing, ok := result[snapshot.NodeID]; ok && !snapshot.CapturedAt.After(existing.CapturedAt) {
|
|
continue
|
|
}
|
|
result[snapshot.NodeID] = snapshot
|
|
}
|
|
return result
|
|
}
|
|
|
|
func latestTrafficReportsByNode(reports []*model.NodeRequestReport) map[string]*model.NodeRequestReport {
|
|
result := make(map[string]*model.NodeRequestReport, len(reports))
|
|
for _, report := range reports {
|
|
if report == nil || report.NodeID == "" {
|
|
continue
|
|
}
|
|
if existing, ok := result[report.NodeID]; ok && !report.WindowEndedAt.After(existing.WindowEndedAt) {
|
|
continue
|
|
}
|
|
result[report.NodeID] = report
|
|
}
|
|
return result
|
|
}
|
|
|
|
func activeHealthEventsByNode(events []*model.NodeHealthEvent) map[string][]*model.NodeHealthEvent {
|
|
result := make(map[string][]*model.NodeHealthEvent)
|
|
for _, event := range events {
|
|
if event == nil || event.NodeID == "" {
|
|
continue
|
|
}
|
|
result[event.NodeID] = append(result[event.NodeID], event)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func severityWeight(severity string) int {
|
|
switch severity {
|
|
case NodeHealthSeverityCritical:
|
|
return 3
|
|
case NodeHealthSeverityWarning:
|
|
return 2
|
|
default:
|
|
return 1
|
|
}
|
|
}
|
|
|
|
func peakTrafficHour(points []TrafficTrendPoint, selector func(point TrafficTrendPoint) int64) DashboardPeakHour {
|
|
var result DashboardPeakHour
|
|
var maxValue int64 = -1
|
|
for _, point := range points {
|
|
value := selector(point)
|
|
if value <= maxValue {
|
|
continue
|
|
}
|
|
maxValue = value
|
|
result = DashboardPeakHour{
|
|
BucketStartedAt: point.BucketStartedAt,
|
|
RequestCount: point.RequestCount,
|
|
ErrorCount: point.ErrorCount,
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func busiestDashboardNode(nodes []DashboardNodeHealth) *DashboardPeakNode {
|
|
var selected *DashboardPeakNode
|
|
for _, node := range nodes {
|
|
candidate := &DashboardPeakNode{
|
|
NodeID: node.NodeID,
|
|
NodeName: node.Name,
|
|
RequestCount: node.RequestCount,
|
|
ErrorCount: node.ErrorCount,
|
|
CPUUsagePercent: node.CPUUsagePercent,
|
|
ActiveEventCount: node.ActiveEventCount,
|
|
OpenrestyStatus: node.OpenrestyStatus,
|
|
StorageUsagePercent: node.StorageUsagePercent,
|
|
}
|
|
if selected == nil || candidate.RequestCount > selected.RequestCount || (candidate.RequestCount == selected.RequestCount && candidate.ErrorCount > selected.ErrorCount) {
|
|
selected = candidate
|
|
}
|
|
}
|
|
return selected
|
|
}
|
|
|
|
func riskiestDashboardNode(nodes []DashboardNodeHealth) *DashboardPeakNode {
|
|
if len(nodes) == 0 {
|
|
return nil
|
|
}
|
|
node := nodes[0]
|
|
return &DashboardPeakNode{
|
|
NodeID: node.NodeID,
|
|
NodeName: node.Name,
|
|
RequestCount: node.RequestCount,
|
|
ErrorCount: node.ErrorCount,
|
|
CPUUsagePercent: node.CPUUsagePercent,
|
|
ActiveEventCount: node.ActiveEventCount,
|
|
OpenrestyStatus: node.OpenrestyStatus,
|
|
StorageUsagePercent: node.StorageUsagePercent,
|
|
}
|
|
}
|