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,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,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user