Compare commits

...

2 Commits

Author SHA1 Message Date
zxr
8495fdf039 fix: correlate log alert recovery lifecycle 2026-07-21 22:15:59 +08:00
zxr
eaca3d0ac8 fix: preserve log alert recovery events 2026-07-21 21:21:47 +08:00
7 changed files with 242 additions and 82 deletions

View File

@@ -38,6 +38,7 @@ type AlertReceiveBody struct {
Labels map[string]string `json:"labels"`
Agent string `json:"agent"`
PolicyID uint `json:"policy_id"`
Fingerprint string `json:"fingerprint,omitempty"`
State string `json:"state,omitempty"`
SourceEventKey string `json:"source_event_key"`
TraceID string `json:"trace_id"`
@@ -104,11 +105,14 @@ func forwardAlert(body AlertReceiveBody) error {
(details.Status != "firing" && details.Status != "resolved") || !validFingerprint(details.Fingerprint) {
return fmt.Errorf("Alert 响应缺少完整写入结果,无法确认转发成功;请稍后重试")
}
if body.Fingerprint != "" && details.Fingerprint != body.Fingerprint {
return fmt.Errorf("Alert 返回的告警指纹与请求不一致;请稍后重试")
}
return nil
}
func postAlertPayload(cfg *config.AlertForwardConf, path string, payload []byte, traceID string) (*alertForwardResponse, error) {
req, err := http.NewRequest(http.MethodPost, cfg.BaseURL+path, bytes.NewReader(payload))
req, err := http.NewRequest(http.MethodPost, strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/")+path, bytes.NewReader(payload))
if err != nil {
return nil, fmt.Errorf("创建 Alert 转发请求失败:%w", err)
}

View File

@@ -156,14 +156,19 @@ func processOneOutbox(db *gorm.DB, row models.AlertOutbox) error {
}
return markOutboxDead(db, row, row.RetryCount, "invalid_payload: "+err.Error(), now)
}
forwardErr := forwardOutboxPayload(row.PayloadJSON, body, fmt.Sprintf("logs-outbox:%d", row.ID))
fallbackKey := fmt.Sprintf("logs-outbox:%d", row.ID)
occurredAt := row.CreatedAt.UTC()
if occurredAt.IsZero() {
occurredAt = time.Now().UTC()
}
forwardErr := forwardOutboxPayload(row.PayloadJSON, body, fallbackKey, occurredAt)
now, err := databaseNow(db)
if err != nil {
return err
}
if forwardErr != nil {
if errors.Is(forwardErr, errAlertForwardDisabled) {
return markOutboxDead(db, row, row.RetryCount, forwardErr.Error(), now)
return markOutboxWaiting(db, row, forwardErr.Error(), now)
}
return markOutboxRetry(db, row, forwardErr.Error(), now)
}
@@ -178,16 +183,35 @@ func databaseNow(db *gorm.DB) (time.Time, error) {
return now.UTC(), nil
}
func forwardOutboxPayload(payloadJSON string, legacyBody AlertReceiveBody, fallbackSourceEventKey string) error {
func forwardOutboxPayload(payloadJSON string, legacyBody AlertReceiveBody, fallbackSourceEventKey string, occurredAt time.Time) error {
var rawEvent RawEventIngestBody
if err := json.Unmarshal([]byte(payloadJSON), &rawEvent); err == nil && rawEvent.SourceType != "" && len(rawEvent.RawPayload) > 0 {
if strings.TrimSpace(rawEvent.SourceEventKey) == "" {
rawEvent.SourceEventKey = fallbackSourceEventKey
}
if rawEvent.EventTime.IsZero() {
rawEvent.EventTime = occurredAt
}
rawEvent.TraceID = ensureAlertTraceID(rawEvent.TraceID, firstNonEmpty(rawEvent.SourceEventKey, fallbackSourceEventKey))
return forwardRawEvent(rawEvent)
}
if strings.TrimSpace(legacyBody.SourceEventKey) == "" {
legacyBody.SourceEventKey = fallbackSourceEventKey
}
if legacyBody.OccurredAt.IsZero() {
legacyBody.OccurredAt = occurredAt
}
legacyBody.TraceID = ensureAlertTraceID(legacyBody.TraceID, firstNonEmpty(legacyBody.SourceEventKey, fallbackSourceEventKey))
return forwardAlert(legacyBody)
}
func markOutboxWaiting(db *gorm.DB, row models.AlertOutbox, msg string, now time.Time) error {
return updateClaimedOutbox(db, row, map[string]interface{}{
"status": outboxStatusRetrying, "next_retry_at": now.Add(30 * time.Second),
"last_error": truncateError(msg, 1024), "lease_until": nil, "lease_owner": "",
}, "retrying")
}
func forwardRawEvent(body RawEventIngestBody) error {
cfg := config.Spec.AlertForward
if cfg == nil || !cfg.Enabled || cfg.BaseURL == "" {
@@ -246,9 +270,16 @@ func markOutboxSent(db *gorm.DB, row models.AlertOutbox, now time.Time) error {
if result.RowsAffected == 0 {
return nil
}
return tx.Model(&models.LogEvent{}).Where("id = ? AND dispatch_outbox_id = ?", row.LogEventID, row.ID).Updates(map[string]interface{}{
eventResult := tx.Model(&models.LogEvent{}).Where("id = ? AND dispatch_outbox_id = ?", row.LogEventID, row.ID).Updates(map[string]interface{}{
"alert_sent": true, "dispatch_status": "sent",
}).Error
})
if eventResult.Error != nil {
return eventResult.Error
}
if eventResult.RowsAffected != 1 {
return fmt.Errorf("log event %d does not belong to outbox %d", row.LogEventID, row.ID)
}
return nil
})
}
@@ -286,7 +317,14 @@ func updateClaimedOutbox(db *gorm.DB, row models.AlertOutbox, updates map[string
if result.RowsAffected == 0 {
return nil
}
return tx.Model(&models.LogEvent{}).Where("id = ? AND dispatch_outbox_id = ?", row.LogEventID, row.ID).Update("dispatch_status", eventStatus).Error
eventResult := tx.Model(&models.LogEvent{}).Where("id = ? AND dispatch_outbox_id = ?", row.LogEventID, row.ID).Update("dispatch_status", eventStatus)
if eventResult.Error != nil {
return eventResult.Error
}
if eventResult.RowsAffected != 1 {
return fmt.Errorf("log event %d does not belong to outbox %d", row.LogEventID, row.ID)
}
return nil
})
}

View File

@@ -1,6 +1,8 @@
package ingest
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"log"
@@ -271,8 +273,9 @@ func (e *Engine) HandleSyslog(addr *net.UDPAddr, payload []byte) {
AlertName: matched.AlertName, Summary: summary, Description: summary,
SeverityCode: firstNonEmpty(matchDetails.SeverityCode, firstNonEmpty(matched.SeverityCode, sev)),
Value: parsed.Message, Labels: labels, Agent: "logs-syslog", PolicyID: matched.PolicyID,
State: "firing", SourceEventKey: logSourceEventKey(stored, "alert"), OccurredAt: occurredAt, RawData: rawBytes,
State: matchDetails.State, SourceEventKey: logSourceEventKey(stored, "alert"), OccurredAt: occurredAt, RawData: rawBytes,
}
body.Fingerprint = alertLifecycleFingerprint(body, matched.LifecycleKey, "")
return enqueueAlertWithDB(tx, stored.ID, body)
}); err != nil {
log.Printf("logs: persist matched syslog event: %v", err)
@@ -281,6 +284,7 @@ func (e *Engine) HandleSyslog(addr *net.UDPAddr, payload []byte) {
type syslogRuleMatch struct {
Matched bool
State string
ResourceUID string
SeverityCode string
Captures map[string]string
@@ -291,11 +295,12 @@ func syslogRuleMatches(rule *models.SyslogRule, device, message, rawLine string)
}
func syslogRuleMatchDetails(rule *models.SyslogRule, device, message, rawLine string) syslogRuleMatch {
result := syslogRuleMatch{Captures: map[string]string{}}
result := syslogRuleMatch{State: "firing", Captures: map[string]string{}}
deviceContains := strings.TrimSpace(rule.DeviceNameContains)
sourceMatch := strings.TrimSpace(rule.SourceMatch)
keywordRegex := strings.TrimSpace(rule.KeywordRegex)
messageRegex := strings.TrimSpace(rule.MessageRegex)
recoveryRegex := strings.TrimSpace(rule.RecoveryMatchRegex)
if deviceContains == "" && sourceMatch == "" && keywordRegex == "" && messageRegex == "" {
return result
}
@@ -312,24 +317,35 @@ func syslogRuleMatchDetails(rule *models.SyslogRule, device, message, rawLine st
return result
}
}
for _, pattern := range []string{keywordRegex, messageRegex} {
if pattern == "" {
continue
}
re, err := regexp.Compile(pattern)
if recoveryRegex != "" {
re, err := regexp.Compile(recoveryRegex)
if err != nil {
return result
}
matches := re.FindStringSubmatch(message)
if matches == nil {
matches = re.FindStringSubmatch(rawLine)
matches := firstRegexMatch(re, message, rawLine)
if matches != nil {
mergeNamedCaptures(result.Captures, re, matches)
result.State = "resolved"
result.Matched = true
}
if matches == nil {
return result
}
mergeNamedCaptures(result.Captures, re, matches)
}
result.Matched = true
if !result.Matched {
for _, pattern := range []string{keywordRegex, messageRegex} {
if pattern == "" {
continue
}
re, err := regexp.Compile(pattern)
if err != nil {
return result
}
matches := firstRegexMatch(re, message, rawLine)
if matches == nil {
return result
}
mergeNamedCaptures(result.Captures, re, matches)
}
result.Matched = true
}
if uid := extractWithNamedRegex(rule.ResourceUIDExtractRegex, "resource_uid", message, rawLine); uid != "" {
result.ResourceUID = normalizeExtractedResourceUID(uid)
} else if uid := result.Captures["resource_uid"]; uid != "" {
@@ -339,6 +355,15 @@ func syslogRuleMatchDetails(rule *models.SyslogRule, device, message, rawLine st
return result
}
func firstRegexMatch(re *regexp.Regexp, values ...string) []string {
for _, value := range values {
if matches := re.FindStringSubmatch(value); matches != nil {
return matches
}
}
return nil
}
func mergeNamedCaptures(dst map[string]string, re *regexp.Regexp, matches []string) {
names := re.SubexpNames()
for i, name := range names {
@@ -366,7 +391,9 @@ func extractWithNamedRegex(pattern, groupName, message, rawLine string) string {
names := re.SubexpNames()
for i, name := range names {
if i > 0 && name == groupName && i < len(matches) {
return strings.TrimSpace(matches[i])
if value := strings.TrimSpace(matches[i]); value != "" {
return value
}
}
}
for i := 1; i < len(matches); i++ {
@@ -506,8 +533,8 @@ func (e *Engine) HandleTrap(addr *net.UDPAddr, pkt *gosnmp.SnmpPacket) {
rules := e.trapRules
e.mu.RUnlock()
matched := firstMatchingTrapRule(rules, trapOID, fp)
if matched == nil {
match := firstMatchingTrapRule(rules, trapOID, fp)
if match.Rule == nil {
rawBytes, mErr := json.Marshal(fp)
if mErr != nil {
return
@@ -537,6 +564,7 @@ func (e *Engine) HandleTrap(addr *net.UDPAddr, pkt *gosnmp.SnmpPacket) {
}
return
}
matched := match.Rule
desc := readable
if dict != nil && dict.RecoveryMessage != "" {
@@ -551,6 +579,10 @@ func (e *Engine) HandleTrap(addr *net.UDPAddr, pkt *gosnmp.SnmpPacket) {
"instance": addr.IP.String(),
"job": "logs-trap",
}
trapInstance := trapInstanceKey(pkt)
if trapInstance != "" {
labels["trap_instance"] = trapInstance
}
if matched.ID != 0 {
labels["resource_type"] = "trap_rule"
labels["resource_id"] = strconv.FormatUint(uint64(matched.ID), 10)
@@ -587,9 +619,10 @@ func (e *Engine) HandleTrap(addr *net.UDPAddr, pkt *gosnmp.SnmpPacket) {
body := AlertReceiveBody{
AlertName: firstNonEmpty(matched.AlertName, "SNMP Trap"), Summary: readable, Description: desc,
SeverityCode: firstNonEmpty(matched.SeverityCode, sev), Value: string(vbJSON), Labels: labels,
Agent: "logs-trap", PolicyID: matched.PolicyID, State: "firing",
Agent: "logs-trap", PolicyID: matched.PolicyID, State: match.State,
SourceEventKey: logSourceEventKey(stored, "alert"), OccurredAt: occurredAt, RawData: rawBytes,
}
body.Fingerprint = alertLifecycleFingerprint(body, matched.LifecycleKey, trapInstance)
return enqueueAlertWithDB(tx, stored.ID, body)
}); err != nil {
log.Printf("logs: persist matched trap event: %v", err)
@@ -634,6 +667,27 @@ func trapVarbinds(pkt *gosnmp.SnmpPacket) []map[string]string {
return out
}
func trapInstanceKey(pkt *gosnmp.SnmpPacket) string {
if pkt == nil {
return ""
}
for _, prefix := range []string{
"1.3.6.1.2.1.31.1.1.1.1.",
"1.3.6.1.2.1.2.2.1.2.",
"1.3.6.1.2.1.2.2.1.1.",
} {
for _, variable := range pkt.Variables {
name := normOID(variable.Name)
if strings.HasPrefix(name, prefix) {
if index := strings.TrimSpace(strings.TrimPrefix(name, prefix)); index != "" {
return index
}
}
}
}
return ""
}
func buildTrapReadable(trapOID string, dict *models.TrapDictionaryEntry, varbindSummary string) string {
if dict != nil && firstNonEmpty(dict.Name, dict.Title) != "" {
return firstNonEmpty(dict.Name, dict.Title) + " (" + trapOID + ")"
@@ -644,34 +698,49 @@ func buildTrapReadable(trapOID string, dict *models.TrapDictionaryEntry, varbind
return truncate(varbindSummary, 256)
}
func trapRuleMatches(rule *models.TrapRule, trapOID, varbindFP string) bool {
type trapRuleMatch struct {
Rule *models.TrapRule
State string
}
func trapRuleState(rule *models.TrapRule, trapOID, varbindFP string) (string, bool) {
hasOID := strings.TrimSpace(rule.OIDPrefix) != ""
hasRE := strings.TrimSpace(rule.VarbindMatchRegex) != ""
hasRecoveryRE := strings.TrimSpace(rule.RecoveryMatchRegex) != ""
if !hasOID && !hasRE {
return false
return "", false
}
if hasOID && !strings.HasPrefix(normOID(trapOID), normOID(rule.OIDPrefix)) {
return false
return "", false
}
if hasRecoveryRE {
re, err := regexp.Compile(rule.RecoveryMatchRegex)
if err != nil {
return "", false
}
if re.MatchString(trapOID) || re.MatchString(varbindFP) {
return "resolved", true
}
}
if hasRE {
re, err := regexp.Compile(rule.VarbindMatchRegex)
if err != nil {
return false
return "", false
}
if !re.MatchString(varbindFP) {
return false
return "", false
}
}
return true
return "firing", true
}
func firstMatchingTrapRule(rules []models.TrapRule, trapOID, varbindFP string) *models.TrapRule {
func firstMatchingTrapRule(rules []models.TrapRule, trapOID, varbindFP string) trapRuleMatch {
for i := range rules {
if trapRuleMatches(&rules[i], trapOID, varbindFP) {
return &rules[i]
if state, matched := trapRuleState(&rules[i], trapOID, varbindFP); matched {
return trapRuleMatch{Rule: &rules[i], State: state}
}
}
return nil
return trapRuleMatch{}
}
func firstNonEmpty(a, b string) string {
@@ -681,6 +750,29 @@ func firstNonEmpty(a, b string) string {
return b
}
func alertLifecycleFingerprint(body AlertReceiveBody, lifecycleKey, instance string) string {
lifecycleKey = strings.TrimSpace(lifecycleKey)
if lifecycleKey == "" {
return ""
}
resourceUID := ""
sourceIP := ""
if body.Labels != nil {
resourceUID = strings.TrimSpace(body.Labels["resource_uid"])
sourceIP = strings.TrimSpace(body.Labels["ip"])
}
identity := strings.Join([]string{
strings.TrimSpace(body.Agent),
resourceUID,
sourceIP,
strconv.FormatUint(uint64(body.PolicyID), 10),
lifecycleKey,
strings.TrimSpace(instance),
}, "\x00")
sum := sha256.Sum256([]byte(identity))
return hex.EncodeToString(sum[:])
}
func (e *Engine) resolveResource(sourceIP, hostname string) (resourceRef, string) {
e.mu.RLock()
ipMap := e.resourceByIP

View File

@@ -16,6 +16,7 @@ func validateSyslogRule(rule *models.SyslogRule) error {
}{
{name: "keyword_regex", pattern: rule.KeywordRegex},
{name: "message_regex", pattern: rule.MessageRegex},
{name: "recovery_match_regex", pattern: rule.RecoveryMatchRegex},
{name: "resource_uid_extract_regex", pattern: rule.ResourceUIDExtractRegex},
}
for _, field := range regexFields {
@@ -48,18 +49,25 @@ func validateSyslogRule(rule *models.SyslogRule) error {
strings.TrimSpace(rule.MessageRegex) == "" {
return fmt.Errorf("Syslog 规则的匹配条件全部为空,运行时永远不会命中;请至少填写 device_name_contains、source_match、keyword_regex、message_regex 中的一项")
}
if strings.TrimSpace(rule.RecoveryMatchRegex) != "" && strings.TrimSpace(rule.LifecycleKey) == "" {
return fmt.Errorf("配置 recovery_match_regex 时 lifecycle_key 不能为空")
}
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 err := validateOptionalRegex("varbind_match_regex", rule.VarbindMatchRegex); err != nil {
return err
}
if err := validateOptionalRegex("recovery_match_regex", rule.RecoveryMatchRegex); err != nil {
return err
}
if strings.TrimSpace(rule.OIDPrefix) == "" && strings.TrimSpace(rule.VarbindMatchRegex) == "" {
return fmt.Errorf("Trap 规则的匹配条件全部为空,运行时永远不会命中;请至少填写 oid_prefix、varbind_match_regex 中的一项")
}
if strings.TrimSpace(rule.RecoveryMatchRegex) != "" && strings.TrimSpace(rule.LifecycleKey) == "" {
return fmt.Errorf("配置 recovery_match_regex 时 lifecycle_key 不能为空")
}
return nil
}

View File

@@ -108,6 +108,8 @@ func seedDefaultSyslogRules(db *gorm.DB) error {
KeywordRegex: "(?i)(link down|interface .* down|port .* down)",
SourceMatch: "",
MessageRegex: "(?i)(link down|interface .* down|port .* down|LINK_DOWN)",
RecoveryMatchRegex: `(?i)(link[ _-]?up|interface .* up|port .* up|ifup)`,
LifecycleKey: "syslog-link-state",
AlertName: "Syslog链路中断",
SeverityCode: "major",
SeverityMappingJSON: `{"(?i)(critical|fatal|emergency)":"critical","(?i)(error|LINK_DOWN|down)":"major","(?i)(warning|warn)":"warning"}`,
@@ -118,8 +120,10 @@ func seedDefaultSyslogRules(db *gorm.DB) error {
Name: "H3C-Syslog-接口中断",
Enabled: true,
Priority: 120,
SourceMatch: "h3c",
DeviceNameContains: "h3c",
MessageRegex: `(?i)(LINK_DOWN|Interface .* down|port .* down)`,
RecoveryMatchRegex: `(?i)(link[ _-]?up|interface .* up|port .* up|ifup)`,
LifecycleKey: "h3c-syslog-interface-state",
AlertName: "H3C Syslog接口中断",
SeverityCode: "major",
SeverityMappingJSON: `{"(?i)(LINK_DOWN|down)":"major","(?i)(LINK_UP|up)":"info"}`,
@@ -147,6 +151,8 @@ func seedDefaultSyslogRules(db *gorm.DB) error {
"source_match",
"keyword_regex",
"message_regex",
"recovery_match_regex",
"lifecycle_key",
"alert_name",
"severity_code",
"severity_mapping_json",
@@ -162,14 +168,16 @@ func seedDefaultSyslogRules(db *gorm.DB) error {
func seedDefaultTrapRules(db *gorm.DB) error {
rows := []TrapRule{
{
Name: "默认-Trap链路中断",
Enabled: true,
Priority: 100,
OIDPrefix: "1.3.6.1.6.3.1.1.5",
VarbindMatchRegex: "(?i)(linkdown|ifdown|down)",
AlertName: "SNMP Trap链路中断",
SeverityCode: "major",
PolicyID: 0,
Name: "默认-Trap链路中断",
Enabled: true,
Priority: 100,
OIDPrefix: "1.3.6.1.6.3.1.1.5",
VarbindMatchRegex: `(?i)(1\.3\.6\.1\.6\.3\.1\.1\.5\.3([^0-9]|$)|\b(linkdown|ifdown|down)\b)`,
RecoveryMatchRegex: `(?i)(1\.3\.6\.1\.6\.3\.1\.1\.5\.4([^0-9]|$)|\b(linkup|ifup)\b)`,
LifecycleKey: "snmp-interface-link-state",
AlertName: "SNMP Trap链路中断",
SeverityCode: "major",
PolicyID: 0,
},
}
for _, row := range rows {
@@ -190,6 +198,8 @@ func seedDefaultTrapRules(db *gorm.DB) error {
"priority",
"o_id_prefix",
"varbind_match_regex",
"recovery_match_regex",
"lifecycle_key",
"alert_name",
"severity_code",
"policy_id",

View File

@@ -24,6 +24,10 @@ type SyslogRule struct {
KeywordRegex string `gorm:"size:512" json:"keyword_regex"`
// MessageRegex 表示消息正文匹配的正则表达式。
MessageRegex string `gorm:"size:1024" json:"message_regex"`
// RecoveryMatchRegex 匹配同一生命周期的恢复消息。
RecoveryMatchRegex string `gorm:"size:1024" json:"recovery_match_regex"`
// LifecycleKey 将故障和恢复事件绑定到同一告警生命周期。
LifecycleKey string `gorm:"size:256" json:"lifecycle_key"`
// AlertName 表示告警名称。
AlertName string `gorm:"size:256" json:"alert_name"`
// SeverityCode 表示严重级别编码。

View File

@@ -1,33 +1,37 @@
package models
import "time"
// TrapRule 表示一条 SNMP Trap 规则,用于匹配并触发告警策略。
type TrapRule struct {
// ID 是数据库主键。
ID uint `gorm:"primaryKey" json:"id"`
// CreatedAt 记录创建时间GORM 自动维护)。
CreatedAt time.Time `json:"created_at"`
// UpdatedAt 记录更新时间GORM 自动维护)。
UpdatedAt time.Time `json:"updated_at"`
// Name 规则名称,用于展示/标识。
Name string `gorm:"size:256" json:"name"`
// Enabled 表示该规则是否启用。
Enabled bool `gorm:"default:true" json:"enabled"`
// Priority 表示匹配优先级(数值越高/低需以业务约定为准)。
Priority int `gorm:"index" json:"priority"`
// OIDPrefix 表示匹配的 OID 前缀。
OIDPrefix string `gorm:"size:512" json:"oid_prefix"`
// VarbindMatchRegex 表示对 varbind 内容的正则匹配条件。
VarbindMatchRegex string `gorm:"size:512" json:"varbind_match_regex"`
// AlertName 表示告警名称
AlertName string `gorm:"size:256" json:"alert_name"`
// SeverityCode 表示严重级别编码
SeverityCode string `gorm:"size:32" json:"severity_code"`
// PolicyID 表示关联的告警/处理策略 ID
PolicyID uint `json:"policy_id"`
}
func (TrapRule) TableName() string {
return "logs_trap_rules"
}
package models
import "time"
// TrapRule 表示一条 SNMP Trap 规则,用于匹配并触发告警策略。
type TrapRule struct {
// ID 是数据库主键。
ID uint `gorm:"primaryKey" json:"id"`
// CreatedAt 记录创建时间GORM 自动维护)。
CreatedAt time.Time `json:"created_at"`
// UpdatedAt 记录更新时间GORM 自动维护)。
UpdatedAt time.Time `json:"updated_at"`
// Name 规则名称,用于展示/标识。
Name string `gorm:"size:256" json:"name"`
// Enabled 表示该规则是否启用。
Enabled bool `gorm:"default:true" json:"enabled"`
// Priority 表示匹配优先级(数值越高/低需以业务约定为准)。
Priority int `gorm:"index" json:"priority"`
// OIDPrefix 表示匹配的 OID 前缀。
OIDPrefix string `gorm:"size:512" json:"oid_prefix"`
// VarbindMatchRegex 表示对 varbind 内容的正则匹配条件。
VarbindMatchRegex string `gorm:"size:512" json:"varbind_match_regex"`
// RecoveryMatchRegex 匹配同一生命周期的恢复 Trap OID 或 varbind
RecoveryMatchRegex string `gorm:"size:1024" json:"recovery_match_regex"`
// LifecycleKey 将故障和恢复事件绑定到同一告警生命周期
LifecycleKey string `gorm:"size:256" json:"lifecycle_key"`
// AlertName 表示告警名称
AlertName string `gorm:"size:256" json:"alert_name"`
// SeverityCode 表示严重级别编码。
SeverityCode string `gorm:"size:32" json:"severity_code"`
// PolicyID 表示关联的告警/处理策略 ID。
PolicyID uint `json:"policy_id"`
}
func (TrapRule) TableName() string {
return "logs_trap_rules"
}