package controllers import ( "strconv" "time" "lokistack/dto" "lokistack/models" "lokistack/repository" "lokistack/services" "lokistack/utils" "github.com/gin-gonic/gin" ) // AlertsController 告警规则 + 飞书通知配置 type AlertsController struct{} func NewAlertsController() *AlertsController { return &AlertsController{} } // ==================== 通知配置 ==================== // GetNotifyConfig 通知配置(只回 enabled/hasWebhook/hasSecret) func (ctrl *AlertsController) GetNotifyConfig(c *gin.Context) { cfg := services.GetNotifyConfig() utils.Success(c, services.NotifyPublicConfig{ Enabled: cfg.Enabled, HasWebhook: cfg.Webhook != "", HasSecret: cfg.SigningSecret != "", }) } // UpdateNotifyConfig 保存(webhook/签名留空=保留旧值) func (ctrl *AlertsController) UpdateNotifyConfig(c *gin.Context) { var req dto.NotifyConfigRequest if !utils.BindAndValidate(c, &req) { return } if err := services.SaveNotifyConfig(req.Enabled, req.Webhook, req.SigningSecret); err != nil { utils.Error(c, 400, err.Error()) return } utils.Success(c, gin.H{"message": "saved"}) } // TestNotifyConfig 发送测试卡片(验证 webhook 连通) func (ctrl *AlertsController) TestNotifyConfig(c *gin.Context) { cfg := services.GetNotifyConfig() if !cfg.Enabled || cfg.Webhook == "" { utils.Error(c, 400, "请先保存并启用飞书通知") return } if err := services.SendLarkCard("✅ LokiStack 通知测试", "green", []string{ "**时间**: " + time.Now().Format("2006-01-02 15:04:05"), "**说明**: 管理员在设置页手动触发的连通性测试。", }); err != nil { utils.Error(c, 502, err.Error()) return } utils.Success(c, gin.H{"status": "sent"}) } // ==================== 告警规则 CRUD ==================== func (ctrl *AlertsController) ListRules(c *gin.Context) { db := repository.GetDB() db.RLock() out := make([]map[string]interface{}, 0, len(db.AlertRules)) for i := range db.AlertRules { out = append(out, db.AlertRules[i].Sanitized()) } db.RUnlock() utils.Success(c, out) } func (ctrl *AlertsController) CreateRule(c *gin.Context) { var req dto.AlertRuleRequest if !utils.BindAndValidate(c, &req) { return } if req.WindowMin <= 0 { req.WindowMin = 15 } if req.CooldownMin <= 0 { req.CooldownMin = 60 } if req.Threshold < 0 { req.Threshold = 0 } db := repository.GetDB() rule := &models.AlertRule{ ID: db.NextID("alertRules"), Name: req.Name, SourceID: req.SourceID, ChannelIDs: req.ChannelIDs, Query: req.Query, WindowMin: req.WindowMin, Threshold: req.Threshold, Enabled: req.Enabled, CooldownMin: req.CooldownMin, } db.Lock() db.AlertRules = append(db.AlertRules, *rule) db.RequestSave() db.Unlock() utils.Success(c, rule.Sanitized()) } func (ctrl *AlertsController) UpdateRule(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { utils.BadRequest(c, "ID 无效") return } var req map[string]interface{} if !utils.BindAndValidate(c, &req) { return } db := repository.GetDB() db.Lock() defer db.Unlock() for i := range db.AlertRules { if db.AlertRules[i].ID != uint(id) { continue } if v, ok := req["name"].(string); ok && v != "" { db.AlertRules[i].Name = v } if v, ok := req["query"].(string); ok && v != "" { db.AlertRules[i].Query = v } if v, ok := req["sourceId"].(float64); ok && v > 0 { db.AlertRules[i].SourceID = uint(v) } if arr, ok := req["channelIds"].([]interface{}); ok { ids := make([]uint, 0, len(arr)) for _, item := range arr { if v, ok := item.(float64); ok && v > 0 { ids = append(ids, uint(v)) } } db.AlertRules[i].ChannelIDs = ids } if v, ok := req["windowMin"].(float64); ok && v > 0 { db.AlertRules[i].WindowMin = int(v) } if v, ok := req["threshold"].(float64); ok && v >= 0 { db.AlertRules[i].Threshold = int(v) } if v, ok := req["cooldownMin"].(float64); ok && v > 0 { db.AlertRules[i].CooldownMin = int(v) } if v, ok := req["enabled"].(bool); ok { db.AlertRules[i].Enabled = v } db.AlertRules[i].UpdatedAt = time.Now() db.RequestSave() utils.Success(c, db.AlertRules[i].Sanitized()) return } utils.NotFound(c, "规则不存在") } func (ctrl *AlertsController) DeleteRule(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { utils.BadRequest(c, "ID 无效") return } db := repository.GetDB() db.Lock() defer db.Unlock() for i := range db.AlertRules { if db.AlertRules[i].ID == uint(id) { db.AlertRules = append(db.AlertRules[:i], db.AlertRules[i+1:]...) db.RequestSave() utils.Success(c, gin.H{"message": "deleted"}) return } } utils.NotFound(c, "规则不存在") } // CheckNow 手动触发一次全量检查(不等定时器) func (ctrl *AlertsController) CheckNow(c *gin.Context) { services.RunAlertCheck(c.Request.Context()) utils.Success(c, gin.H{"message": "checked"}) } // Deliveries 发送历史(最近 50) func (ctrl *AlertsController) Deliveries(c *gin.Context) { db := repository.GetDB() db.RLock() out := make([]map[string]interface{}, 0, len(db.AlertDeliveries)) for i := len(db.AlertDeliveries) - 1; i >= 0 && len(out) < 50; i-- { d := db.AlertDeliveries[i] out = append(out, map[string]interface{}{ "id": d.ID, "ruleId": d.RuleID, "ruleName": d.RuleName, "count": d.Count, "status": d.Status, "reason": d.Reason, "createdAt": d.CreatedAt, }) } db.RUnlock() utils.Success(c, out) } // ==================== 通知通道(多 webhook) ==================== func (ctrl *AlertsController) ListChannels(c *gin.Context) { utils.Success(c, services.ListNotifyChannels()) } func (ctrl *AlertsController) SaveChannel(c *gin.Context) { var req dto.NotifyChannelRequest if !utils.BindAndValidate(c, &req) { return } // PUT /channels/:id 时路径参数优先 if pathID := c.Param("id"); pathID != "" { if v, err := strconv.ParseUint(pathID, 10, 64); err == nil { req.ID = uint(v) } } ch, err := services.SaveNotifyChannel(req.ID, req.Name, req.Webhook, req.SigningSecret, req.Enabled) if err != nil { utils.Error(c, 400, err.Error()) return } utils.Success(c, ch.Sanitized()) } func (ctrl *AlertsController) DeleteChannel(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { utils.BadRequest(c, "ID 无效") return } if err := services.DeleteNotifyChannel(uint(id)); err != nil { utils.Error(c, 400, err.Error()) return } utils.Success(c, gin.H{"message": "deleted"}) } // TestChannel 向指定通道发送测试卡片(用表单参数或已存通道) func (ctrl *AlertsController) TestChannel(c *gin.Context) { var req dto.NotifyChannelRequest if !utils.BindAndValidate(c, &req) { return } // 测试已存通道时 name 可不传(沿用原名称);直测新 webhook 时缺省名 name := req.Name if req.ID > 0 { if existing := services.GetNotifyChannel(req.ID); existing != nil && name == "" { name = existing.Name } } else if name == "" { name = "临时测试通道" } var channelID uint if req.ID > 0 { channelID = req.ID // 表单 webhook 非空时先保存再测(未保存也可测新地址) if req.Webhook != "" { if _, err := services.SaveNotifyChannel(req.ID, name, req.Webhook, req.SigningSecret, true); err != nil { utils.Error(c, 400, err.Error()) return } } } else if req.Webhook != "" { // 未保存直测:保存后可删 ch, err := services.SaveNotifyChannel(0, name, req.Webhook, req.SigningSecret, true) if err != nil { utils.Error(c, 400, err.Error()) return } channelID = ch.ID } if channelID == 0 { utils.BadRequest(c, "请提供通道 ID 或 webhook 地址") return } if err := services.SendLarkCardToChannel(channelID, "✅ LokiStack 通道测试", "green", []string{ "**时间**: " + time.Now().Format("2006-01-02 15:04:05"), "**说明**: 通道连通性测试。", }); err != nil { utils.Error(c, 502, err.Error()) return } utils.Success(c, gin.H{"status": "sent"}) }