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:
attacker committed 2026-06-25 20:16:30 +08:00
1 parent e57718998a
commit 77c234b060
314 files changed
+10050 -4065

No files matched your search

+352
View File
@@ -0,0 +1,352 @@
package controller
import (
"crypto/tls"
"encoding/base64"
"encoding/json"
"net/http"
"time"
"ezops/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// PromDSHandler Prometheus 数据源管理 handler // Logan.Li
type PromDSHandler struct {
db *gorm.DB
}
// NewPromDSHandler 创建 Prometheus 数据源 handler
func NewPromDSHandler(db *gorm.DB) *PromDSHandler {
return &PromDSHandler{db: db}
}
// ============================================
// CRUD
// ============================================
// List 数据源列表
// GET /api/v1/prometheus-datasources
func (h *PromDSHandler) List(c *gin.Context) {
var sources []model.PrometheusDataSource
if err := h.db.Order("is_default DESC, id ASC").Find(&sources).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败: " + err.Error()})
return
}
if sources == nil {
sources = []model.PrometheusDataSource{}
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: sources})
}
// Get 获取单个数据源
// GET /api/v1/prometheus-datasources/:id
func (h *PromDSHandler) Get(c *gin.Context) {
id := c.Param("id")
var ds model.PrometheusDataSource
if err := h.db.First(&ds, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "数据源不存在"})
return
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: ds})
}
// Create 创建数据源
// POST /api/v1/prometheus-datasources
func (h *PromDSHandler) Create(c *gin.Context) {
var req model.CreatePromDSReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
return
}
// 如果设为默认,先清除其他默认
if req.IsDefault {
h.db.Model(&model.PrometheusDataSource{}).Where("is_default = ?", true).Update("is_default", false)
}
// 默认值
if req.Access == "" {
req.Access = "server"
}
if req.HTTPMethod == "" {
req.HTTPMethod = "POST"
}
if req.ScrapeInterval <= 0 {
req.ScrapeInterval = 15
}
if req.QueryTimeout <= 0 {
req.QueryTimeout = 60
}
enableAlerting := true
if req.EnableAlerting != nil {
enableAlerting = *req.EnableAlerting
}
ds := model.PrometheusDataSource{
Name: req.Name,
URL: req.URL,
Access: req.Access,
BasicAuth: req.BasicAuth,
BasicAuthUser: req.BasicAuthUser,
BasicAuthPass: req.BasicAuthPass,
WithCredentials: req.WithCredentials,
TLSClientAuth: req.TLSClientAuth,
WithCACert: req.WithCACert,
CACert: req.CACert,
SkipTLSVerify: req.SkipTLSVerify,
CustomHeaders: req.CustomHeaders,
EnableAlerting: enableAlerting,
AlertmanagerDS: req.AlertmanagerDS,
ScrapeInterval: req.ScrapeInterval,
QueryTimeout: req.QueryTimeout,
HTTPMethod: req.HTTPMethod,
DisableMetricsLookup: req.DisableMetricsLookup,
CustomQueryParams: req.CustomQueryParams,
IsDefault: req.IsDefault,
Status: 1,
}
if err := h.db.Create(&ds).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
return
}
h.recordAudit(c, "create", "prometheus_datasource", ds.Name)
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: ds})
}
// Update 更新数据源
// PUT /api/v1/prometheus-datasources/:id
func (h *PromDSHandler) Update(c *gin.Context) {
id := c.Param("id")
var ds model.PrometheusDataSource
if err := h.db.First(&ds, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "数据源不存在"})
return
}
var req model.UpdatePromDSReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
return
}
updates := map[string]interface{}{}
if req.Name != nil {
updates["name"] = *req.Name
}
if req.URL != nil {
updates["url"] = *req.URL
}
if req.Access != nil {
updates["access"] = *req.Access
}
if req.BasicAuth != nil {
updates["basic_auth"] = *req.BasicAuth
}
if req.BasicAuthUser != nil {
updates["basic_auth_user"] = *req.BasicAuthUser
}
if req.BasicAuthPass != nil {
updates["basic_auth_pass"] = *req.BasicAuthPass
}
if req.WithCredentials != nil {
updates["with_credentials"] = *req.WithCredentials
}
if req.TLSClientAuth != nil {
updates["tls_client_auth"] = *req.TLSClientAuth
}
if req.WithCACert != nil {
updates["with_ca_cert"] = *req.WithCACert
}
if req.CACert != nil {
updates["ca_cert"] = *req.CACert
}
if req.SkipTLSVerify != nil {
updates["skip_tls_verify"] = *req.SkipTLSVerify
}
if req.CustomHeaders != nil {
updates["custom_headers"] = *req.CustomHeaders
}
if req.EnableAlerting != nil {
updates["enable_alerting"] = *req.EnableAlerting
}
if req.AlertmanagerDS != nil {
updates["alertmanager_ds"] = *req.AlertmanagerDS
}
if req.ScrapeInterval != nil {
updates["scrape_interval"] = *req.ScrapeInterval
}
if req.QueryTimeout != nil {
updates["query_timeout"] = *req.QueryTimeout
}
if req.HTTPMethod != nil {
updates["http_method"] = *req.HTTPMethod
}
if req.DisableMetricsLookup != nil {
updates["disable_metrics_lookup"] = *req.DisableMetricsLookup
}
if req.CustomQueryParams != nil {
updates["custom_query_params"] = *req.CustomQueryParams
}
if req.IsDefault != nil {
// 如果设为默认,先清除其他默认
if *req.IsDefault {
h.db.Model(&model.PrometheusDataSource{}).Where("is_default = ?", true).Update("is_default", false)
}
updates["is_default"] = *req.IsDefault
}
if req.Status != nil {
updates["status"] = *req.Status
}
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无更新字段"})
return
}
if err := h.db.Model(&ds).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败: " + err.Error()})
return
}
// 重新查询返回
h.db.First(&ds, id)
h.recordAudit(c, "update", "prometheus_datasource", ds.Name)
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: ds})
}
// Delete 删除数据源
// DELETE /api/v1/prometheus-datasources/:id
func (h *PromDSHandler) Delete(c *gin.Context) {
id := c.Param("id")
var ds model.PrometheusDataSource
if err := h.db.First(&ds, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "数据源不存在"})
return
}
if err := h.db.Delete(&ds).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败: " + err.Error()})
return
}
h.recordAudit(c, "delete", "prometheus_datasource", ds.Name)
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
}
// SetDefault 设为默认数据源
// POST /api/v1/prometheus-datasources/:id/default
func (h *PromDSHandler) SetDefault(c *gin.Context) {
id := c.Param("id")
var ds model.PrometheusDataSource
if err := h.db.First(&ds, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "数据源不存在"})
return
}
// 清除所有默认
h.db.Model(&model.PrometheusDataSource{}).Where("is_default = ?", true).Update("is_default", false)
// 设置当前为默认
h.db.Model(&ds).Update("is_default", true)
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已设为默认"})
}
// ============================================
// Test Connection
// ============================================
// TestConnection 测试数据源连接
// POST /api/v1/prometheus-datasources/test
func (h *PromDSHandler) TestConnection(c *gin.Context) {
var req model.TestPromDSReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
return
}
client := &http.Client{Timeout: 15 * time.Second}
// TLS 配置
if req.SkipTLSVerify {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
// 构造请求
apiURL := req.URL + "/api/v1/query?query=up&time=" + time.Now().UTC().Format(time.RFC3339)
httpReq, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
c.JSON(http.StatusOK, model.Response{Code: 400, Data: gin.H{
"success": false, "message": "URL 格式错误: " + err.Error(),
}})
return
}
// Basic Auth
if req.BasicAuth {
auth := base64.StdEncoding.EncodeToString([]byte(req.BasicAuthUser + ":" + req.BasicAuthPass))
httpReq.Header.Set("Authorization", "Basic "+auth)
}
resp, err := client.Do(httpReq)
if err != nil {
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
"success": false, "message": "连接失败: " + err.Error(),
}})
return
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
// 尝试解析响应获取 target 数量
var result struct {
Data struct {
Result []interface{} `json:"result"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&result)
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
"success": true,
"message": "连接成功",
"status_code": resp.StatusCode,
"target_count": len(result.Data.Result),
}})
} else {
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
"success": false,
"message": "连接返回异常状态码",
"status_code": resp.StatusCode,
}})
}
}
// ============================================
// 审计
// ============================================
func (h *PromDSHandler) recordAudit(c *gin.Context, action, resource, name string) {
userID, _ := c.Get("userID")
username, _ := c.Get("username")
var uid uint
var uname string
if v, ok := userID.(uint); ok {
uid = v
}
if v, ok := username.(string); ok {
uname = v
}
h.db.Create(&model.AuditLog{
UserID: &uid,
Username: uname,
IP: c.ClientIP(),
UserAgent: c.Request.UserAgent(),
Module: "prometheus_datasource",
Action: action,
Resource: resource,
ResourceID: name,
Status: 200,
})
}