Compare commits
6 Commits
88e5c24e54
...
6d5f680386
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d5f680386 | |||
| b07661383a | |||
| d381360dfb | |||
| 601bfc58aa | |||
| 7efe430b9c | |||
| 93d654586a |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -41,7 +41,8 @@ logs/
|
||||
*_local.yaml
|
||||
.env
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
.worktrees/
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -11,6 +12,8 @@ import (
|
||||
"git.apinb.com/ops/logs/internal/config"
|
||||
)
|
||||
|
||||
const alertResponseBodyLimit = 1 << 20
|
||||
|
||||
// AlertReceiveBody 与 alert ReceiveRequest 对齐(含必填 raw_data)
|
||||
type AlertReceiveBody struct {
|
||||
AlertName string `json:"alert_name"`
|
||||
@@ -53,28 +56,56 @@ func forwardAlert(body AlertReceiveBody) error {
|
||||
if body.PolicyID == 0 && cfg.DefaultPolicyID > 0 {
|
||||
body.PolicyID = cfg.DefaultPolicyID
|
||||
}
|
||||
rawEvent := buildRawEventIngestBody(body, "parsed")
|
||||
raw, err := json.Marshal(rawEvent)
|
||||
raw, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := cfg.BaseURL + "/Alert/v1/raw-events/ingest"
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(raw))
|
||||
return postAlertPayload(cfg, "/Alert/v1/alerts/receive", raw)
|
||||
}
|
||||
|
||||
func postAlertPayload(cfg *config.AlertForwardConf, path string, payload []byte) error {
|
||||
req, err := http.NewRequest(http.MethodPost, cfg.BaseURL+path, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建 Alert 转发请求失败:%w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if cfg.InternalKey != "" {
|
||||
req.Header.Set("X-Internal-Key", cfg.InternalKey)
|
||||
}
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("发送 Alert 转发请求失败:%w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(io.LimitReader(resp.Body, alertResponseBodyLimit+1))
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 Alert 响应失败:%w", err)
|
||||
}
|
||||
if len(responseBody) > alertResponseBodyLimit {
|
||||
return fmt.Errorf("Alert 响应体超过 %d 字节限制,请稍后重试", alertResponseBodyLimit)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("alert returned HTTP %d", resp.StatusCode)
|
||||
return fmt.Errorf("Alert 返回 HTTP %d,请稍后重试", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Code *int32 `json:"code"`
|
||||
}
|
||||
if err := json.Unmarshal(responseBody, &result); err != nil {
|
||||
return fmt.Errorf("Alert 响应不是有效 JSON:%v;请稍后重试", err)
|
||||
}
|
||||
if result.Code == nil {
|
||||
return fmt.Errorf("Alert 响应缺少 code,无法确认转发成功;请稍后重试")
|
||||
}
|
||||
if *result.Code != 0 {
|
||||
return fmt.Errorf("Alert 拒绝转发,业务 code=%d;请稍后重试", *result.Code)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package ingest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -118,25 +116,7 @@ func forwardRawEvent(body RawEventIngestBody) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := cfg.BaseURL + "/Alert/v1/raw-events/ingest"
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if cfg.InternalKey != "" {
|
||||
req.Header.Set("X-Internal-Key", cfg.InternalKey)
|
||||
}
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("alert returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
return postAlertPayload(cfg, "/Alert/v1/raw-events/ingest", raw)
|
||||
}
|
||||
|
||||
func markOutboxRetry(row models.AlertOutbox, msg string) {
|
||||
|
||||
@@ -498,21 +498,7 @@ func (e *Engine) HandleTrap(addr *net.UDPAddr, pkt *gosnmp.SnmpPacket) {
|
||||
rules := e.trapRules
|
||||
e.mu.RUnlock()
|
||||
|
||||
var matched *models.TrapRule
|
||||
for i := range rules {
|
||||
if trapRuleMatches(&rules[i], trapOID, fp) {
|
||||
matched = &rules[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matched == nil && dict != nil && strings.TrimSpace(dict.SeverityCode) != "" {
|
||||
matched = &models.TrapRule{
|
||||
AlertName: firstNonEmpty(firstNonEmpty(dict.Name, dict.Title), "SNMP Trap"),
|
||||
SeverityCode: dict.SeverityCode,
|
||||
PolicyID: 0,
|
||||
}
|
||||
}
|
||||
matched := firstMatchingTrapRule(rules, trapOID, fp)
|
||||
if matched == nil {
|
||||
rawBytes, mErr := json.Marshal(fp)
|
||||
if mErr != nil {
|
||||
@@ -665,7 +651,7 @@ func trapRuleMatches(rule *models.TrapRule, trapOID, varbindFP string) bool {
|
||||
if hasOID && !strings.HasPrefix(normOID(trapOID), normOID(rule.OIDPrefix)) {
|
||||
return false
|
||||
}
|
||||
if rule.VarbindMatchRegex != "" {
|
||||
if hasRE {
|
||||
re, err := regexp.Compile(rule.VarbindMatchRegex)
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -677,6 +663,15 @@ func trapRuleMatches(rule *models.TrapRule, trapOID, varbindFP string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func firstMatchingTrapRule(rules []models.TrapRule, trapOID, varbindFP string) *models.TrapRule {
|
||||
for i := range rules {
|
||||
if trapRuleMatches(&rules[i], trapOID, varbindFP) {
|
||||
return &rules[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func firstNonEmpty(a, b string) string {
|
||||
if strings.TrimSpace(a) != "" {
|
||||
return a
|
||||
|
||||
@@ -35,6 +35,10 @@ func CreateSyslogRule(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := validateSyslogRule(&row); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
row.ID = 0
|
||||
if err := impl.DBService.Create(&row).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
@@ -60,6 +64,10 @@ func UpdateSyslogRule(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
row.ID = id
|
||||
if err := validateSyslogRule(&row); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Save(&row).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -98,6 +106,10 @@ func CreateTrapRule(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := validateTrapRule(&row); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
row.ID = 0
|
||||
if err := impl.DBService.Create(&row).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
@@ -123,6 +135,10 @@ func UpdateTrapRule(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
row.ID = id
|
||||
if err := validateTrapRule(&row); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Save(&row).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
|
||||
75
internal/logic/controllers/rule_validation.go
Normal file
75
internal/logic/controllers/rule_validation.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.apinb.com/ops/logs/internal/models"
|
||||
)
|
||||
|
||||
func validateSyslogRule(rule *models.SyslogRule) error {
|
||||
regexFields := []struct {
|
||||
name string
|
||||
pattern string
|
||||
}{
|
||||
{name: "keyword_regex", pattern: rule.KeywordRegex},
|
||||
{name: "message_regex", pattern: rule.MessageRegex},
|
||||
{name: "resource_uid_extract_regex", pattern: rule.ResourceUIDExtractRegex},
|
||||
}
|
||||
for _, field := range regexFields {
|
||||
if err := validateOptionalRegex(field.name, field.pattern); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if raw := strings.TrimSpace(rule.SeverityMappingJSON); raw != "" {
|
||||
var mapping map[string]any
|
||||
if err := json.Unmarshal([]byte(raw), &mapping); err != nil {
|
||||
return fmt.Errorf("severity_mapping_json 必须是字符串到字符串的 JSON 对象:%v;请修正 severity_mapping_json 后重试", err)
|
||||
}
|
||||
if mapping == nil {
|
||||
return fmt.Errorf("severity_mapping_json 必须是字符串到字符串的 JSON 对象,不能是 null;请改为 JSON 对象后重试")
|
||||
}
|
||||
for pattern, severity := range mapping {
|
||||
if _, ok := severity.(string); !ok {
|
||||
return fmt.Errorf("severity_mapping_json 的映射键 %q 对应值必须是字符串;请修正该映射值后重试", pattern)
|
||||
}
|
||||
if _, err := regexp.Compile(pattern); err != nil {
|
||||
return fmt.Errorf("severity_mapping_json 的映射键 %q 不是有效正则表达式:%v;请修正该映射键后重试", pattern, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(rule.DeviceNameContains) == "" &&
|
||||
strings.TrimSpace(rule.SourceMatch) == "" &&
|
||||
strings.TrimSpace(rule.KeywordRegex) == "" &&
|
||||
strings.TrimSpace(rule.MessageRegex) == "" {
|
||||
return fmt.Errorf("Syslog 规则的匹配条件全部为空,运行时永远不会命中;请至少填写 device_name_contains、source_match、keyword_regex、message_regex 中的一项")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateTrapRule(rule *models.TrapRule) error {
|
||||
if strings.TrimSpace(rule.VarbindMatchRegex) != "" {
|
||||
if _, err := regexp.Compile(rule.VarbindMatchRegex); err != nil {
|
||||
return fmt.Errorf("varbind_match_regex 不是有效正则表达式:%v;请修正 varbind_match_regex 后重试", err)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(rule.OIDPrefix) == "" && strings.TrimSpace(rule.VarbindMatchRegex) == "" {
|
||||
return fmt.Errorf("Trap 规则的匹配条件全部为空,运行时永远不会命中;请至少填写 oid_prefix、varbind_match_regex 中的一项")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOptionalRegex(field, pattern string) error {
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if pattern == "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := regexp.Compile(pattern); err != nil {
|
||||
return fmt.Errorf("%s 不是有效正则表达式:%v;请修正 %s 后重试", field, err, field)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user