feat(monitor): F6 日志告警引擎 — AlertEngine 支持 type=log 规则查询Loki/ES+触发/恢复 + 数据源认证升级(none/basic/bearer/custom) + 从查询创建告警真实API对接
This commit is contained in:
1 parent
894cb16a05
commit
6bba6144f3
17 files changed
+657
-74
No files matched your search
@@ -79,7 +79,7 @@ func (h *LogProxyHandler) QueryLoki(c *gin.Context) {
|
||||
client := newLogHTTPClient(ds.SkipTLSVerify, time.Duration(60)*time.Second)
|
||||
target := ds.URL + "/loki/api/v1/query_range?" + params.Encode()
|
||||
httpReq, _ := http.NewRequest(http.MethodGet, target, nil)
|
||||
applyLogDSAuth(httpReq, ds.BasicAuth, ds.BasicAuthUser, ds.BasicAuthPass)
|
||||
applyLogDSAuthV2(httpReq, ds)
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
c.JSON(502, model.Response{Code: 502, Message: "查询失败: " + err.Error()})
|
||||
@@ -88,7 +88,7 @@ func (h *LogProxyHandler) QueryLoki(c *gin.Context) {
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 400 {
|
||||
c.JSON(502, model.Response{Code: 502, Message: "Loki 错误: " + string(body)})
|
||||
c.JSON(logProxyErrStatus(resp.StatusCode), model.Response{Code: logProxyErrStatus(resp.StatusCode), Message: "Loki 错误: " + string(body), Data: map[string]string{"category": logProxyErrCategory(resp.StatusCode)}})
|
||||
return
|
||||
}
|
||||
var raw map[string]interface{}
|
||||
@@ -137,6 +137,8 @@ func (h *LogProxyHandler) QueryES(c *gin.Context) {
|
||||
if searchBody == nil {
|
||||
searchBody = map[string]interface{}{
|
||||
"size": req.Size,
|
||||
// ES 7+ 默认 track_total_hits 为 10000,必须显式开启才能返回真实命中总数 // Logan.Li
|
||||
"track_total_hits": true,
|
||||
"sort": []interface{}{
|
||||
map[string]interface{}{"_score": map[string]string{"order": "desc"}},
|
||||
},
|
||||
@@ -210,7 +212,7 @@ func (h *LogProxyHandler) QueryES(c *gin.Context) {
|
||||
target := fmt.Sprintf("%s/%s/_search", ds.URL, strings.Trim(index, "/"))
|
||||
httpReq, _ := http.NewRequest(http.MethodPost, target, bytes.NewReader(payload))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
applyLogDSAuth(httpReq, ds.BasicAuth, ds.BasicAuthUser, ds.BasicAuthPass)
|
||||
applyLogDSAuthV2(httpReq, ds)
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
c.JSON(502, model.Response{Code: 502, Message: "查询失败: " + err.Error()})
|
||||
@@ -219,17 +221,43 @@ func (h *LogProxyHandler) QueryES(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)})
|
||||
c.JSON(logProxyErrStatus(resp.StatusCode), model.Response{Code: logProxyErrStatus(resp.StatusCode), Message: "ES 错误: " + string(body), Data: map[string]string{"category": logProxyErrCategory(resp.StatusCode)}})
|
||||
return
|
||||
}
|
||||
var raw map[string]interface{}
|
||||
_ = json.Unmarshal(body, &raw)
|
||||
entries := flattenESEntries(raw)
|
||||
total := esHitsTotal(raw)
|
||||
c.JSON(200, model.Response{Code: 200, Data: gin.H{
|
||||
"entries": entries, "total": len(entries),
|
||||
"entries": entries, "total": total,
|
||||
}})
|
||||
}
|
||||
|
||||
// logProxyErrStatus 把下游 HTTP 状态码映射为合适的 API 状态码 // Logan.Li
|
||||
func logProxyErrStatus(status int) int {
|
||||
if status >= 400 && status < 500 {
|
||||
return 400
|
||||
}
|
||||
return 502
|
||||
}
|
||||
|
||||
// logProxyErrCategory 对下游错误进行分类,便于前端展示中文提示 // Logan.Li
|
||||
func logProxyErrCategory(status int) string {
|
||||
switch status {
|
||||
case 401:
|
||||
return "auth_failed"
|
||||
case 403:
|
||||
return "forbidden"
|
||||
case 400, 404:
|
||||
return "bad_request"
|
||||
default:
|
||||
if status >= 500 {
|
||||
return "server_error"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func parseLogTime(s string) (time.Time, error) {
|
||||
if n, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||
if n > 1e15 {
|
||||
@@ -314,6 +342,20 @@ func flattenESEntries(raw map[string]interface{}) []logEntryRow {
|
||||
return out
|
||||
}
|
||||
|
||||
// esHitsTotal 从 ES 响应中提取真实命中总数(兼容 ES 7+ total.value 与旧版 total)// Logan.Li
|
||||
func esHitsTotal(raw map[string]interface{}) int64 {
|
||||
hitsWrap, _ := raw["hits"].(map[string]interface{})
|
||||
if total, ok := hitsWrap["total"].(map[string]interface{}); ok {
|
||||
if v, ok := total["value"].(float64); ok {
|
||||
return int64(v)
|
||||
}
|
||||
}
|
||||
if v, ok := hitsWrap["total"].(float64); ok {
|
||||
return int64(v)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func pickESMessage(src map[string]interface{}) string {
|
||||
for _, key := range []string{"message", "msg", "log", "content"} {
|
||||
if v, ok := src[key]; ok {
|
||||
|
||||
Reference in new issue
Block a user