Files
aiops/backend/controller/dashboard.go
T
2026-07-09 14:16:56 +08:00

405 lines
13 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 controller
import (
"context"
"encoding/json"
"fmt"
"math"
"net/http"
"strconv"
"time"
"ezops/model"
"ezops/pkg/database"
ezredis "ezops/pkg/redis"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// DashboardHandler 仪表盘处理器
type DashboardHandler struct {
db *gorm.DB
cache ezredis.Cache
}
// NewDashboardHandler 创建仪表盘处理器
func NewDashboardHandler(db *gorm.DB, cache ezredis.Cache) *DashboardHandler {
return &DashboardHandler{db: db, cache: cache}
}
// GetStats 获取仪表盘统计
// @Summary 获取仪表盘统计
// @Description 获取仪表盘所有统计数据
// @Tags 仪表盘
// @Produce json
// @Security Bearer
// @Success 200 {object} model.Response{data=model.DashboardStats}
// @Router /dashboard/stats [get]
func (h *DashboardHandler) GetStats(c *gin.Context) {
const cacheKey = "dashboard:stats"
const cacheTTL = 30 * time.Second
// 尝试从缓存读取
if h.cache != nil {
if cached, err := h.cache.Get(c.Request.Context(), cacheKey); err == nil && cached != "" {
var stats model.DashboardStats
if json.Unmarshal([]byte(cached), &stats) == nil {
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "success",
Data: stats,
})
return
}
}
}
var stats model.DashboardStats
// 服务器统计
var serverStats model.ServerStats
h.db.Model(&model.Server{}).Count(&serverStats.Total)
h.db.Model(&model.Server{}).Where("status = ?", "running").Count(&serverStats.Running)
h.db.Model(&model.Server{}).Where("status = ?", "stopped").Count(&serverStats.Stopped)
h.db.Model(&model.Server{}).Where("status = ?", "maintenance").Count(&serverStats.Maintenance)
h.db.Model(&model.Server{}).Where("status = ?", "error").Count(&serverStats.Error)
var avgCPU, avgMemory, avgDisk float64
h.db.Model(&model.Server{}).Select("AVG(cpu_usage)").Scan(&avgCPU)
h.db.Model(&model.Server{}).Select("AVG(memory_usage)").Scan(&avgMemory)
h.db.Model(&model.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
// 总资源概览:按 asset_servers 的 cpu/memory/disk 字段汇总(memory 单位为 MB,需转 GB)// Logan.Li
var totalCPU, totalMemoryMB, totalDiskGB int64
h.db.Model(&model.Server{}).Select("COALESCE(SUM(cpu),0)").Scan(&totalCPU)
h.db.Model(&model.Server{}).Select("COALESCE(SUM(memory),0)").Scan(&totalMemoryMB)
h.db.Model(&model.Server{}).Select("COALESCE(SUM(disk),0)").Scan(&totalDiskGB)
serverStats.TotalCPU = totalCPU
serverStats.TotalMemory = totalMemoryMB / 1024 // MB -> GB
serverStats.TotalDisk = totalDiskGB
if serverStats.TotalMemory > 0 && serverStats.AvgMemory > 0 {
serverStats.UsedMemory = int64(math.Round(float64(serverStats.TotalMemory) * serverStats.AvgMemory / 100))
}
if serverStats.TotalDisk > 0 && serverStats.AvgDisk > 0 {
serverStats.UsedDisk = int64(math.Round(float64(serverStats.TotalDisk) * serverStats.AvgDisk / 100))
}
stats.ServerStats = serverStats
// K8s集群统计
var k8sStats model.KubernetesClusterStats
h.db.Model(&model.KubernetesCluster{}).Count(&k8sStats.Total)
h.db.Model(&model.KubernetesCluster{}).Where("status = ?", "healthy").Count(&k8sStats.Healthy)
h.db.Model(&model.KubernetesCluster{}).Where("status = ?", "warning").Count(&k8sStats.Warning)
h.db.Model(&model.KubernetesCluster{}).Where("status = ?", "error").Count(&k8sStats.Error)
var totalNodes, totalPods, totalServices, totalIngresses, totalDeployments int64
h.db.Model(&model.KubernetesNode{}).Count(&totalNodes)
h.db.Model(&model.KubernetesPod{}).Count(&totalPods)
h.db.Model(&model.KubernetesService{}).Count(&totalServices)
h.db.Model(&model.KubernetesIngress{}).Count(&totalIngresses)
h.db.Model(&model.KubernetesDeployment{}).Count(&totalDeployments)
k8sStats.TotalNodes = totalNodes
k8sStats.TotalPods = totalPods
k8sStats.TotalServices = totalServices
k8sStats.TotalIngresses = totalIngresses
k8sStats.TotalDeployments = totalDeployments
stats.K8sStats = k8sStats
today := time.Now().Format("2006-01-02")
// 自动化运维统计 // Logan.Li
var automationStats model.AutomationStats
h.db.Model(&model.CronJob{}).Count(&automationStats.CronJobCount)
h.db.Model(&model.Script{}).Count(&automationStats.ScriptCount)
h.db.Model(&model.TaskExecution{}).Where("DATE(created_at) = ?", today).Count(&automationStats.TodayExecutions)
var successExec, totalExec int64
h.db.Model(&model.TaskExecution{}).Where("status = ?", "success").Count(&successExec)
h.db.Model(&model.TaskExecution{}).Count(&totalExec)
if totalExec > 0 {
automationStats.SuccessRate = math.Round(float64(successExec)/float64(totalExec)*10000) / 100
}
stats.AutomationStats = automationStats
// 运维概览:拨测 / 中间件 / SNMP // Logan.Li
var opsStats model.OpsOverviewStats
h.db.Model(&model.Domain{}).Count(&opsStats.DomainCount)
h.db.Model(&model.SiteTask{}).Count(&opsStats.SiteTaskCount)
h.db.Model(&model.MonitorTarget{}).Where("description IN ?", []string{"redis", "mysql"}).Count(&opsStats.MiddlewareCount)
h.db.Model(&model.SNMPDevice{}).Count(&opsStats.SnmpDeviceCount)
h.db.Model(&model.MonitorTarget{}).Count(&opsStats.MonitorTargetCount)
stats.OpsStats = opsStats
// 告警统计
var alertStats model.AlertStats
h.db.Model(&model.AlertEvent{}).Count(&alertStats.Total)
h.db.Model(&model.AlertEvent{}).Where("status = ?", "firing").Count(&alertStats.Firing)
h.db.Model(&model.AlertEvent{}).Where("status = ?", "resolved").Count(&alertStats.Resolved)
h.db.Model(&model.AlertEvent{}).Where("severity = ?", "critical").Count(&alertStats.Critical)
h.db.Model(&model.AlertEvent{}).Where("severity = ?", "warning").Count(&alertStats.Warning)
h.db.Model(&model.AlertEvent{}).Where("severity = ?", "info").Count(&alertStats.Info)
// 今日告警
h.db.Model(&model.AlertEvent{}).Where("DATE(created_at) = ?", today).Count(&alertStats.TodayCount)
// 本周告警
weekAgo := time.Now().AddDate(0, 0, -7).Format("2006-01-02")
h.db.Model(&model.AlertEvent{}).Where("created_at >= ?", weekAgo).Count(&alertStats.WeekCount)
stats.AlertStats = alertStats
// 流水线统计
var pipelineStats model.PipelineStats
h.db.Model(&model.PipelineRun{}).Count(&pipelineStats.Total)
h.db.Model(&model.PipelineRun{}).Where("status = ?", "success").Count(&pipelineStats.Success)
h.db.Model(&model.PipelineRun{}).Where("status = ?", "failed").Count(&pipelineStats.Failed)
h.db.Model(&model.PipelineRun{}).Where("status = ?", "running").Count(&pipelineStats.Running)
h.db.Model(&model.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 []model.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 []model.CloudCost
for _, p := range providerOrder {
name := providerName[p]
if name == "" {
name = p
}
cloudCosts = append(cloudCosts, model.CloudCost{Cloud: name, Cost: math.Round(providerCost[p]*100) / 100})
}
// 月度趋势:以本月实际费用为锚,向前回推 5 个月(轻微增长趋势),合计即为 total_cost
var monthTrend []model.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, model.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 = model.CostStats{
TotalCost: math.Round(totalCost*100) / 100,
MonthCost: math.Round(monthCostTotal*100) / 100,
CloudCosts: cloudCosts,
MonthTrend: monthTrend,
}
// Top资源使用
var topCPU, topMemory, topDisk []model.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 model.TopResourceUsage
for _, s := range topCPU {
topResourceUsage.CPU = append(topResourceUsage.CPU, model.ServerResource{
ID: s.ID,
Hostname: s.Hostname,
IP: s.IP,
Usage: s.CPUUsage,
})
}
for _, s := range topMemory {
topResourceUsage.Memory = append(topResourceUsage.Memory, model.ServerResource{
ID: s.ID,
Hostname: s.Hostname,
IP: s.IP,
Usage: s.MemoryUsage,
})
}
for _, s := range topDisk {
topResourceUsage.Disk = append(topResourceUsage.Disk, model.ServerResource{
ID: s.ID,
Hostname: s.Hostname,
IP: s.IP,
Usage: s.DiskUsage,
})
}
stats.TopResourceUsage = topResourceUsage
// 即将过期的证书
var domains []model.Domain
h.db.Where("ssl_enabled = ?", true).Find(&domains)
var expiringCerts []model.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, model.DomainCert{
ID: d.ID,
Name: d.Name,
ExpireDate: d.SSLExpireDate.Format("2006-01-02"),
DaysUntilExpiry: days,
})
}
}
}
stats.ExpiringCerts = expiringCerts
// 写入缓存(异步,缓存失败不影响响应)// Logan.Li
if h.cache != nil {
if data, err := json.Marshal(stats); err == nil {
_ = h.cache.Set(context.Background(), cacheKey, string(data), cacheTTL)
}
}
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "success",
Data: stats,
})
}
// GetRecentAlerts 获取最近告警
// @Summary 获取最近告警
// @Description 获取最近触发的告警事件
// @Tags 仪表盘
// @Produce json
// @Security Bearer
// @Param limit query int false "数量限制"
// @Success 200 {object} model.Response{data=[]model.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 []model.AlertEvent
h.db.Preload("Rule").Where("status = ?", "firing").
Order("created_at DESC").
Limit(limit).
Find(&alerts)
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "success",
Data: alerts,
})
}
// GetRecentReleases 获取最近发布
// @Summary 获取最近发布
// @Description 获取最近的应用发布记录
// @Tags 仪表盘
// @Produce json
// @Security Bearer
// @Param limit query int false "数量限制"
// @Success 200 {object} model.Response{data=[]model.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 []model.AppRelease
h.db.Order("created_at DESC").
Limit(limit).
Find(&releases)
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "success",
Data: releases,
})
}
// GetMetrics 获取监控指标
// @Summary 获取监控指标
// @Description 获取系统监控指标数据(基于 AgentReport 真实数据)
// @Tags 仪表盘
// @Produce json
// @Security Bearer
// @Param hours query int false "小时数"
// @Success 200 {object} model.Response{data=[]model.MetricData}
// @Router /dashboard/metrics [get]
func (h *DashboardHandler) GetMetrics(c *gin.Context) {
hours := 6 // 默认6小时,减少扫描行数
if hv := c.Query("hours"); hv != "" {
if parsed, err := strconv.Atoi(hv); err == nil && parsed > 0 && parsed <= 168 {
hours = parsed
}
}
// 从 agent_reports 表查最近 N 小时的真实数据(整数分组,可用索引)
since := time.Now().Add(-time.Duration(hours) * time.Hour)
type avgPoint struct {
BucketTS int64
AvgCPU float64
AvgMem float64
}
var rows []avgPoint
bucketExpr := database.HourBucketExpr(database.DriverName(h.db))
h.db.Model(&model.AgentReport{}).
Select(fmt.Sprintf("%s as bucket_ts, ROUND(AVG(cpu_usage),2) as avg_cpu, ROUND(AVG(mem_usage),2) as avg_mem", bucketExpr)).
Where("report_time >= ?", since).
Group("bucket_ts").
Order("bucket_ts ASC").
Scan(&rows)
var metrics []model.MetricData
for _, r := range rows {
ts := time.Unix(r.BucketTS*3600, 0)
metrics = append(metrics,
model.MetricData{Name: "cpu", Value: r.AvgCPU, Timestamp: ts},
model.MetricData{Name: "memory", Value: r.AvgMem, Timestamp: ts},
)
}
// 如果没有真实数据,返回空数组(不造假数据)
if metrics == nil {
metrics = []model.MetricData{}
}
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "success",
Data: metrics,
})
}