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