332 lines
10 KiB
Go
332 lines
10 KiB
Go
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.KubernetesClusterStats
|
|
h.db.Model(&models.KubernetesCluster{}).Count(&k8sStats.Total)
|
|
h.db.Model(&models.KubernetesCluster{}).Where("status = ?", "healthy").Count(&k8sStats.Healthy)
|
|
h.db.Model(&models.KubernetesCluster{}).Where("status = ?", "warning").Count(&k8sStats.Warning)
|
|
h.db.Model(&models.KubernetesCluster{}).Where("status = ?", "error").Count(&k8sStats.Error)
|
|
|
|
var totalNodes, totalPods, totalServices int64
|
|
h.db.Model(&models.KubernetesNode{}).Count(&totalNodes)
|
|
h.db.Model(&models.KubernetesPod{}).Count(&totalPods)
|
|
h.db.Model(&models.KubernetesService{}).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 小时的真实数据(整数分组,可用索引)
|
|
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
|
type avgPoint struct {
|
|
BucketTS int64
|
|
AvgCPU float64
|
|
AvgMem float64
|
|
}
|
|
var rows []avgPoint
|
|
h.db.Model(&models.AgentReport{}).
|
|
Select("UNIX_TIMESTAMP(report_time) DIV 3600 as bucket_ts, ROUND(AVG(cpu_usage),2) as avg_cpu, ROUND(AVG(mem_usage),2) as avg_mem").
|
|
Where("report_time >= ?", since).
|
|
Group("bucket_ts").
|
|
Order("bucket_ts ASC").
|
|
Scan(&rows)
|
|
|
|
var metrics []models.MetricData
|
|
for _, r := range rows {
|
|
ts := time.Unix(r.BucketTS*3600, 0)
|
|
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,
|
|
})
|
|
}
|