feat: add monitoring agent (Go) + agent report API + cloud billing UI
- Go agent: CPU/mem/disk/port collectors, HTTP report, static binary - Cross-compile: linux/amd64 + linux/arm64 (statically linked) - One-line install script for all Linux distros - Backend: POST /api/v1/agent/report (no JWT) + GET /agent/latest,history (JWT) - CloudBilling.tsx: fixed syntax errors, stats/trends/records/alerts - K8s menu renamed to Kubernetes管理 Signed-off-by: Logan.Li
This commit is contained in:
1 parent
77d13b07d5
commit
ac0b8e75c0
17 files changed
+1267
No files matched your search
@@ -0,0 +1,110 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AgentHandler 处理 Agent 上报数据 // Logan.Li
|
||||
type AgentHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAgentHandler(db *gorm.DB) *AgentHandler {
|
||||
return &AgentHandler{db: db}
|
||||
}
|
||||
|
||||
// ReceiveReport 接收 Agent 上报的指标数据
|
||||
// POST /api/v1/agent/report
|
||||
func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
var req models.AgentReportRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求格式错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析上报时间
|
||||
reportTime := time.Now()
|
||||
if req.Timestamp != "" {
|
||||
if t, err := time.Parse(time.RFC3339, req.Timestamp); err == nil {
|
||||
reportTime = t
|
||||
}
|
||||
}
|
||||
|
||||
// 序列化磁盘和端口信息
|
||||
diskJSON, _ := json.Marshal(req.Disks)
|
||||
portJSON, _ := json.Marshal(req.Ports)
|
||||
|
||||
// 存储到数据库
|
||||
report := models.AgentReport{
|
||||
Hostname: req.Hostname,
|
||||
CPUUsage: req.CPU.UsagePercent,
|
||||
CPUCores: req.CPU.CoreCount,
|
||||
Load1: req.CPU.Load1,
|
||||
Load5: req.CPU.Load5,
|
||||
Load15: req.CPU.Load15,
|
||||
MemTotal: req.Memory.TotalGB,
|
||||
MemUsed: req.Memory.UsedGB,
|
||||
MemUsage: req.Memory.UsagePercent,
|
||||
DiskInfo: string(diskJSON),
|
||||
PortInfo: string(portJSON),
|
||||
ReportTime: reportTime,
|
||||
}
|
||||
|
||||
if err := h.db.Create(&report).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "保存失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 保存成功
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "ok"})
|
||||
}
|
||||
|
||||
// GetLatestReports 获取各主机最新上报
|
||||
// GET /api/v1/agent/latest
|
||||
func (h *AgentHandler) GetLatestReports(c *gin.Context) {
|
||||
// 子查询:每个 hostname 最新的一条记录
|
||||
var reports []models.AgentReport
|
||||
h.db.Raw(`
|
||||
SELECT * FROM agent_reports
|
||||
WHERE id IN (
|
||||
SELECT MAX(id) FROM agent_reports GROUP BY hostname
|
||||
)
|
||||
ORDER BY report_time DESC
|
||||
`).Scan(&reports)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: reports,
|
||||
})
|
||||
}
|
||||
|
||||
// GetHostHistory 获取指定主机的历史指标
|
||||
// GET /api/v1/agent/history/:hostname?hours=1
|
||||
func (h *AgentHandler) GetHostHistory(c *gin.Context) {
|
||||
hostname := c.Param("hostname")
|
||||
hours := 1
|
||||
if v := c.Query("hours"); v != "" {
|
||||
fmt.Sscanf(v, "%d", &hours)
|
||||
}
|
||||
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
||||
|
||||
var reports []models.AgentReport
|
||||
h.db.Where("hostname = ? AND report_time >= ?", hostname, since).
|
||||
Order("report_time ASC").
|
||||
Find(&reports)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: reports,
|
||||
})
|
||||
}
|
||||
@@ -289,4 +289,17 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
|
||||
audit.GET("/statistics", auditHandler.GetAuditStatistics)
|
||||
audit.POST("/logs/clear", auditHandler.ClearAuditLogs)
|
||||
}
|
||||
|
||||
// Agent 上报路由(无需 JWT,使用 Agent Token 鉴权)
|
||||
agentHandler := handlers.NewAgentHandler(db)
|
||||
agentGroup := api.Group("/agent")
|
||||
{
|
||||
agentGroup.POST("/report", agentHandler.ReceiveReport)
|
||||
}
|
||||
// Agent 数据查询(需要 JWT)
|
||||
agentQuery := authorized.Group("/agent")
|
||||
{
|
||||
agentQuery.GET("/latest", agentHandler.GetLatestReports)
|
||||
agentQuery.GET("/history/:hostname", agentHandler.GetHostHistory)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AgentReport Agent 上报的原始指标数据 // Logan.Li
|
||||
type AgentReport struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Hostname string `gorm:"size:255;index;not null" json:"hostname"`
|
||||
CPUUsage float64 `gorm:"type:decimal(6,2)" json:"cpu_usage"` // CPU 使用率 %
|
||||
CPUCores int `json:"cpu_cores"` // CPU 核心数
|
||||
Load1 float64 `gorm:"type:decimal(8,2)" json:"load1"` // 1分钟负载
|
||||
Load5 float64 `gorm:"type:decimal(8,2)" json:"load5"` // 5分钟负载
|
||||
Load15 float64 `gorm:"type:decimal(8,2)" json:"load15"` // 15分钟负载
|
||||
MemTotal float64 `gorm:"type:decimal(10,2)" json:"mem_total"` // 总内存 GB
|
||||
MemUsed float64 `gorm:"type:decimal(10,2)" json:"mem_used"` // 已用内存 GB
|
||||
MemUsage float64 `gorm:"type:decimal(6,2)" json:"mem_usage"` // 内存使用率 %
|
||||
DiskInfo string `gorm:"type:text" json:"disk_info"` // 磁盘信息 JSON
|
||||
PortInfo string `gorm:"type:text" json:"port_info"` // 端口信息 JSON
|
||||
ReportTime time.Time `json:"report_time"` // 上报时间
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
func (AgentReport) TableName() string {
|
||||
return "agent_reports"
|
||||
}
|
||||
|
||||
// AgentReportRequest Agent 上报请求体 // Logan.Li
|
||||
type AgentReportRequest struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
CPU CPUMetric `json:"cpu"`
|
||||
Memory MemMetric `json:"memory"`
|
||||
Disks []DiskMetric `json:"disks"`
|
||||
Ports []PortMetric `json:"ports"`
|
||||
}
|
||||
|
||||
type CPUMetric struct {
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
CoreCount int `json:"core_count"`
|
||||
Load1 float64 `json:"load1"`
|
||||
Load5 float64 `json:"load5"`
|
||||
Load15 float64 `json:"load15"`
|
||||
}
|
||||
|
||||
type MemMetric struct {
|
||||
TotalGB float64 `json:"total_gb"`
|
||||
UsedGB float64 `json:"used_gb"`
|
||||
CachedGB float64 `json:"cached_gb"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
}
|
||||
|
||||
type DiskMetric struct {
|
||||
Mount string `json:"mount"`
|
||||
Device string `json:"device"`
|
||||
TotalGB float64 `json:"total_gb"`
|
||||
UsedGB float64 `json:"used_gb"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
}
|
||||
|
||||
type PortMetric struct {
|
||||
Port int `json:"port"`
|
||||
Protocol string `json:"protocol"`
|
||||
Process string `json:"process"`
|
||||
PID int `json:"pid"`
|
||||
}
|
||||
Reference in new issue
Block a user