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

286 lines
11 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 (
"net/http"
"strconv"
"strings"
"ezops/model"
"time"
"github.com/gin-gonic/gin"
)
// ListMonitorAlertRules 列出中间件/站点告警规则
// GET /api/v1/alerts/monitor-rules?category=middleware|site
func (h *AlertHandler) ListMonitorAlertRules(c *gin.Context) {
q := h.db.Model(&model.MonitorAlertRule{})
if cat := c.Query("category"); cat != "" {
q = q.Where("category = ?", cat)
}
var rules []model.MonitorAlertRule
var total int64
q.Count(&total)
q.Order("id DESC").Find(&rules)
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": rules, "total": total}})
}
// CreateMonitorAlertRule 创建中间件/站点告警规则
// POST /api/v1/alerts/monitor-rules
func (h *AlertHandler) CreateMonitorAlertRule(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
}
rule := parseMonitorAlertRule(raw)
if rule.Name == "" || rule.Metric == "" {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "名称和指标不能为空"})
return
}
if rule.Category != model.MonitorAlertMiddleware && rule.Category != model.MonitorAlertSite && rule.Category != model.MonitorAlertSNMP {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "category 须为 middleware、snmp 或 site"})
return
}
if err := h.db.Create(&rule).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
return
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: rule, Message: "创建成功"})
}
// UpdateMonitorAlertRule 更新规则
// PUT /api/v1/alerts/monitor-rules/:id
func (h *AlertHandler) UpdateMonitorAlertRule(c *gin.Context) {
id := c.Param("id")
var rule model.MonitorAlertRule
if err := h.db.First(&rule, 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 := buildMonitorRuleUpdates(raw)
if len(updates) > 0 {
h.db.Model(&rule).Updates(updates)
h.db.First(&rule, id)
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: rule, Message: "更新成功"})
}
// DeleteMonitorAlertRule 删除规则
// DELETE /api/v1/alerts/monitor-rules/:id
func (h *AlertHandler) DeleteMonitorAlertRule(c *gin.Context) {
id := c.Param("id")
if err := h.db.Delete(&model.MonitorAlertRule{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
return
}
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
}
// ListUnifiedAlertRules 统一规则列表(主机 AgentRule + 中间件/站点 MonitorAlertRule)
// GET /api/v1/alerts/unified-rules?category=host|middleware|site|all
func (h *AlertHandler) ListUnifiedAlertRules(c *gin.Context) {
cat := c.DefaultQuery("category", "all")
list := make([]gin.H, 0)
if cat == "all" || cat == "host" {
var agentRules []model.AgentRule
h.db.Order("id DESC").Find(&agentRules)
for _, r := range agentRules {
list = append(list, gin.H{
"id": r.ID, "category": "host", "name": r.Name,
"target": r.Hostname, "metric": r.Metric, "operator": r.Operator,
"threshold": r.Threshold, "port": r.Port, "enabled": r.Enabled,
"severity": "warning", "description": r.Hostname + " " + r.Metric,
"rule_type": "agent",
})
}
}
if cat == "all" || cat == "middleware" || cat == "site" || cat == "snmp" {
q := h.db.Model(&model.MonitorAlertRule{})
if cat != "all" {
q = q.Where("category = ?", cat)
}
var rules []model.MonitorAlertRule
q.Order("id DESC").Find(&rules)
for _, r := range rules {
list = append(list, gin.H{
"id": r.ID, "category": r.Category, "name": r.Name,
"target_id": r.TargetID, "metric": r.Metric, "operator": r.Operator,
"threshold": r.Threshold, "enabled": r.Enabled, "severity": r.Severity,
"description": r.Description, "rule_type": "monitor",
})
}
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": list, "total": len(list)}})
}
func parseMonitorAlertRule(raw map[string]interface{}) model.MonitorAlertRule {
getStr := func(keys ...string) string {
for _, k := range keys {
if v, ok := raw[k].(string); ok {
return strings.TrimSpace(v)
}
}
return ""
}
getFloat := func(def float64, keys ...string) float64 {
for _, k := range keys {
if v, ok := raw[k].(float64); ok {
return v
}
}
return def
}
getUint := func(keys ...string) uint {
for _, k := range keys {
switch v := raw[k].(type) {
case float64:
return uint(v)
case int:
return uint(v)
}
}
return 0
}
rule := model.MonitorAlertRule{
Category: model.MonitorAlertCategory(getStr("category")),
Name: getStr("name"),
Metric: getStr("metric"),
Operator: getStr("operator", "op"),
Threshold: getFloat(0, "threshold"),
Severity: getStr("severity"),
Description: getStr("description"),
TargetID: getUint("target_id"),
Enabled: true,
}
if rule.Operator == "" {
rule.Operator = ">"
}
if rule.Severity == "" {
rule.Severity = "warning"
}
if v, ok := raw["enabled"].(bool); ok {
rule.Enabled = v
}
return rule
}
func buildMonitorRuleUpdates(raw map[string]interface{}) map[string]interface{} {
updates := map[string]interface{}{}
if v, ok := raw["name"].(string); ok && strings.TrimSpace(v) != "" {
updates["name"] = strings.TrimSpace(v)
}
if v, ok := raw["metric"].(string); ok && v != "" {
updates["metric"] = v
}
if v, ok := raw["operator"].(string); ok && v != "" {
updates["operator"] = v
}
if v, ok := raw["threshold"].(float64); ok {
updates["threshold"] = v
}
if v, ok := raw["severity"].(string); ok && v != "" {
updates["severity"] = v
}
if v, ok := raw["description"].(string); ok {
updates["description"] = v
}
if v, ok := raw["enabled"].(bool); ok {
updates["enabled"] = v
}
if v, ok := raw["target_id"].(float64); ok {
updates["target_id"] = uint(v)
}
return updates
}
// QuickSetupMonitorRules 一键创建中间件/站点默认告警规则
// POST /api/v1/alerts/monitor-quick-setup
func (h *AlertHandler) QuickSetupMonitorRules(c *gin.Context) {
var req struct {
Category string `json:"category"`
}
c.ShouldBindJSON(&req)
cat := req.Category
if cat == "" {
cat = "all"
}
created := 0
if cat == "all" || cat == "middleware" {
defaults := []model.MonitorAlertRule{
{Category: model.MonitorAlertMiddleware, Name: "Redis 连接数过高", Metric: "redis_connected_clients", Operator: ">", Threshold: 500, Severity: "warning", Enabled: true, Description: "Redis 连接数 > 500"},
{Category: model.MonitorAlertMiddleware, Name: "Redis 命中率过低", Metric: "redis_hit_rate", Operator: "<", Threshold: 0.9, Severity: "warning", Enabled: true, Description: "命中率 < 90%"},
{Category: model.MonitorAlertMiddleware, Name: "MySQL 连接数过高", Metric: "mysql_connections", Operator: ">", Threshold: 200, Severity: "warning", Enabled: true, Description: "连接数 > 200"},
{Category: model.MonitorAlertMiddleware, Name: "MySQL 慢查询过多", Metric: "mysql_slow_queries", Operator: ">", Threshold: 10, Severity: "warning", Enabled: true, Description: "慢查询 > 10"},
}
for _, r := range defaults {
var exist model.MonitorAlertRule
h.db.Where("name = ? AND category = ?", r.Name, r.Category).First(&exist)
if exist.ID == 0 {
h.db.Create(&r)
created++
}
}
}
if cat == "all" || cat == "site" {
defaults := []model.MonitorAlertRule{
{Category: model.MonitorAlertSite, Name: "拨测失败", Metric: "probe_fail", Operator: "==", Threshold: 1, Severity: "critical", Enabled: true, Description: "拨测连续失败"},
{Category: model.MonitorAlertSite, Name: "响应延迟过高", Metric: "response_time", Operator: ">", Threshold: 500, Severity: "warning", Enabled: true, Description: "延迟 > 500ms"},
{Category: model.MonitorAlertSite, Name: "可用率过低", Metric: "availability", Operator: "<", Threshold: 99, Severity: "warning", Enabled: true, Description: "1h 可用率 < 99%"},
{Category: model.MonitorAlertSite, Name: "SSL 证书即将过期", Metric: "ssl_expiry_days", Operator: "<", Threshold: 30, Severity: "warning", Enabled: true, Description: "SSL 拨测证书剩余 < 30 天"},
}
for _, r := range defaults {
var exist model.MonitorAlertRule
h.db.Where("name = ? AND category = ?", r.Name, r.Category).First(&exist)
if exist.ID == 0 {
h.db.Create(&r)
created++
}
}
}
if cat == "all" || cat == "snmp" {
defaults := []model.MonitorAlertRule{
{Category: model.MonitorAlertSNMP, Name: "SNMP CPU 过高", Metric: "snmp_cpu_usage", Operator: ">", Threshold: 80, Severity: "warning", Enabled: true, Description: "CPU > 80%"},
{Category: model.MonitorAlertSNMP, Name: "SNMP 内存过高", Metric: "snmp_mem_usage", Operator: ">", Threshold: 85, Severity: "warning", Enabled: true, Description: "内存 > 85%"},
{Category: model.MonitorAlertSNMP, Name: "SNMP 温度过高", Metric: "snmp_temperature", Operator: ">", Threshold: 65, Severity: "warning", Enabled: true, Description: "温度 > 65°C"},
{Category: model.MonitorAlertSNMP, Name: "SNMP 设备离线", Metric: "snmp_offline", Operator: "==", Threshold: 1, Severity: "critical", Enabled: true, Description: "设备离线或采集失败"},
}
for _, r := range defaults {
var exist model.MonitorAlertRule
h.db.Where("name = ? AND category = ?", r.Name, r.Category).First(&exist)
if exist.ID == 0 {
h.db.Create(&r)
created++
}
}
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"created": created}, Message: "默认规则已创建"})
}
// ResolveUnifiedAlert 统一告警恢复
// POST /api/v1/alerts/unified/:source/:id/resolve
func (h *AlertHandler) ResolveUnifiedAlert(c *gin.Context) {
source := c.Param("source")
id, _ := strconv.Atoi(c.Param("id"))
now := time.Now()
switch source {
case "agent":
h.db.Model(&model.AgentAlert{}).Where("id = ?", id).Updates(map[string]interface{}{"resolved": true, "resolved_at": now})
case "site":
h.db.Model(&model.SiteAlert{}).Where("id = ?", id).Updates(map[string]interface{}{"status": model.SiteAlertStatusResolved, "resolved_at": now})
case "event":
h.db.Model(&model.AlertEvent{}).Where("id = ?", id).Updates(map[string]interface{}{"status": model.AlertEventResolved, "resolved_at": now})
default:
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "未知告警来源"})
return
}
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已恢复"})
}