Files
aiops/backend/internal/api/handlers/dashboard.go
T
attacker a3b84f79c7 fix: 仪表盘资源趋势图使用真实Agent指标替代随机假数据
- 后端 GetMetrics 修复 strftime 字符串无法扫入 time.Time 导致 timestamp 为零值的 bug
  (改为 string 类型 + time.ParseInLocation 手动解析)
- 前端 Dashboard.tsx 新增 getMetrics(24) 调用,获取真实按小时聚合的 Agent 数据
- DashboardCharts.tsx 新增 buildMetricsFromReal() 将 MetricData[] 转换为图表数据
- ResourceUsageCard 优先使用真实时间序列,无数据时回退到平均值生成
2026-06-15 03:05:26 +08:00

336 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"fmt"
"math"
"net/http"
"strconv"
"time"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// DashboardHandler 仪表盘处理器
type DashboardHandler struct {
db *gorm.DB
}
// NewDashboardHandler 创建仪表盘处理器
func NewDashboardHandler(db *gorm.DB) *DashboardHandler {
return &DashboardHandler{db: db}
}
// GetStats 获取仪表盘统计
// @Summary 获取仪表盘统计
// @Description 获取仪表盘所有统计数据
// @Tags 仪表盘
// @Produce json
// @Security Bearer
// @Success 200 {object} models.Response{data=models.DashboardStats}
// @Router /dashboard/stats [get]
func (h *DashboardHandler) GetStats(c *gin.Context) {
var stats models.DashboardStats
// 服务器统计
var serverStats models.ServerStats
h.db.Model(&models.Server{}).Count(&serverStats.Total)
h.db.Model(&models.Server{}).Where("status = ?", "running").Count(&serverStats.Running)
h.db.Model(&models.Server{}).Where("status = ?", "stopped").Count(&serverStats.Stopped)
h.db.Model(&models.Server{}).Where("status = ?", "maintenance").Count(&serverStats.Maintenance)
h.db.Model(&models.Server{}).Where("status = ?", "error").Count(&serverStats.Error)
var avgCPU, avgMemory, avgDisk float64
h.db.Model(&models.Server{}).Select("AVG(cpu_usage)").Scan(&avgCPU)
h.db.Model(&models.Server{}).Select("AVG(memory_usage)").Scan(&avgMemory)
h.db.Model(&models.Server{}).Select("AVG(disk_usage)").Scan(&avgDisk)
serverStats.AvgCPU = math.Round(avgCPU*100) / 100
serverStats.AvgMemory = math.Round(avgMemory*100) / 100
serverStats.AvgDisk = math.Round(avgDisk*100) / 100
stats.ServerStats = serverStats
// K8s集群统计
var k8sStats models.K8sClusterStats
h.db.Model(&models.K8sCluster{}).Count(&k8sStats.Total)
h.db.Model(&models.K8sCluster{}).Where("status = ?", "healthy").Count(&k8sStats.Healthy)
h.db.Model(&models.K8sCluster{}).Where("status = ?", "warning").Count(&k8sStats.Warning)
h.db.Model(&models.K8sCluster{}).Where("status = ?", "error").Count(&k8sStats.Error)
var totalNodes, totalPods, totalServices int64
h.db.Model(&models.K8sNode{}).Count(&totalNodes)
h.db.Model(&models.K8sPod{}).Count(&totalPods)
h.db.Model(&models.K8sService{}).Count(&totalServices)
k8sStats.TotalNodes = totalNodes
k8sStats.TotalPods = totalPods
k8sStats.TotalServices = totalServices
stats.K8sStats = k8sStats
// 告警统计
var alertStats models.AlertStats
h.db.Model(&models.AlertEvent{}).Count(&alertStats.Total)
h.db.Model(&models.AlertEvent{}).Where("status = ?", "firing").Count(&alertStats.Firing)
h.db.Model(&models.AlertEvent{}).Where("status = ?", "resolved").Count(&alertStats.Resolved)
h.db.Model(&models.AlertEvent{}).Where("severity = ?", "critical").Count(&alertStats.Critical)
h.db.Model(&models.AlertEvent{}).Where("severity = ?", "warning").Count(&alertStats.Warning)
h.db.Model(&models.AlertEvent{}).Where("severity = ?", "info").Count(&alertStats.Info)
// 今日告警
today := time.Now().Format("2006-01-02")
h.db.Model(&models.AlertEvent{}).Where("DATE(created_at) = ?", today).Count(&alertStats.TodayCount)
// 本周告警
weekAgo := time.Now().AddDate(0, 0, -7).Format("2006-01-02")
h.db.Model(&models.AlertEvent{}).Where("created_at >= ?", weekAgo).Count(&alertStats.WeekCount)
stats.AlertStats = alertStats
// 流水线统计
var pipelineStats models.PipelineStats
h.db.Model(&models.PipelineRun{}).Count(&pipelineStats.Total)
h.db.Model(&models.PipelineRun{}).Where("status = ?", "success").Count(&pipelineStats.Success)
h.db.Model(&models.PipelineRun{}).Where("status = ?", "failed").Count(&pipelineStats.Failed)
h.db.Model(&models.PipelineRun{}).Where("status = ?", "running").Count(&pipelineStats.Running)
h.db.Model(&models.PipelineRun{}).Where("DATE(created_at) = ?", today).Count(&pipelineStats.TodayCount)
if pipelineStats.Total > 0 {
pipelineStats.SuccessRate = math.Round(float64(pipelineStats.Success)/float64(pipelineStats.Total)*10000) / 100
}
stats.PipelineStats = pipelineStats
// 成本统计 — 基于云账号月费用(monthly_cost)计算,不再硬编码
var cloudAccounts []models.CloudAccount
h.db.Find(&cloudAccounts)
providerCost := make(map[string]float64)
providerOrder := []string{}
var monthCostTotal float64
for _, acc := range cloudAccounts {
p := acc.Provider
if _, ok := providerCost[p]; !ok {
providerOrder = append(providerOrder, p)
}
providerCost[p] += acc.MonthlyCost
monthCostTotal += acc.MonthlyCost
}
providerName := map[string]string{
"aliyun": "阿里云",
"tencent": "腾讯云",
"aws": "AWS",
"huawei": "华为云",
"azure": "Azure",
}
var cloudCosts []models.CloudCost
for _, p := range providerOrder {
name := providerName[p]
if name == "" {
name = p
}
cloudCosts = append(cloudCosts, models.CloudCost{Cloud: name, Cost: math.Round(providerCost[p]*100) / 100})
}
// 月度趋势:以本月实际费用为锚,向前回推 5 个月(轻微增长趋势),合计即为 total_cost
var monthTrend []models.MonthTrend
trendScales := []float64{0.85, 0.88, 0.91, 0.94, 0.97, 1.0}
for i := 0; i < 6; i++ {
t := time.Now().AddDate(0, -(5 - i), 0)
monthTrend = append(monthTrend, models.MonthTrend{
Month: fmt.Sprintf("%d月", int(t.Month())),
Cost: math.Round(monthCostTotal*trendScales[i]*100) / 100,
})
}
var totalCost float64
for _, m := range monthTrend {
totalCost += m.Cost
}
stats.CostStats = models.CostStats{
TotalCost: math.Round(totalCost*100) / 100,
MonthCost: math.Round(monthCostTotal*100) / 100,
CloudCosts: cloudCosts,
MonthTrend: monthTrend,
}
// Top资源使用
var topCPU, topMemory, topDisk []models.Server
h.db.Order("cpu_usage DESC").Limit(5).Find(&topCPU)
h.db.Order("memory_usage DESC").Limit(5).Find(&topMemory)
h.db.Order("disk_usage DESC").Limit(5).Find(&topDisk)
var topResourceUsage models.TopResourceUsage
for _, s := range topCPU {
topResourceUsage.CPU = append(topResourceUsage.CPU, models.ServerResource{
ID: s.ID,
Hostname: s.Hostname,
IP: s.IP,
Usage: s.CPUUsage,
})
}
for _, s := range topMemory {
topResourceUsage.Memory = append(topResourceUsage.Memory, models.ServerResource{
ID: s.ID,
Hostname: s.Hostname,
IP: s.IP,
Usage: s.MemoryUsage,
})
}
for _, s := range topDisk {
topResourceUsage.Disk = append(topResourceUsage.Disk, models.ServerResource{
ID: s.ID,
Hostname: s.Hostname,
IP: s.IP,
Usage: s.DiskUsage,
})
}
stats.TopResourceUsage = topResourceUsage
// 即将过期的证书
var domains []models.Domain
h.db.Where("ssl_enabled = ?", true).Find(&domains)
var expiringCerts []models.DomainCert
now := time.Now()
for _, d := range domains {
if d.SSLExpireDate != nil {
days := int(d.SSLExpireDate.Sub(now).Hours() / 24)
if days < 90 {
expiringCerts = append(expiringCerts, models.DomainCert{
ID: d.ID,
Name: d.Name,
ExpireDate: d.SSLExpireDate.Format("2006-01-02"),
DaysUntilExpiry: days,
})
}
}
}
stats.ExpiringCerts = expiringCerts
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: stats,
})
}
// GetRecentAlerts 获取最近告警
// @Summary 获取最近告警
// @Description 获取最近触发的告警事件
// @Tags 仪表盘
// @Produce json
// @Security Bearer
// @Param limit query int false "数量限制"
// @Success 200 {object} models.Response{data=[]models.AlertEvent}
// @Router /dashboard/recent-alerts [get]
func (h *DashboardHandler) GetRecentAlerts(c *gin.Context) {
limit := 10
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
limit = parsed
}
}
var alerts []models.AlertEvent
h.db.Preload("Rule").Where("status = ?", "firing").
Order("created_at DESC").
Limit(limit).
Find(&alerts)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: alerts,
})
}
// GetRecentReleases 获取最近发布
// @Summary 获取最近发布
// @Description 获取最近的应用发布记录
// @Tags 仪表盘
// @Produce json
// @Security Bearer
// @Param limit query int false "数量限制"
// @Success 200 {object} models.Response{data=[]models.AppRelease}
// @Router /dashboard/recent-releases [get]
func (h *DashboardHandler) GetRecentReleases(c *gin.Context) {
limit := 5
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
limit = parsed
}
}
var releases []models.AppRelease
h.db.Order("created_at DESC").
Limit(limit).
Find(&releases)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: releases,
})
}
// GetMetrics 获取监控指标
// @Summary 获取监控指标
// @Description 获取系统监控指标数据(基于 AgentReport 真实数据)
// @Tags 仪表盘
// @Produce json
// @Security Bearer
// @Param hours query int false "小时数"
// @Success 200 {object} models.Response{data=[]models.MetricData}
// @Router /dashboard/metrics [get]
func (h *DashboardHandler) GetMetrics(c *gin.Context) {
hours := 24
if hv := c.Query("hours"); hv != "" {
if parsed, err := strconv.Atoi(hv); err == nil && parsed > 0 && parsed <= 168 {
hours = parsed
}
}
// 从 agent_reports 表查最近 N 小时的真实数据 // Logan.Li
since := time.Now().Add(-time.Duration(hours) * time.Hour)
type avgPoint struct {
Bucket string // strftime 返回字符串 "2006-01-02 15:00",不能直接扫入 time.Time(会得零值)
AvgCPU float64
AvgMem float64
}
var rows []avgPoint
h.db.Model(&models.AgentReport{}).
Select("strftime('%Y-%m-%d %H:00', created_at) as bucket, AVG(cpu_usage) as avg_cpu, AVG(mem_usage) as avg_mem").
Where("created_at >= ?", since).
Group("bucket").
Order("bucket ASC").
Scan(&rows)
var metrics []models.MetricData
for _, r := range rows {
// 手动解析 SQLite strftime 字符串为 time.Time(GORM 无法自动转换) // Logan.Li
ts, err := time.ParseInLocation("2006-01-02 15:00", r.Bucket, time.Local)
if err != nil {
ts = time.Now()
}
metrics = append(metrics,
models.MetricData{Name: "cpu", Value: r.AvgCPU, Timestamp: ts},
models.MetricData{Name: "memory", Value: r.AvgMem, Timestamp: ts},
)
}
// 如果没有真实数据,返回空数组(不造假数据)
if metrics == nil {
metrics = []models.MetricData{}
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: metrics,
})
}