Files
logs/internal/ingest/replay.go

183 lines
4.9 KiB
Go

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 "重放日志事件"
}