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,308 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ────────────────────────────────────────
|
||||
// HealthProbe CRUD(健康探针配置管理)
|
||||
// ────────────────────────────────────────
|
||||
|
||||
// ListHealthProbes GET /api/v1/agent/health-probes
|
||||
func (h *AgentHandler) ListHealthProbes(c *gin.Context) {
|
||||
query := h.db.Model(&model.HealthProbe{})
|
||||
|
||||
if hostname := c.Query("hostname"); hostname != "" {
|
||||
query = query.Where("hostname = ?", hostname)
|
||||
}
|
||||
if enabled := c.Query("enabled"); enabled != "" {
|
||||
if enabled == "true" {
|
||||
query = query.Where("enabled = ?", true)
|
||||
} else if enabled == "false" {
|
||||
query = query.Where("enabled = ?", false)
|
||||
}
|
||||
}
|
||||
|
||||
var probes []model.HealthProbe
|
||||
query.Order("created_at DESC").Limit(500).Find(&probes)
|
||||
|
||||
if probes == nil {
|
||||
probes = []model.HealthProbe{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: probes,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateHealthProbe POST /api/v1/agent/health-probes
|
||||
func (h *AgentHandler) CreateHealthProbe(c *gin.Context) {
|
||||
var probe model.HealthProbe
|
||||
if err := c.ShouldBindJSON(&probe); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if probe.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "探针名称不能为空"})
|
||||
return
|
||||
}
|
||||
if probe.Hostname == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "目标主机不能为空"})
|
||||
return
|
||||
}
|
||||
if probe.ProbeType == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "探针类型不能为空"})
|
||||
return
|
||||
}
|
||||
// 验证探针类型配置
|
||||
switch probe.ProbeType {
|
||||
case model.ProbeTypeHTTP:
|
||||
if probe.HTTPURL == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "HTTP 探针必须填写 URL"})
|
||||
return
|
||||
}
|
||||
case model.ProbeTypeTCP:
|
||||
if probe.TCPHost == "" || probe.TCPPort == 0 {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "TCP 探针必须填写 Host 和 Port"})
|
||||
return
|
||||
}
|
||||
case model.ProbeTypeExec:
|
||||
if probe.ExecCommand == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "Exec 探针必须填写执行命令"})
|
||||
return
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "不支持的探针类型: " + string(probe.ProbeType)})
|
||||
return
|
||||
}
|
||||
|
||||
probe.ID = 0
|
||||
probe.Enabled = true
|
||||
if probe.Timeout == 0 {
|
||||
probe.Timeout = 10
|
||||
}
|
||||
if probe.Interval == 0 {
|
||||
probe.Interval = 60
|
||||
}
|
||||
|
||||
if err := h.db.Create(&probe).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: probe})
|
||||
}
|
||||
|
||||
// UpdateHealthProbe PUT /api/v1/agent/health-probes/:id
|
||||
func (h *AgentHandler) UpdateHealthProbe(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var probe model.HealthProbe
|
||||
if err := h.db.First(&probe, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "探针不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var input model.HealthProbe
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if input.Name != "" {
|
||||
updates["name"] = input.Name
|
||||
}
|
||||
if input.Hostname != "" {
|
||||
updates["hostname"] = input.Hostname
|
||||
}
|
||||
if input.ProbeType != "" {
|
||||
updates["probe_type"] = input.ProbeType
|
||||
}
|
||||
if input.HTTPURL != "" {
|
||||
updates["http_url"] = input.HTTPURL
|
||||
}
|
||||
if input.TCPHost != "" {
|
||||
updates["tcp_host"] = input.TCPHost
|
||||
}
|
||||
if input.TCPPort != 0 {
|
||||
updates["tcp_port"] = input.TCPPort
|
||||
}
|
||||
if input.ExecCommand != "" {
|
||||
updates["exec_command"] = input.ExecCommand
|
||||
}
|
||||
if input.Timeout > 0 {
|
||||
updates["timeout"] = input.Timeout
|
||||
}
|
||||
if input.Interval > 0 {
|
||||
updates["interval"] = input.Interval
|
||||
}
|
||||
// enabled 布尔处理:显式传入
|
||||
updates["enabled"] = input.Enabled
|
||||
if input.HTTPMethod != "" {
|
||||
updates["http_method"] = input.HTTPMethod
|
||||
}
|
||||
if input.HTTPHeaders != "" {
|
||||
updates["http_headers"] = input.HTTPHeaders
|
||||
}
|
||||
if input.HTTPExpectCode > 0 {
|
||||
updates["http_expect_code"] = input.HTTPExpectCode
|
||||
}
|
||||
if input.HTTPExpectBody != "" {
|
||||
updates["http_expect_body"] = input.HTTPExpectBody
|
||||
}
|
||||
if input.Description != "" {
|
||||
updates["description"] = input.Description
|
||||
}
|
||||
|
||||
if err := h.db.Model(&probe).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.First(&probe, id)
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: probe})
|
||||
}
|
||||
|
||||
// DeleteHealthProbe DELETE /api/v1/agent/health-probes/:id
|
||||
func (h *AgentHandler) DeleteHealthProbe(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.db.Delete(&model.HealthProbe{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// GetHealthProbesForAgent Agent 拉取本机探针配置(Agent 调用,无需 JWT)
|
||||
// GET /api/v1/agent/health-probes/config?hostname=xxx
|
||||
func (h *AgentHandler) GetHealthProbesForAgent(c *gin.Context) {
|
||||
hostname := c.Query("hostname")
|
||||
if hostname == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "hostname 不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
var probes []model.HealthProbe
|
||||
h.db.Where("hostname = ? AND enabled = ?", hostname, true).
|
||||
Order("id ASC").
|
||||
Find(&probes)
|
||||
|
||||
if probes == nil {
|
||||
probes = []model.HealthProbe{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: probes,
|
||||
})
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────
|
||||
// ReceiveReport 增强:处理探针结果 + 返回配置
|
||||
// ────────────────────────────────────────
|
||||
|
||||
// processProbeResults 处理 Agent 上报的探针结果,更新探针状态
|
||||
func (h *AgentHandler) processProbeResults(hostname string, results []model.HealthProbeResult) {
|
||||
now := time.Now()
|
||||
for _, r := range results {
|
||||
updates := map[string]interface{}{
|
||||
"last_status": string(r.Status),
|
||||
"last_checked_at": now,
|
||||
"last_latency_ms": r.LatencyMs,
|
||||
}
|
||||
|
||||
if r.Status == model.ProbeStatusDown {
|
||||
// 失败时增加连续失败计数
|
||||
h.db.Model(&model.HealthProbe{}).
|
||||
Where("id = ? AND hostname = ?", r.ProbeID, hostname).
|
||||
UpdateColumn("fail_count", gorm.Expr("fail_count + 1"))
|
||||
} else {
|
||||
// 成功时清零
|
||||
updates["fail_count"] = 0
|
||||
}
|
||||
|
||||
h.db.Model(&model.HealthProbe{}).
|
||||
Where("id = ? AND hostname = ?", r.ProbeID, hostname).
|
||||
Updates(updates)
|
||||
|
||||
// ═══ 探针告警:Agent 本地探测 down → 平台创建告警事件 ═══
|
||||
if r.Status == model.ProbeStatusDown {
|
||||
// 检查是否存在未恢复的同探针告警
|
||||
var existingAlert model.AgentAlert
|
||||
h.db.Where("rule_name = ? AND hostname = ? AND metric = ? AND resolved = ?",
|
||||
r.Name, hostname, "health_probe", false).
|
||||
First(&existingAlert)
|
||||
|
||||
if existingAlert.ID == 0 {
|
||||
// 获取 fail_count 判断严重级别
|
||||
var probe model.HealthProbe
|
||||
if err := h.db.First(&probe, r.ProbeID).Error; err == nil {
|
||||
severity := "warning"
|
||||
if probe.FailCount >= 3 {
|
||||
severity = "critical"
|
||||
}
|
||||
alert := model.AgentAlert{
|
||||
RuleName: r.Name,
|
||||
Hostname: hostname,
|
||||
Metric: "health_probe",
|
||||
Severity: severity,
|
||||
Message: "健康探针 [" + r.Name + "] 检测异常: " + r.Message + " (类型=" + r.ProbeType + ", 延迟=" + strconv.Itoa(r.LatencyMs) + "ms)",
|
||||
Threshold: 0,
|
||||
Resolved: false,
|
||||
}
|
||||
h.db.Create(&alert)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 探针恢复正常:自动恢复告警
|
||||
h.db.Model(&model.AgentAlert{}).
|
||||
Where("rule_name = ? AND hostname = ? AND metric = ? AND resolved = ?",
|
||||
r.Name, hostname, "health_probe", false).
|
||||
Updates(map[string]interface{}{
|
||||
"resolved": true,
|
||||
"resolved_at": now,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// probeConfigResponse ReceiveReport 的响应结构(含探针配置)
|
||||
type probeConfigResponse struct {
|
||||
model.Response
|
||||
ProbeConfigs []model.HealthProbe `json:"probe_configs,omitempty"`
|
||||
}
|
||||
|
||||
// makeProbeConfigResponse 构建带探针配置的响应
|
||||
func (h *AgentHandler) makeProbeConfigResponse(hostname string, respCode int, respMsg string, probeResults []model.HealthProbeResult) probeConfigResponse {
|
||||
// 处理上报的探针结果
|
||||
if len(probeResults) > 0 {
|
||||
h.processProbeResults(hostname, probeResults)
|
||||
}
|
||||
|
||||
// 拉取该主机的新探针配置(供 Agent 下次执行)
|
||||
var probes []model.HealthProbe
|
||||
h.db.Where("hostname = ? AND enabled = ?", hostname, true).
|
||||
Order("id ASC").
|
||||
Find(&probes)
|
||||
if probes == nil {
|
||||
probes = []model.HealthProbe{}
|
||||
}
|
||||
|
||||
return probeConfigResponse{
|
||||
Response: model.Response{
|
||||
Code: respCode,
|
||||
Message: respMsg,
|
||||
},
|
||||
ProbeConfigs: probes,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user