feat(audit): F1+F2 告警静默规则标记完成 + 审计日志CSV导出实现
F1 告警静默规则 [done]: - 后端 alert_silence.go: CRUD + Toggle (onetime/daily/weekly 三模式) - 告警引擎 isSilenced 集成 (alert_engine.go) - 前端 SilenceRuleTab + SilenceDialog - 路由 /alerts/silences 已注册 F2 审计日志导出 [done]: - 重写 ExportAuditLogs: 真正的 CSV 导出 (encoding/csv + UTF-8 BOM) - 支持 module/action/username/startTime/endTime 筛选 - 导出限制 10000 条防溢出,自动记录审计日志 - JSON 格式向后兼容 (format=json) - 前端 exportLogs 传递查询参数 同时包含累积的前期工作: - 目录重构 internal/ -> controller/model/router (v0.55-v0.57) - 后端分层架构 dto/repository/service/utils - 审计模块全功能 (终端审计/操作审计/登录日志) - asciicast v2 录制格式 - SFTP 文件浏览器 - StatStrip 紧凑统计条 - 在线会话自动刷新 - LiveMonitor WebSocket 诊断修复 验证: go build ✅ | npm run build ✅ | Docker 部署 ✅ | 10 回归端点 ✅ | CSV 导出 ✅ | 静默 CRUD ✅
This commit is contained in:
1 parent
e57718998a
commit
77c234b060
314 files changed
+10050
-4065
No files matched your search
@@ -0,0 +1,260 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 告警静默规则 CRUD(AlertHandler 方法接收者) // Logan.Li
|
||||
|
||||
// CreateSilenceRequest 创建静默规则请求
|
||||
type CreateSilenceRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
RuleIDs []uint `json:"rule_ids"`
|
||||
TargetIDs []uint `json:"target_ids"`
|
||||
Matchers []model.AlertSilenceMatcher `json:"matchers"`
|
||||
SilenceType string `json:"silence_type"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time" binding:"required"`
|
||||
Weekdays []int `json:"weekdays"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
|
||||
// UpdateSilenceRequest 更新静默规则请求
|
||||
type UpdateSilenceRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
RuleIDs []uint `json:"rule_ids"`
|
||||
TargetIDs []uint `json:"target_ids"`
|
||||
Matchers []model.AlertSilenceMatcher `json:"matchers"`
|
||||
SilenceType string `json:"silence_type"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
Weekdays []int `json:"weekdays"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
|
||||
// ListSilences GET /alerts/silences
|
||||
func (h *AlertHandler) ListSilences(c *gin.Context) {
|
||||
var silences []model.AlertSilence
|
||||
query := h.db.Order("created_at DESC")
|
||||
|
||||
if status := c.Query("status"); status != "" {
|
||||
if status == "active" {
|
||||
now := time.Now()
|
||||
query = query.Where("status = ? AND start_time <= ? AND end_time >= ?", 1, now, now)
|
||||
} else if s, err := strconv.Atoi(status); err == nil {
|
||||
query = query.Where("status = ?", s)
|
||||
}
|
||||
}
|
||||
|
||||
query.Find(&silences)
|
||||
|
||||
// 解析 JSON 字段为前端友好的格式
|
||||
type SilenceWithParsed struct {
|
||||
model.AlertSilence
|
||||
RuleIDList []uint `json:"rule_id_list"`
|
||||
TargetIDList []uint `json:"target_id_list"`
|
||||
MatcherList []model.AlertSilenceMatcher `json:"matcher_list"`
|
||||
WeekdayList []int `json:"weekday_list"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
var result []SilenceWithParsed
|
||||
now := time.Now()
|
||||
for _, s := range silences {
|
||||
item := SilenceWithParsed{AlertSilence: s}
|
||||
if s.RuleIDs != "" {
|
||||
json.Unmarshal([]byte(s.RuleIDs), &item.RuleIDList)
|
||||
}
|
||||
if s.TargetIDs != "" {
|
||||
json.Unmarshal([]byte(s.TargetIDs), &item.TargetIDList)
|
||||
}
|
||||
if s.Matchers != "" {
|
||||
json.Unmarshal([]byte(s.Matchers), &item.MatcherList)
|
||||
}
|
||||
if s.Weekdays != "" {
|
||||
json.Unmarshal([]byte(s.Weekdays), &item.WeekdayList)
|
||||
}
|
||||
item.IsActive = s.Status == 1 && now.After(s.StartTime) && now.Before(s.EndTime)
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: model.ListResponse{List: result, Total: int64(len(result))},
|
||||
})
|
||||
}
|
||||
|
||||
// CreateSilence POST /alerts/silences
|
||||
func (h *AlertHandler) CreateSilence(c *gin.Context) {
|
||||
var req CreateSilenceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
silenceType := model.AlertSilenceOneTime
|
||||
if req.SilenceType != "" {
|
||||
silenceType = model.AlertSilenceType(req.SilenceType)
|
||||
}
|
||||
|
||||
// 解析时间
|
||||
var startTime, endTime time.Time
|
||||
var err error
|
||||
if req.StartTime != "" {
|
||||
startTime, err = time.Parse(time.RFC3339, req.StartTime)
|
||||
if err != nil {
|
||||
startTime, err = time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local)
|
||||
if err != nil {
|
||||
startTime = time.Now()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startTime = time.Now()
|
||||
}
|
||||
|
||||
endTime, err = time.Parse(time.RFC3339, req.EndTime)
|
||||
if err != nil {
|
||||
endTime, err = time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "结束时间格式错误"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ruleIDsJSON, _ := json.Marshal(req.RuleIDs)
|
||||
targetIDsJSON, _ := json.Marshal(req.TargetIDs)
|
||||
matchersJSON, _ := json.Marshal(req.Matchers)
|
||||
weekdaysJSON, _ := json.Marshal(req.Weekdays)
|
||||
|
||||
silence := model.AlertSilence{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
RuleIDs: string(ruleIDsJSON),
|
||||
TargetIDs: string(targetIDsJSON),
|
||||
Matchers: string(matchersJSON),
|
||||
SilenceType: silenceType,
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
Weekdays: string(weekdaysJSON),
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
// 获取创建人 ID
|
||||
if userID, exists := c.Get("userID"); exists {
|
||||
if uid, ok := userID.(uint); ok {
|
||||
silence.CreatedBy = uid
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.db.Create(&silence).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: silence})
|
||||
}
|
||||
|
||||
// UpdateSilence PUT /alerts/silences/:id
|
||||
func (h *AlertHandler) UpdateSilence(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var req UpdateSilenceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
var silence model.AlertSilence
|
||||
if err := h.db.First(&silence, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "静默规则不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if req.Name != "" {
|
||||
updates["name"] = req.Name
|
||||
}
|
||||
if req.Description != "" {
|
||||
updates["description"] = req.Description
|
||||
}
|
||||
if req.SilenceType != "" {
|
||||
updates["silence_type"] = req.SilenceType
|
||||
}
|
||||
if len(req.RuleIDs) > 0 || req.RuleIDs != nil {
|
||||
ruleIDsJSON, _ := json.Marshal(req.RuleIDs)
|
||||
updates["rule_ids"] = string(ruleIDsJSON)
|
||||
}
|
||||
if len(req.TargetIDs) > 0 || req.TargetIDs != nil {
|
||||
targetIDsJSON, _ := json.Marshal(req.TargetIDs)
|
||||
updates["target_ids"] = string(targetIDsJSON)
|
||||
}
|
||||
if len(req.Matchers) > 0 || req.Matchers != nil {
|
||||
matchersJSON, _ := json.Marshal(req.Matchers)
|
||||
updates["matchers"] = string(matchersJSON)
|
||||
}
|
||||
if len(req.Weekdays) > 0 || req.Weekdays != nil {
|
||||
weekdaysJSON, _ := json.Marshal(req.Weekdays)
|
||||
updates["weekdays"] = string(weekdaysJSON)
|
||||
}
|
||||
if req.StartTime != "" {
|
||||
if st, err := time.Parse(time.RFC3339, req.StartTime); err == nil {
|
||||
updates["start_time"] = st
|
||||
} else if st, err := time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local); err == nil {
|
||||
updates["start_time"] = st
|
||||
}
|
||||
}
|
||||
if req.EndTime != "" {
|
||||
if et, err := time.Parse(time.RFC3339, req.EndTime); err == nil {
|
||||
updates["end_time"] = et
|
||||
} else if et, err := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local); err == nil {
|
||||
updates["end_time"] = et
|
||||
}
|
||||
}
|
||||
if req.Status != nil {
|
||||
updates["status"] = *req.Status
|
||||
}
|
||||
|
||||
h.db.Model(&silence).Updates(updates)
|
||||
h.db.First(&silence, id)
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: silence})
|
||||
}
|
||||
|
||||
// DeleteSilence DELETE /alerts/silences/:id
|
||||
func (h *AlertHandler) DeleteSilence(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := h.db.Delete(&model.AlertSilence{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// ToggleSilence POST /alerts/silences/:id/toggle
|
||||
func (h *AlertHandler) ToggleSilence(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var silence model.AlertSilence
|
||||
if err := h.db.First(&silence, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "静默规则不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
newStatus := 0
|
||||
if silence.Status == 0 {
|
||||
newStatus = 1
|
||||
}
|
||||
|
||||
h.db.Model(&silence).Update("status", newStatus)
|
||||
h.db.First(&silence, id)
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "切换成功", Data: silence})
|
||||
}
|
||||
Reference in new issue
Block a user