feat: 监控告警体系统一重构 + 自动化任务调度引擎 + 监控大屏拆分
告警中心 - 新增统一告警(alert_unified)、监控告警规则(monitor_alert_rule)、指标目录(alert_metric_catalog) - 通知策略与消息分发(alert_notify_policy/dispatch/message)、静默规则 - 告警引擎按阈值/SNMP 拆分(alert_engine_monitor/snmp) 监控采集 - SNMP 常量/模板与历史指标(snmp templates/constants、snmp_metrics_history、snmp_collect) - 中间件监控 Redis 采集与种子(middleware_monitor_redis/seed) - 站点探测 pkg(siteprobe) + 站点视图(site_view) 自动化任务 - cron 调度器(cronscheduler)、脚本运行器(scriptrunner)、系统任务(systask)及运行记录(cron_job_run、system_task) 审计与凭证 - 审计设置(audit_setting) + 历史清理(auditcleaner) 前端 - 监控页面(host/middleware/site/snmp)拆分为子模块并大屏化 - 告警中心多 Tab(规则/指标目录/通知策略/静默)、任务管理组件重构、Agent CLI Markdown 渲染 - 移除旧脚本卡片/详情/日志组件,统一为 ScriptTable/ScriptCodeEditor/TerminalOutput Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
092d0e4b9e
commit
471353e3d7
190 files changed
+16640
-3484
No files matched your search
+166
-64
@@ -1,9 +1,13 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"ezops/model"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/model"
|
||||
"ezops/pkg/siteprobe"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -11,60 +15,79 @@ import (
|
||||
|
||||
// SiteHandler 站点拨测管理 handler // Logan.Li
|
||||
type SiteHandler struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
worker *siteprobe.Worker
|
||||
}
|
||||
|
||||
// NewSiteHandler 创建站点拨测 handler
|
||||
func NewSiteHandler(db *gorm.DB) *SiteHandler {
|
||||
return &SiteHandler{db: db}
|
||||
func NewSiteHandler(db *gorm.DB, worker *siteprobe.Worker) *SiteHandler {
|
||||
return &SiteHandler{db: db, worker: worker}
|
||||
}
|
||||
|
||||
// ========== Site Task CRUD ==========
|
||||
|
||||
// ListTasks 列出拨测任务
|
||||
// GET /api/v1/site/tasks?page=1&page_size=20&type=&status=
|
||||
// GET /api/v1/site/tasks
|
||||
func (h *SiteHandler) ListTasks(c *gin.Context) {
|
||||
var tasks []model.SiteTask
|
||||
q := h.db.Model(&model.SiteTask{})
|
||||
|
||||
if t := c.Query("type"); t != "" {
|
||||
q = q.Where("type = ?", t)
|
||||
}
|
||||
if s := c.Query("status"); s != "" {
|
||||
q = q.Where("status = ?", s)
|
||||
q = q.Where("task_type = ?", strings.ToLower(t))
|
||||
}
|
||||
if kw := c.Query("keyword"); kw != "" {
|
||||
q = q.Where("name LIKE ? OR url LIKE ?", "%"+kw+"%", "%"+kw+"%")
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "100"))
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
q.Offset((page - 1) * pageSize).Limit(pageSize).Order("name ASC").Find(&tasks)
|
||||
q.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&tasks)
|
||||
|
||||
if tasks == nil {
|
||||
tasks = []model.SiteTask{}
|
||||
views := make([]SiteTaskView, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
if task.TaskType == "" {
|
||||
inferred := model.SiteTaskType(siteprobe.InferTaskTypePublic(task))
|
||||
h.db.Model(&task).Update("task_type", inferred)
|
||||
task.TaskType = inferred
|
||||
}
|
||||
views = append(views, toSiteTaskView(task))
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": tasks, "total": total}})
|
||||
enrichSiteTaskViewsSSL(h.db, views)
|
||||
enrichSiteTaskViewsAlert(h.db, views)
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": views, "total": total}})
|
||||
}
|
||||
|
||||
// CreateTask 创建拨测任务
|
||||
// POST /api/v1/site/tasks
|
||||
func (h *SiteHandler) CreateTask(c *gin.Context) {
|
||||
var task model.SiteTask
|
||||
if err := c.ShouldBindJSON(&task); err != nil {
|
||||
var raw map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&raw); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
task := parseCreateTaskRequest(raw)
|
||||
if task.Name == "" || task.URL == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "名称和地址不能为空"})
|
||||
return
|
||||
}
|
||||
if task.Frequency < 10 {
|
||||
task.Frequency = 60
|
||||
}
|
||||
if task.Timeout <= 0 {
|
||||
task.Timeout = 10
|
||||
}
|
||||
if err := h.db.Create(&task).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: task, Message: "创建成功"})
|
||||
if h.worker != nil {
|
||||
go h.worker.RunTaskNow(task.ID)
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: toSiteTaskView(task), Message: "创建成功"})
|
||||
}
|
||||
|
||||
// UpdateTask 更新拨测任务
|
||||
// UpdateTask 更新拨测任务(启用/停用或配置变更)
|
||||
// PUT /api/v1/site/tasks/:id
|
||||
func (h *SiteHandler) UpdateTask(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
@@ -73,19 +96,72 @@ func (h *SiteHandler) UpdateTask(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "任务不存在"})
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&task); err != nil {
|
||||
|
||||
var raw map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&raw); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
h.db.Save(&task)
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: task, Message: "更新成功"})
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if v, ok := raw["name"].(string); ok && strings.TrimSpace(v) != "" {
|
||||
updates["name"] = strings.TrimSpace(v)
|
||||
}
|
||||
if v, ok := raw["url"].(string); ok && strings.TrimSpace(v) != "" {
|
||||
updates["url"] = strings.TrimSpace(v)
|
||||
}
|
||||
if v, ok := raw["type"].(string); ok && v != "" {
|
||||
t := strings.ToLower(v)
|
||||
if t == "https" {
|
||||
t = "http"
|
||||
}
|
||||
updates["task_type"] = t
|
||||
}
|
||||
if v, ok := raw["frequency_sec"].(float64); ok {
|
||||
updates["frequency"] = int(v)
|
||||
}
|
||||
if v, ok := raw["timeout_sec"].(float64); ok {
|
||||
updates["timeout"] = int(v)
|
||||
}
|
||||
if v, ok := raw["expected_status_code"].(float64); ok {
|
||||
updates["expected_status"] = int(v)
|
||||
}
|
||||
if v, ok := raw["enabled"].(bool); ok {
|
||||
if v {
|
||||
updates["status"] = model.SiteTaskStatusRunning
|
||||
} else {
|
||||
updates["status"] = model.SiteTaskStatusPaused
|
||||
}
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
h.db.Model(&task).Updates(updates)
|
||||
h.db.First(&task, id)
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: toSiteTaskView(task), Message: "更新成功"})
|
||||
}
|
||||
|
||||
// deleteSiteTaskCascade 先删子表再删任务,避免外键约束 // Logan.Li
|
||||
func (h *SiteHandler) deleteSiteTaskCascade(taskID uint) error {
|
||||
return h.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("task_id = ?", taskID).Delete(&model.SiteResult{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("task_id = ?", taskID).Delete(&model.SiteAlert{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
tx.Where("category = ? AND target_id = ?", model.MonitorAlertSite, taskID).Delete(&model.MonitorAlertRule{})
|
||||
return tx.Delete(&model.SiteTask{}, taskID).Error
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTask 删除拨测任务
|
||||
// DELETE /api/v1/site/tasks/:id
|
||||
func (h *SiteHandler) DeleteTask(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.db.Delete(&model.SiteTask{}, id).Error; err != nil {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效 ID"})
|
||||
return
|
||||
}
|
||||
if err := h.deleteSiteTaskCascade(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -93,74 +169,100 @@ func (h *SiteHandler) DeleteTask(c *gin.Context) {
|
||||
}
|
||||
|
||||
// BatchDeleteTasks 批量删除
|
||||
// DELETE /api/v1/site/tasks/batch-delete
|
||||
func (h *SiteHandler) BatchDeleteTasks(c *gin.Context) {
|
||||
var req model.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
h.db.Where("id IN ?", req.IDs).Delete(&model.SiteTask{})
|
||||
for _, tid := range req.IDs {
|
||||
if err := h.deleteSiteTaskCascade(uint(tid)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "批量删除成功"})
|
||||
}
|
||||
|
||||
// ========== Site Results ==========
|
||||
|
||||
// ListResults 查询拨测结果
|
||||
// GET /api/v1/site/results?task_id=N&from=&to=&limit=100
|
||||
// GET /api/v1/site/results?task_id=N&limit=100
|
||||
func (h *SiteHandler) ListResults(c *gin.Context) {
|
||||
var results []model.SiteResult
|
||||
q := h.db.Model(&model.SiteResult{})
|
||||
|
||||
if tid := c.Query("task_id"); tid != "" {
|
||||
q = q.Where("task_id = ?", tid)
|
||||
}
|
||||
if from := c.Query("from"); from != "" {
|
||||
q = q.Where("created_at >= ?", from)
|
||||
if h := c.Query("hours"); h != "" {
|
||||
if hours, err := strconv.Atoi(h); err == nil && hours > 0 {
|
||||
if hours > 168 {
|
||||
hours = 168
|
||||
}
|
||||
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
||||
q = q.Where("checked_at >= ?", since)
|
||||
}
|
||||
}
|
||||
if to := c.Query("to"); to != "" {
|
||||
q = q.Where("created_at <= ?", to)
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "500"))
|
||||
if limit <= 0 {
|
||||
limit = 500
|
||||
}
|
||||
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "100"))
|
||||
q.Order("created_at DESC").Limit(limit).Find(&results)
|
||||
|
||||
if limit > 2000 {
|
||||
limit = 2000
|
||||
}
|
||||
q.Order("checked_at DESC").Limit(limit).Find(&results)
|
||||
if results == nil {
|
||||
results = []model.SiteResult{}
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: results})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": results, "total": len(results)}})
|
||||
}
|
||||
|
||||
// ========== Site Statistics ==========
|
||||
// ProbeNow 立即触发拨测
|
||||
// POST /api/v1/site/tasks/:id/probe
|
||||
func (h *SiteHandler) ProbeNow(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效 ID"})
|
||||
return
|
||||
}
|
||||
if h.worker == nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "拨测引擎未启动"})
|
||||
return
|
||||
}
|
||||
if err := h.worker.RunTaskNow(uint(id)); err != nil {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "任务不存在"})
|
||||
return
|
||||
}
|
||||
var task model.SiteTask
|
||||
h.db.First(&task, id)
|
||||
view := toSiteTaskView(task)
|
||||
enrichSiteTaskViewsSSL(h.db, []SiteTaskView{view})
|
||||
enrichSiteTaskViewsAlert(h.db, []SiteTaskView{view})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: view, Message: "拨测完成"})
|
||||
}
|
||||
|
||||
// GetSiteStats 获取拨测概览统计
|
||||
// GET /api/v1/site/stats
|
||||
func (h *SiteHandler) GetSiteStats(c *gin.Context) {
|
||||
var total, normal, abnormal int64
|
||||
var total, enabled, upCount, downCount int64
|
||||
h.db.Model(&model.SiteTask{}).Count(&total)
|
||||
h.db.Model(&model.SiteTask{}).Where("status = ?", "active").Count(&normal)
|
||||
h.db.Model(&model.SiteTask{}).Where("status = ?", "error").Count(&abnormal)
|
||||
h.db.Model(&model.SiteTask{}).Where("status = ?", model.SiteTaskStatusRunning).Count(&enabled)
|
||||
h.db.Model(&model.SiteTask{}).Where("last_result = ?", "success").Count(&upCount)
|
||||
h.db.Model(&model.SiteTask{}).Where("last_result IN ?", []string{"fail", "timeout"}).Count(&downCount)
|
||||
|
||||
// Calculate availability rate from recent results
|
||||
var availRate float64
|
||||
if total > 0 {
|
||||
var successCount int64
|
||||
h.db.Model(&model.SiteResult{}).
|
||||
Where("created_at > datetime('now', '-1 hour') AND status = 'success'").
|
||||
Count(&successCount)
|
||||
var totalRecent int64
|
||||
h.db.Model(&model.SiteResult{}).
|
||||
Where("created_at > datetime('now', '-1 hour')").
|
||||
Count(&totalRecent)
|
||||
if totalRecent > 0 {
|
||||
availRate = float64(successCount) / float64(totalRecent) * 100
|
||||
}
|
||||
var availability float64
|
||||
since := time.Now().Add(-24 * time.Hour)
|
||||
var successRecent, totalRecent int64
|
||||
h.db.Model(&model.SiteResult{}).Where("checked_at >= ? AND status = ?", since, model.SiteResultSuccess).Count(&successRecent)
|
||||
h.db.Model(&model.SiteResult{}).Where("checked_at >= ?", since).Count(&totalRecent)
|
||||
if totalRecent > 0 {
|
||||
availability = float64(successRecent) / float64(totalRecent)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"total": total,
|
||||
"normal": normal,
|
||||
"abnormal": abnormal,
|
||||
"avail_rate": availRate,
|
||||
"total_tasks": total,
|
||||
"enabled_tasks": enabled,
|
||||
"up_count": upCount,
|
||||
"down_count": downCount,
|
||||
"availability": availability,
|
||||
"probe_count": totalRecent,
|
||||
}})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user