286 lines
11 KiB
Go
286 lines
11 KiB
Go
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: "已恢复"})
|
||
}
|