Files
aiops/backend/controller/site.go
T
LoganandClaude 471353e3d7 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>
2026-07-01 23:44:45 +08:00

268 lines
8.2 KiB
Go

package controller
import (
"net/http"
"strconv"
"strings"
"time"
"ezops/model"
"ezops/pkg/siteprobe"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// SiteHandler 站点拨测管理 handler // Logan.Li
type SiteHandler struct {
db *gorm.DB
worker *siteprobe.Worker
}
// NewSiteHandler 创建站点拨测 handler
func NewSiteHandler(db *gorm.DB, worker *siteprobe.Worker) *SiteHandler {
return &SiteHandler{db: db, worker: worker}
}
// ListTasks 列出拨测任务
// 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("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", "100"))
var total int64
q.Count(&total)
q.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&tasks)
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))
}
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 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
}
if h.worker != nil {
go h.worker.RunTaskNow(task.ID)
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: toSiteTaskView(task), Message: "创建成功"})
}
// UpdateTask 更新拨测任务(启用/停用或配置变更)
// PUT /api/v1/site/tasks/:id
func (h *SiteHandler) UpdateTask(c *gin.Context) {
id := c.Param("id")
var task model.SiteTask
if err := h.db.First(&task, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "任务不存在"})
return
}
var raw map[string]interface{}
if err := c.ShouldBindJSON(&raw); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
return
}
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 删除拨测任务
func (h *SiteHandler) DeleteTask(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 err := h.deleteSiteTaskCascade(uint(id)); err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
return
}
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
}
// BatchDeleteTasks 批量删除
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
}
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: "批量删除成功"})
}
// ListResults 查询拨测结果
// 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 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)
}
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "500"))
if limit <= 0 {
limit = 500
}
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: gin.H{"list": results, "total": len(results)}})
}
// 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, enabled, upCount, downCount int64
h.db.Model(&model.SiteTask{}).Count(&total)
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)
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_tasks": total,
"enabled_tasks": enabled,
"up_count": upCount,
"down_count": downCount,
"availability": availability,
"probe_count": totalRecent,
}})
}