feat(monitor): F6 日志告警引擎 — AlertEngine 支持 type=log 规则查询Loki/ES+触发/恢复 + 数据源认证升级(none/basic/bearer/custom) + 从查询创建告警真实API对接

This commit is contained in:
attacker committed 2026-06-28 04:00:38 +08:00
1 parent 894cb16a05
commit 6bba6144f3
17 files changed
+657 -74

No files matched your search

+95 -10
View File
@@ -4,6 +4,7 @@ import (
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"io"
"net/http"
"strings"
@@ -68,12 +69,18 @@ func (h *LogDSHandler) Create(c *gin.Context) {
}
ds := model.LogDataSource{
Name: req.Name, Type: req.Type, URL: strings.TrimRight(req.URL, "/"),
AuthType: normalizeLogDSAuthType(req.AuthType),
BasicAuth: req.BasicAuth, BasicAuthUser: req.BasicAuthUser, BasicAuthPass: req.BasicAuthPass,
Token: req.Token, Headers: req.Headers,
SkipTLSVerify: req.SkipTLSVerify, DefaultIndex: req.DefaultIndex,
IsDefault: req.IsDefault, Status: 1,
}
if err := h.db.Create(&ds).Error; err != nil {
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
if errors.Is(err, gorm.ErrDuplicatedKey) {
c.JSON(409, model.Response{Code: 409, Message: "数据源名称已存在"})
return
}
c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
return
}
c.JSON(200, model.Response{Code: 200, Message: "创建成功", Data: ds})
@@ -101,6 +108,9 @@ func (h *LogDSHandler) Update(c *gin.Context) {
if req.URL != nil {
updates["url"] = strings.TrimRight(*req.URL, "/")
}
if req.AuthType != nil {
updates["auth_type"] = normalizeLogDSAuthType(*req.AuthType)
}
if req.BasicAuth != nil {
updates["basic_auth"] = *req.BasicAuth
}
@@ -110,6 +120,12 @@ func (h *LogDSHandler) Update(c *gin.Context) {
if req.BasicAuthPass != nil && *req.BasicAuthPass != "" {
updates["basic_auth_pass"] = *req.BasicAuthPass
}
if req.Token != nil {
updates["token"] = *req.Token
}
if req.Headers != nil {
updates["headers"] = *req.Headers
}
if req.SkipTLSVerify != nil {
updates["skip_tls_verify"] = *req.SkipTLSVerify
}
@@ -126,7 +142,11 @@ func (h *LogDSHandler) Update(c *gin.Context) {
updates["is_default"] = false
}
if err := h.db.Model(&ds).Updates(updates).Error; err != nil {
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
if errors.Is(err, gorm.ErrDuplicatedKey) {
c.JSON(409, model.Response{Code: 409, Message: "数据源名称已存在"})
return
}
c.JSON(500, model.Response{Code: 500, Message: "更新失败: " + err.Error()})
return
}
h.db.First(&ds, ds.ID)
@@ -172,9 +192,12 @@ func (h *LogDSHandler) TestConnection(c *gin.Context) {
}
req.Type = ds.Type
req.URL = ds.URL
req.AuthType = ds.AuthType
req.BasicAuth = ds.BasicAuth
req.BasicAuthUser = ds.BasicAuthUser
req.BasicAuthPass = ds.BasicAuthPass
req.Token = ds.Token
req.Headers = ds.Headers
req.SkipTLSVerify = ds.SkipTLSVerify
} else if req.Type == "" || req.URL == "" {
c.JSON(400, model.Response{Code: 400, Message: "datasource_id 与 (type,url) 至少提供一组"})
@@ -197,19 +220,24 @@ func (h *LogDSHandler) ListIndices(c *gin.Context) {
c.JSON(400, model.Response{Code: 400, Message: "url 参数必填"})
return
}
basicAuth := c.Query("basic_auth") == "true"
user := c.Query("basic_auth_user")
pass := c.Query("basic_auth_pass")
skipTLS := c.Query("skip_tls_verify") == "true"
tempDS := model.LogDataSource{
AuthType: c.Query("auth_type"),
BasicAuth: c.Query("basic_auth") == "true",
BasicAuthUser: c.Query("basic_auth_user"),
BasicAuthPass: c.Query("basic_auth_pass"),
Token: c.Query("token"),
Headers: c.Query("headers"),
SkipTLSVerify: c.Query("skip_tls_verify") == "true",
}
client := newLogHTTPClient(skipTLS, 10*time.Second)
client := newLogHTTPClient(tempDS.SkipTLSVerify, 10*time.Second)
target := strings.TrimRight(esURL, "/") + "/_cat/indices?format=json&h=index"
httpReq, err := http.NewRequest(http.MethodGet, target, nil)
if err != nil {
c.JSON(502, model.Response{Code: 502, Message: err.Error()})
return
}
applyLogDSAuth(httpReq, basicAuth, user, pass)
applyLogDSAuthV2(httpReq, &tempDS)
resp, err := client.Do(httpReq)
if err != nil {
c.JSON(502, model.Response{Code: 502, Message: "查询索引失败: " + err.Error()})
@@ -218,7 +246,8 @@ func (h *LogDSHandler) ListIndices(c *gin.Context) {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
c.JSON(502, model.Response{Code: 502, Message: "ES 错误: " + string(body)})
status := logProxyErrStatus(resp.StatusCode)
c.JSON(status, model.Response{Code: status, Message: "ES 错误: " + string(body), Data: map[string]string{"category": logProxyErrCategory(resp.StatusCode)}})
return
}
var raw []map[string]interface{}
@@ -251,7 +280,16 @@ func testLogDSConnection(req model.TestLogDSReq) (bool, string) {
if err != nil {
return false, err.Error()
}
applyLogDSAuth(httpReq, req.BasicAuth, req.BasicAuthUser, req.BasicAuthPass)
// 用临时 LogDataSource 统一走 V2 认证逻辑 // Logan.Li
tempDS := model.LogDataSource{
AuthType: req.AuthType,
BasicAuth: req.BasicAuth,
BasicAuthUser: req.BasicAuthUser,
BasicAuthPass: req.BasicAuthPass,
Token: req.Token,
Headers: req.Headers,
}
applyLogDSAuthV2(httpReq, &tempDS)
resp, err := client.Do(httpReq)
if err != nil {
return false, "连接失败: " + err.Error()
@@ -284,4 +322,51 @@ func applyLogDSAuth(req *http.Request, basic bool, user, pass string) {
if basic && user != "" {
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(user+":"+pass)))
}
}
// applyLogDSAuthV2 根据完整数据源配置应用认证(支持 basic/bearer/custom headers)// Logan.Li
func applyLogDSAuthV2(req *http.Request, ds *model.LogDataSource) {
authType := strings.ToLower(ds.AuthType)
// 兼容旧版:显式开启 BasicAuth 但没有 AuthType 时按 basic 处理 // Logan.Li
if authType == "" && ds.BasicAuth {
authType = "basic"
}
switch authType {
case "basic":
if ds.BasicAuthUser != "" {
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(ds.BasicAuthUser+":"+ds.BasicAuthPass)))
}
case "bearer":
if ds.Token != "" {
req.Header.Set("Authorization", "Bearer "+ds.Token)
}
case "custom":
if ds.Headers != "" {
var headers []struct {
Name string `json:"name"`
Value string `json:"value"`
}
if err := json.Unmarshal([]byte(ds.Headers), &headers); err == nil {
for _, h := range headers {
if h.Name != "" {
req.Header.Set(h.Name, h.Value)
}
}
}
}
}
}
// normalizeLogDSAuthType 规范认证方式取值 // Logan.Li
func normalizeLogDSAuthType(t string) string {
switch strings.ToLower(t) {
case "basic":
return "basic"
case "bearer":
return "bearer"
case "custom":
return "custom"
default:
return "none"
}
}