发布 0.3.0:恢复 Chat 接口并增强云端事件通知
CI / test (push) Successful in 31s
Release / release (push) Successful in 48s

This commit is contained in:
2026-07-13 10:13:44 +08:00
parent 489cb49cb3
commit c7cc5616ed
31 changed files with 1682 additions and 1625 deletions
+121 -24
View File
@@ -57,9 +57,6 @@ type LogEventService struct {
relayPollTick time.Duration // 订阅确认轮询间隔,零值用默认
relayPollTimeout time.Duration // 订阅确认轮询上限,零值用默认
alertMu sync.Mutex // 保护告警规则冷却表
alertSentAt map[uint]time.Time // 规则 ID → 上次告警时刻(阈值型规则冷却)
}
// NewLogEventService 组装依赖;调用 StartParser / StartCleanup 后台协程后生效。
@@ -373,20 +370,18 @@ func (s *LogEventService) parseOnce(ctx context.Context) {
if len(events) == 0 {
return
}
rules := s.loadEnabledAlertRules(ctx)
for i := range events {
s.processLogEvent(ctx, &events[i], rules)
s.processLogEvent(ctx, &events[i])
}
}
// processLogEvent 仅在条件更新命中原行后触发通知,删除并发胜出时静默跳过。
func (s *LogEventService) processLogEvent(ctx context.Context, event *model.LogEvent, rules []model.AlertRule) {
func (s *LogEventService) processLogEvent(ctx context.Context, event *model.LogEvent) {
parsed := parseLogEvent([]byte(event.Payload))
if !s.updateParsedEvent(ctx, event, parsed) {
return
}
s.notifyCritical(ctx, event, parsed)
s.matchAlertRules(ctx, rules, event, parsed)
}
// updateParsedEvent 用条件 UPDATE 禁止 Save 在删除后隐式重建事件。
@@ -413,26 +408,49 @@ func (s *LogEventService) updateParsedEvent(ctx context.Context, event *model.Lo
// onsEnvelope 覆盖 ONS 消息与 CloudEvents 审计事件的常见字段;
// 真实格式以联调实测为准,提不出字段时只置 Processed 不回填。
type onsEnvelope struct {
EventType string `json:"eventType"`
Type string `json:"type"`
Source string `json:"source"`
EventTime string `json:"eventTime"`
ResourceName string `json:"resourceName"`
Data json.RawMessage `json:"data"`
Identity *onsIdentity `json:"identity"`
EventType string `json:"eventType"`
Type string `json:"type"`
Source string `json:"source"`
EventTime string `json:"eventTime"`
ResourceName string `json:"resourceName"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
Identity *onsIdentity `json:"identity"`
AdditionalDetails *onsAddDetails `json:"additionalDetails"`
StateChange *onsStateChange `json:"stateChange"`
}
// onsIdentity 是 Audit 事件 data.identity 中与展示相关的字段。
type onsIdentity struct {
IPAddress string `json:"ipAddress"`
IPAddress string `json:"ipAddress"`
PrincipalName string `json:"principalName"`
}
// parsedEvent 是从消息原文提取的展示字段集;ResourceName 仅供 P2 推送文案,不落库。
// onsAddDetails 是 IDCS 登录类事件 data.additionalDetails 的补充字段;
// AuditEventMapValue 为嵌套的 JSON 字符串(含 eventId 成败与失败原因)。
type onsAddDetails struct {
ActorName string `json:"actorName"`
ClientIP string `json:"clientIp"`
AuditEventMapValue string `json:"auditEventMapValue"`
}
// onsStateChange 承载 Audit v2 的资源变更快照;description 供策略类事件推送文案。
type onsStateChange struct {
Current struct {
Description string `json:"description"`
} `json:"current"`
}
// parsedEvent 是从消息原文提取的展示字段集;ResourceName/Actor/Outcome/Detail
// 仅供 P2 推送文案,不落库。
type parsedEvent struct {
EventType string
Source string
SourceIP string
ResourceName string
Actor string // 操作者(identity.principalName,登录事件回退 actorName)
Outcome string // 成功 / 失败 / 空(判读不出)
Detail string // 补充说明(策略描述、登录失败原因等)
EventTime *time.Time
}
@@ -455,7 +473,7 @@ func parseLogEvent(payload []byte) parsedEvent {
return envelopeFields(env)
}
// mergeEnvelope 外层缺失字段时以 data 内层补齐(Audit 的 identity 在内层)。
// mergeEnvelope 外层缺失字段时以 data 内层补齐(Audit 的 identity 在内层)。
func mergeEnvelope(outer, inner onsEnvelope) onsEnvelope {
if outer.EventType == "" {
outer.EventType = inner.EventType
@@ -472,13 +490,22 @@ func mergeEnvelope(outer, inner onsEnvelope) onsEnvelope {
if outer.ResourceName == "" {
outer.ResourceName = inner.ResourceName
}
if outer.Message == "" {
outer.Message = inner.Message
}
if outer.Identity == nil {
outer.Identity = inner.Identity
}
if outer.AdditionalDetails == nil {
outer.AdditionalDetails = inner.AdditionalDetails
}
if outer.StateChange == nil {
outer.StateChange = inner.StateChange
}
return outer
}
// envelopeFields 收敛字段别名并解析事件时间。
// envelopeFields 收敛字段别名并解析事件时间与推送用补充字段
func envelopeFields(env onsEnvelope) parsedEvent {
eventType := env.EventType
if eventType == "" {
@@ -490,19 +517,81 @@ func envelopeFields(env onsEnvelope) parsedEvent {
eventTime = &ts
}
}
ip := ""
if env.Identity != nil {
ip = env.Identity.IPAddress
}
outcome, detail := envOutcome(env)
return parsedEvent{
EventType: clip(eventType, 128),
Source: clip(env.Source, 64),
SourceIP: clip(ip, 64),
SourceIP: clip(envIP(env), 64),
ResourceName: clip(env.ResourceName, 128),
Actor: clipRunes(envActor(env), 64),
Outcome: outcome,
Detail: clipRunes(detail, 200),
EventTime: eventTime,
}
}
// envIP 取发起方 IP:Audit 的 identity.ipAddress,登录事件回退 additionalDetails.clientIp。
func envIP(env onsEnvelope) string {
if env.Identity != nil && env.Identity.IPAddress != "" {
return env.Identity.IPAddress
}
if env.AdditionalDetails != nil {
return env.AdditionalDetails.ClientIP
}
return ""
}
// envActor 取操作者:Audit 的 identity.principalName,登录事件回退 additionalDetails.actorName。
func envActor(env onsEnvelope) string {
if env.Identity != nil && env.Identity.PrincipalName != "" {
return env.Identity.PrincipalName
}
if env.AdditionalDetails != nil {
return env.AdditionalDetails.ActorName
}
return ""
}
// envOutcome 判读事件成败与补充说明:登录事件解析 auditEventMapValue;其余
// Audit 事件看 message 后缀,补充说明取 stateChange.current.description(策略描述)。
func envOutcome(env onsEnvelope) (outcome, detail string) {
if env.StateChange != nil {
detail = env.StateChange.Current.Description
}
if env.AdditionalDetails != nil && env.AdditionalDetails.AuditEventMapValue != "" {
return ssoOutcome(env.AdditionalDetails.AuditEventMapValue, detail)
}
switch {
case strings.HasSuffix(env.Message, " succeeded"):
outcome = "成功"
case strings.HasSuffix(env.Message, " failed"):
outcome = "失败"
}
return outcome, detail
}
// ssoOutcome 解析 IDCS 审计负载(JSON 字符串):eventId 含 success/failure 定成败,
// 失败时以其 message 作为原因说明。
func ssoOutcome(raw, detail string) (string, string) {
var ev struct {
EventID string `json:"eventId"`
Message string `json:"message"`
}
if json.Unmarshal([]byte(raw), &ev) != nil {
return "", detail
}
switch {
case strings.Contains(ev.EventID, "success"):
return "成功", detail
case strings.Contains(ev.EventID, "failure"):
if ev.Message != "" {
detail = ev.Message
}
return "失败", detail
}
return "", detail
}
// clip 按模型列宽截断解析出的字段。
func clip(s string, max int) string {
if len(s) > max {
@@ -511,6 +600,15 @@ func clip(s string, max int) string {
return s
}
// clipRunes 按字符数截断(通知文案用,中文安全)。
func clipRunes(s string, max int) string {
r := []rune(s)
if len(r) > max {
return string(r[:max])
}
return s
}
// StartCleanup 启动周期清理:启动即清一次,之后每 24h 一次,随 ctx 取消退出。
func (s *LogEventService) StartCleanup(ctx context.Context) {
s.wg.Add(1)
@@ -535,7 +633,6 @@ func (s *LogEventService) cleanupOnce(ctx context.Context) {
if err := s.cleanup(ctx, logEventRetention, logEventMaxRows); err != nil {
log.Printf("log event cleanup: %v", err)
}
s.cleanupAlertHits(ctx)
}
// cleanup 先删过期记录,再对超量部分删最旧;阈值参数化便于测试。