package ingest import ( "encoding/json" "fmt" "strconv" "strings" "time" "git.apinb.com/ops/logs/internal/impl" "git.apinb.com/ops/logs/internal/models" "gorm.io/gorm" ) func BuildReplayRawEventPayload(ev models.LogEvent) (RawEventIngestBody, error) { sourceType := replaySourceType(ev.SourceKind) if sourceType == "" { return RawEventIngestBody{}, fmt.Errorf("unsupported source kind %q", ev.SourceKind) } now := time.Now().UTC() rawObj := map[string]interface{}{ "source": sourceType, "replayed_at": now.Format(time.RFC3339Nano), "log_entry_id": ev.ID, "source_ip": ev.SourceIP, "remote_addr": ev.RemoteAddr, "raw_packet": ev.RawPayload, } if ev.TrapOID != "" { rawObj["trap_oid"] = ev.TrapOID } if ev.NormalizedDetail != "" { var detail interface{} if err := json.Unmarshal([]byte(ev.NormalizedDetail), &detail); err == nil { rawObj["parsed"] = detail } } rawBytes, err := json.Marshal(rawObj) if err != nil { return RawEventIngestBody{}, err } labels := map[string]string{ "source_type": "log", "source_subtype": replaySubtype(ev.SourceKind), "replay_of_log_event_id": strconv.FormatUint(uint64(ev.ID), 10), "ip": ev.SourceIP, "remote_addr": ev.RemoteAddr, "device": ev.DeviceName, "job": "logs-replay", } resourceUID := replayResourceUID(ev) category, identity := splitResourceUID(resourceUID) labels["resource_uid"] = resourceUID labels["resource_category"] = category labels["service_identity"] = identity return RawEventIngestBody{ SourceType: sourceType, SourceEventKey: fmt.Sprintf("logs:replay:%d:%d", ev.ID, now.UnixNano()), State: "firing", ResourceUID: resourceUID, EventTime: now, Severity: firstNonEmpty(ev.SeverityCode, "warning"), Title: replayTitle(ev), Message: firstNonEmpty(ev.NormalizedSummary, ev.RawPayload), Labels: labels, Annotations: map[string]string{ "replay": "true", "dispatch_status": ev.DispatchStatus, }, ParseStatus: "parsed", RawPayload: rawBytes, }, nil } func EnqueueReplayLogEvent(ev models.LogEvent) (uint, error) { body, err := BuildReplayRawEventPayload(ev) if err != nil { return 0, err } payload, err := json.Marshal(body) if err != nil { return 0, err } row := models.AlertOutbox{ LogEventID: ev.ID, PayloadJSON: string(payload), Status: outboxStatusPending, RetryCount: 0, } if impl.DBService == nil { return 0, fmt.Errorf("database is not initialized") } if err := impl.DBService.Transaction(func(tx *gorm.DB) error { now, err := databaseNow(tx) if err != nil { return err } row.NextRetryAt = now if err := enqueueOutboxRowWithDB(tx, &row); err != nil { return err } result := tx.Model(&models.LogEvent{}).Where("id = ?", ev.ID).Updates(map[string]interface{}{ "dispatch_status": "pending", "dispatch_outbox_id": row.ID, "alert_sent": false, }) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return fmt.Errorf("log event %d does not exist", ev.ID) } return nil }); err != nil { return 0, err } return row.ID, nil } func enqueueOutboxRowWithDB(db *gorm.DB, row *models.AlertOutbox) error { if db == nil { return fmt.Errorf("database is not initialized") } return db.Create(row).Error } func replaySourceType(kind string) string { switch strings.TrimSpace(kind) { case "syslog": return "syslog" case "snmp_trap", "trap": return "trap" default: return "" } } func replaySubtype(kind string) string { if kind == "snmp_trap" { return "snmp_trap" } return replaySourceType(kind) } func replayResourceUID(ev models.LogEvent) string { if uid := strings.TrimSpace(ev.ResourceUID); uid != "" { if category, identity, ok := splitKnownResourceUID(uid); ok { return category + ":" + identity } return canonicalLogResourceCategory(ev.ResourceType, replaySubtype(ev.SourceKind)) + ":" + uid } if category, identity, ok := splitKnownResourceUID(ev.ResourceID); ok { return category + ":" + identity } if ev.ResourceType != "" && ev.ResourceID != "" { return canonicalLogResourceCategory(ev.ResourceType, replaySubtype(ev.SourceKind)) + ":" + ev.ResourceID } category := canonicalLogResourceCategory("", replaySubtype(ev.SourceKind)) identity := firstNonEmpty(strings.TrimSpace(ev.SourceIP), strings.TrimSpace(ev.DeviceName)) if identity == "" { identity = fmt.Sprintf("log-event-%d", ev.ID) } return category + ":" + identity } func splitResourceUID(uid string) (string, string) { parts := strings.SplitN(strings.TrimSpace(uid), ":", 2) if len(parts) != 2 { return "log_source", strings.TrimSpace(uid) } return parts[0], parts[1] } func replayTitle(ev models.LogEvent) string { if ev.SourceKind == "snmp_trap" && ev.TrapOID != "" { return "重放 SNMP Trap " + ev.TrapOID } if ev.SourceKind == "syslog" { return "重放 Syslog" } return "重放日志事件" }