任务执行1-19
This commit is contained in:
@@ -1,64 +1,139 @@
|
||||
package ingest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/ops/logs/internal/config"
|
||||
)
|
||||
|
||||
// AlertReceiveBody 与 alert ReceiveRequest 对齐(含必填 raw_data)
|
||||
type AlertReceiveBody struct {
|
||||
AlertName string `json:"alert_name"`
|
||||
Summary string `json:"summary"`
|
||||
Description string `json:"description"`
|
||||
SeverityCode string `json:"severity_code"`
|
||||
Value string `json:"value"`
|
||||
Threshold string `json:"threshold"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Agent string `json:"agent"`
|
||||
PolicyID uint `json:"policy_id"`
|
||||
RawData json.RawMessage `json:"raw_data"`
|
||||
}
|
||||
|
||||
func forwardAlert(body AlertReceiveBody) error {
|
||||
cfg := config.Spec.AlertForward
|
||||
if cfg == nil || !cfg.Enabled || cfg.BaseURL == "" {
|
||||
return nil
|
||||
}
|
||||
if len(body.RawData) == 0 {
|
||||
return fmt.Errorf("raw_data 不能为空")
|
||||
}
|
||||
if body.AlertName == "" {
|
||||
body.AlertName = "日志告警"
|
||||
}
|
||||
if body.PolicyID == 0 && cfg.DefaultPolicyID > 0 {
|
||||
body.PolicyID = cfg.DefaultPolicyID
|
||||
}
|
||||
raw, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := cfg.BaseURL + "/Alert/v1/alerts/receive"
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if cfg.InternalKey != "" {
|
||||
req.Header.Set("X-Internal-Key", cfg.InternalKey)
|
||||
}
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("alert returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
package ingest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/ops/logs/internal/config"
|
||||
)
|
||||
|
||||
// AlertReceiveBody 与 alert ReceiveRequest 对齐(含必填 raw_data)
|
||||
type AlertReceiveBody struct {
|
||||
AlertName string `json:"alert_name"`
|
||||
Summary string `json:"summary"`
|
||||
Description string `json:"description"`
|
||||
SeverityCode string `json:"severity_code"`
|
||||
Value string `json:"value"`
|
||||
Threshold string `json:"threshold"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Agent string `json:"agent"`
|
||||
PolicyID uint `json:"policy_id"`
|
||||
RawData json.RawMessage `json:"raw_data"`
|
||||
}
|
||||
|
||||
type RawEventIngestBody struct {
|
||||
SourceType string `json:"source_type"`
|
||||
ResourceUID string `json:"resource_uid,omitempty"`
|
||||
EventTime time.Time `json:"event_time"`
|
||||
Severity string `json:"severity"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
ParseStatus string `json:"parse_status"`
|
||||
RawPayload json.RawMessage `json:"raw_payload"`
|
||||
TraceID string `json:"trace_id,omitempty"`
|
||||
}
|
||||
|
||||
func forwardAlert(body AlertReceiveBody) error {
|
||||
cfg := config.Spec.AlertForward
|
||||
if cfg == nil || !cfg.Enabled || cfg.BaseURL == "" {
|
||||
return nil
|
||||
}
|
||||
if len(body.RawData) == 0 {
|
||||
return fmt.Errorf("raw_data 不能为空")
|
||||
}
|
||||
if body.AlertName == "" {
|
||||
body.AlertName = "日志告警"
|
||||
}
|
||||
if body.PolicyID == 0 && cfg.DefaultPolicyID > 0 {
|
||||
body.PolicyID = cfg.DefaultPolicyID
|
||||
}
|
||||
rawEvent := buildRawEventIngestBody(body, "parsed")
|
||||
raw, err := json.Marshal(rawEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := cfg.BaseURL + "/Alert/v1/raw-events/ingest"
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if cfg.InternalKey != "" {
|
||||
req.Header.Set("X-Internal-Key", cfg.InternalKey)
|
||||
}
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("alert returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildRawEventIngestBody(body AlertReceiveBody, parseStatus string) RawEventIngestBody {
|
||||
sourceType := rawEventSourceType(body)
|
||||
if parseStatus == "" {
|
||||
parseStatus = "parsed"
|
||||
}
|
||||
annotations := map[string]string{
|
||||
"description": body.Description,
|
||||
"value": body.Value,
|
||||
"threshold": body.Threshold,
|
||||
"agent": body.Agent,
|
||||
}
|
||||
return RawEventIngestBody{
|
||||
SourceType: sourceType,
|
||||
ResourceUID: rawEventResourceUID(body.Labels),
|
||||
EventTime: time.Now().UTC(),
|
||||
Severity: body.SeverityCode,
|
||||
Title: firstNonEmpty(body.AlertName, "日志事件"),
|
||||
Message: firstNonEmpty(body.Summary, body.Description),
|
||||
Labels: body.Labels,
|
||||
Annotations: annotations,
|
||||
ParseStatus: parseStatus,
|
||||
RawPayload: body.RawData,
|
||||
}
|
||||
}
|
||||
|
||||
func rawEventSourceType(body AlertReceiveBody) string {
|
||||
if body.Labels != nil {
|
||||
switch strings.TrimSpace(body.Labels["source_subtype"]) {
|
||||
case "syslog":
|
||||
return "syslog"
|
||||
case "snmp_trap":
|
||||
return "trap"
|
||||
}
|
||||
}
|
||||
switch body.Agent {
|
||||
case "logs-syslog":
|
||||
return "syslog"
|
||||
case "logs-trap":
|
||||
return "trap"
|
||||
default:
|
||||
return "syslog"
|
||||
}
|
||||
}
|
||||
|
||||
func rawEventResourceUID(labels map[string]string) string {
|
||||
if labels == nil {
|
||||
return ""
|
||||
}
|
||||
if uid := strings.TrimSpace(labels["resource_uid"]); uid != "" {
|
||||
return uid
|
||||
}
|
||||
category := strings.TrimSpace(labels["resource_category"])
|
||||
identity := strings.TrimSpace(labels["service_identity"])
|
||||
if category != "" && identity != "" {
|
||||
return category + ":" + identity
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
59
internal/ingest/alert_forward_test.go
Normal file
59
internal/ingest/alert_forward_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package ingest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.apinb.com/ops/logs/internal/config"
|
||||
)
|
||||
|
||||
func TestForwardAlertPostsRawEventIngestPayload(t *testing.T) {
|
||||
var gotPath string
|
||||
var gotBody RawEventIngestBody
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Fatalf("decode body: %v", err)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config.Spec.AlertForward = &config.AlertForwardConf{
|
||||
Enabled: true,
|
||||
BaseURL: server.URL,
|
||||
InternalKey: "internal-key",
|
||||
}
|
||||
|
||||
err := forwardAlert(AlertReceiveBody{
|
||||
AlertName: "H3C Trap",
|
||||
Summary: "Interface down",
|
||||
Description: "Interface down",
|
||||
SeverityCode: "major",
|
||||
Labels: map[string]string{
|
||||
"source_subtype": "snmp_trap",
|
||||
"ip": "192.168.1.10",
|
||||
},
|
||||
Agent: "logs-trap",
|
||||
RawData: json.RawMessage(`{"trap_oid":"1.3.6.1.4.1"}`),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("forwardAlert returned error: %v", err)
|
||||
}
|
||||
|
||||
if gotPath != "/Alert/v1/raw-events/ingest" {
|
||||
t.Fatalf("expected raw event ingest path, got %q", gotPath)
|
||||
}
|
||||
if gotBody.SourceType != "trap" {
|
||||
t.Fatalf("expected trap source type, got %q", gotBody.SourceType)
|
||||
}
|
||||
if gotBody.ParseStatus != "parsed" {
|
||||
t.Fatalf("expected parsed status, got %q", gotBody.ParseStatus)
|
||||
}
|
||||
if string(gotBody.RawPayload) != `{"trap_oid":"1.3.6.1.4.1"}` {
|
||||
t.Fatalf("raw payload changed: %s", string(gotBody.RawPayload))
|
||||
}
|
||||
}
|
||||
@@ -1,125 +1,181 @@
|
||||
package ingest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/ops/logs/internal/impl"
|
||||
"git.apinb.com/ops/logs/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
outboxStatusPending = "pending"
|
||||
outboxStatusRetrying = "retrying"
|
||||
outboxStatusSent = "sent"
|
||||
outboxStatusDead = "dead"
|
||||
)
|
||||
|
||||
func enqueueAlert(logEventID uint, body AlertReceiveBody) error {
|
||||
payload, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row := models.AlertOutbox{
|
||||
LogEventID: logEventID,
|
||||
PayloadJSON: string(payload),
|
||||
Status: outboxStatusPending,
|
||||
RetryCount: 0,
|
||||
NextRetryAt: time.Now(),
|
||||
LastError: "",
|
||||
}
|
||||
return impl.DBService.Create(&row).Error
|
||||
}
|
||||
|
||||
func StartAlertDispatcher() {
|
||||
go func() {
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
processAlertOutboxBatch(20)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func processAlertOutboxBatch(limit int) {
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
var rows []models.AlertOutbox
|
||||
now := time.Now()
|
||||
err := impl.DBService.
|
||||
Where("status IN ? AND next_retry_at <= ?", []string{outboxStatusPending, outboxStatusRetrying}, now).
|
||||
Order("id asc").
|
||||
Limit(limit).
|
||||
Find(&rows).Error
|
||||
if err != nil || len(rows) == 0 {
|
||||
return
|
||||
}
|
||||
for _, row := range rows {
|
||||
processOneOutbox(row)
|
||||
}
|
||||
}
|
||||
|
||||
func processOneOutbox(row models.AlertOutbox) {
|
||||
var body AlertReceiveBody
|
||||
if err := json.Unmarshal([]byte(row.PayloadJSON), &body); err != nil {
|
||||
markOutboxDead(row.ID, row.RetryCount, "invalid_payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := forwardAlert(body); err != nil {
|
||||
markOutboxRetry(row, err.Error())
|
||||
return
|
||||
}
|
||||
_ = impl.DBService.Model(&models.AlertOutbox{}).Where("id = ?", row.ID).Updates(map[string]interface{}{
|
||||
"status": outboxStatusSent,
|
||||
"last_error": "",
|
||||
"next_retry_at": time.Now(),
|
||||
}).Error
|
||||
_ = impl.DBService.Model(&models.LogEvent{}).Where("id = ?", row.LogEventID).Updates(map[string]interface{}{
|
||||
"alert_sent": true,
|
||||
"dispatch_status": "sent",
|
||||
}).Error
|
||||
}
|
||||
|
||||
func markOutboxRetry(row models.AlertOutbox, msg string) {
|
||||
retry := row.RetryCount + 1
|
||||
const maxRetry = 5
|
||||
if retry > maxRetry {
|
||||
markOutboxDead(row.ID, retry, msg)
|
||||
return
|
||||
}
|
||||
backoff := time.Duration(retry*retry) * time.Second
|
||||
if backoff > 60*time.Second {
|
||||
backoff = 60 * time.Second
|
||||
}
|
||||
_ = impl.DBService.Model(&models.AlertOutbox{}).Where("id = ?", row.ID).Updates(map[string]interface{}{
|
||||
"status": outboxStatusRetrying,
|
||||
"retry_count": retry,
|
||||
"next_retry_at": time.Now().Add(backoff),
|
||||
"last_error": truncateError(msg, 1024),
|
||||
}).Error
|
||||
_ = impl.DBService.Model(&models.LogEvent{}).Where("id = ?", row.LogEventID).Update("dispatch_status", "retrying").Error
|
||||
}
|
||||
|
||||
func markOutboxDead(id uint, retry int, msg string) {
|
||||
_ = impl.DBService.Model(&models.AlertOutbox{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"status": outboxStatusDead,
|
||||
"retry_count": retry,
|
||||
"next_retry_at": time.Now(),
|
||||
"last_error": truncateError(msg, 1024),
|
||||
}).Error
|
||||
var row models.AlertOutbox
|
||||
if err := impl.DBService.Select("log_event_id").First(&row, id).Error; err == nil && row.LogEventID > 0 {
|
||||
_ = impl.DBService.Model(&models.LogEvent{}).Where("id = ?", row.LogEventID).Update("dispatch_status", "dead").Error
|
||||
}
|
||||
}
|
||||
|
||||
func truncateError(s string, n int) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n]
|
||||
}
|
||||
|
||||
package ingest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/ops/logs/internal/config"
|
||||
"git.apinb.com/ops/logs/internal/impl"
|
||||
"git.apinb.com/ops/logs/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
outboxStatusPending = "pending"
|
||||
outboxStatusRetrying = "retrying"
|
||||
outboxStatusSent = "sent"
|
||||
outboxStatusDead = "dead"
|
||||
)
|
||||
|
||||
func enqueueAlert(logEventID uint, body AlertReceiveBody) error {
|
||||
payload, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return enqueuePayload(logEventID, string(payload))
|
||||
}
|
||||
|
||||
func enqueueRawEvent(logEventID uint, body AlertReceiveBody, parseStatus string) error {
|
||||
payload, err := json.Marshal(buildRawEventIngestBody(body, parseStatus))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return enqueuePayload(logEventID, string(payload))
|
||||
}
|
||||
|
||||
func enqueuePayload(logEventID uint, payloadJSON string) error {
|
||||
row := models.AlertOutbox{
|
||||
LogEventID: logEventID,
|
||||
PayloadJSON: payloadJSON,
|
||||
Status: outboxStatusPending,
|
||||
RetryCount: 0,
|
||||
NextRetryAt: time.Now(),
|
||||
LastError: "",
|
||||
}
|
||||
return impl.DBService.Create(&row).Error
|
||||
}
|
||||
|
||||
func StartAlertDispatcher() {
|
||||
go func() {
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
processAlertOutboxBatch(20)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func processAlertOutboxBatch(limit int) {
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
var rows []models.AlertOutbox
|
||||
now := time.Now()
|
||||
err := impl.DBService.
|
||||
Where("status IN ? AND next_retry_at <= ?", []string{outboxStatusPending, outboxStatusRetrying}, now).
|
||||
Order("id asc").
|
||||
Limit(limit).
|
||||
Find(&rows).Error
|
||||
if err != nil || len(rows) == 0 {
|
||||
return
|
||||
}
|
||||
for _, row := range rows {
|
||||
processOneOutbox(row)
|
||||
}
|
||||
}
|
||||
|
||||
func processOneOutbox(row models.AlertOutbox) {
|
||||
var body AlertReceiveBody
|
||||
if err := json.Unmarshal([]byte(row.PayloadJSON), &body); err != nil {
|
||||
markOutboxDead(row.ID, row.RetryCount, "invalid_payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := forwardOutboxPayload(row.PayloadJSON, body); err != nil {
|
||||
markOutboxRetry(row, err.Error())
|
||||
return
|
||||
}
|
||||
_ = impl.DBService.Model(&models.AlertOutbox{}).Where("id = ?", row.ID).Updates(map[string]interface{}{
|
||||
"status": outboxStatusSent,
|
||||
"last_error": "",
|
||||
"next_retry_at": time.Now(),
|
||||
}).Error
|
||||
_ = impl.DBService.Model(&models.LogEvent{}).Where("id = ?", row.LogEventID).Updates(map[string]interface{}{
|
||||
"alert_sent": true,
|
||||
"dispatch_status": "sent",
|
||||
}).Error
|
||||
}
|
||||
|
||||
func forwardOutboxPayload(payloadJSON string, legacyBody AlertReceiveBody) error {
|
||||
var rawEvent RawEventIngestBody
|
||||
if err := json.Unmarshal([]byte(payloadJSON), &rawEvent); err == nil && rawEvent.SourceType != "" && len(rawEvent.RawPayload) > 0 {
|
||||
return forwardRawEvent(rawEvent)
|
||||
}
|
||||
return forwardAlert(legacyBody)
|
||||
}
|
||||
|
||||
func forwardRawEvent(body RawEventIngestBody) error {
|
||||
cfg := config.Spec.AlertForward
|
||||
if cfg == nil || !cfg.Enabled || cfg.BaseURL == "" {
|
||||
return nil
|
||||
}
|
||||
if len(body.RawPayload) == 0 {
|
||||
return fmt.Errorf("raw_payload 不能为空")
|
||||
}
|
||||
raw, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := cfg.BaseURL + "/Alert/v1/raw-events/ingest"
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if cfg.InternalKey != "" {
|
||||
req.Header.Set("X-Internal-Key", cfg.InternalKey)
|
||||
}
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("alert returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func markOutboxRetry(row models.AlertOutbox, msg string) {
|
||||
retry := row.RetryCount + 1
|
||||
const maxRetry = 5
|
||||
if retry > maxRetry {
|
||||
markOutboxDead(row.ID, retry, msg)
|
||||
return
|
||||
}
|
||||
backoff := time.Duration(retry*retry) * time.Second
|
||||
if backoff > 60*time.Second {
|
||||
backoff = 60 * time.Second
|
||||
}
|
||||
_ = impl.DBService.Model(&models.AlertOutbox{}).Where("id = ?", row.ID).Updates(map[string]interface{}{
|
||||
"status": outboxStatusRetrying,
|
||||
"retry_count": retry,
|
||||
"next_retry_at": time.Now().Add(backoff),
|
||||
"last_error": truncateError(msg, 1024),
|
||||
}).Error
|
||||
_ = impl.DBService.Model(&models.LogEvent{}).Where("id = ?", row.LogEventID).Update("dispatch_status", "retrying").Error
|
||||
}
|
||||
|
||||
func markOutboxDead(id uint, retry int, msg string) {
|
||||
_ = impl.DBService.Model(&models.AlertOutbox{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"status": outboxStatusDead,
|
||||
"retry_count": retry,
|
||||
"next_retry_at": time.Now(),
|
||||
"last_error": truncateError(msg, 1024),
|
||||
}).Error
|
||||
var row models.AlertOutbox
|
||||
if err := impl.DBService.Select("log_event_id").First(&row, id).Error; err == nil && row.LogEventID > 0 {
|
||||
_ = impl.DBService.Model(&models.LogEvent{}).Where("id = ?", row.LogEventID).Update("dispatch_status", "dead").Error
|
||||
}
|
||||
}
|
||||
|
||||
func truncateError(s string, n int) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
131
internal/ingest/replay.go
Normal file
131
internal/ingest/replay.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package ingest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/ops/logs/internal/impl"
|
||||
"git.apinb.com/ops/logs/internal/models"
|
||||
)
|
||||
|
||||
func BuildReplayRawEventPayload(ev models.LogEvent) (RawEventIngestBody, error) {
|
||||
sourceType := replaySourceType(ev.SourceKind)
|
||||
if sourceType == "" {
|
||||
return RawEventIngestBody{}, fmt.Errorf("unsupported source kind %q", ev.SourceKind)
|
||||
}
|
||||
rawObj := map[string]interface{}{
|
||||
"source": sourceType,
|
||||
"replayed_at": time.Now().UTC().Format(time.RFC3339),
|
||||
"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",
|
||||
}
|
||||
if uid := replayResourceUID(ev); uid != "" {
|
||||
labels["resource_uid"] = uid
|
||||
}
|
||||
return RawEventIngestBody{
|
||||
SourceType: sourceType,
|
||||
ResourceUID: replayResourceUID(ev),
|
||||
EventTime: time.Now().UTC(),
|
||||
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: "replayed",
|
||||
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,
|
||||
NextRetryAt: time.Now(),
|
||||
}
|
||||
if err := enqueueOutboxRow(&row); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return row.ID, nil
|
||||
}
|
||||
|
||||
func enqueueOutboxRow(row *models.AlertOutbox) error {
|
||||
return impl.DBService.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 strings.Contains(ev.ResourceID, ":") {
|
||||
return ev.ResourceID
|
||||
}
|
||||
if ev.ResourceType != "" && ev.ResourceID != "" {
|
||||
return ev.ResourceType + ":" + ev.ResourceID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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 "重放日志事件"
|
||||
}
|
||||
61
internal/ingest/replay_test.go
Normal file
61
internal/ingest/replay_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package ingest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"git.apinb.com/ops/logs/internal/models"
|
||||
)
|
||||
|
||||
func TestSyslogRuleMatchDetailsExtractsResourceUID(t *testing.T) {
|
||||
rule := models.SyslogRule{
|
||||
Name: "H3C link down",
|
||||
Enabled: true,
|
||||
SourceMatch: "h3c-core",
|
||||
MessageRegex: `Interface (?P<iface>GigabitEthernet[0-9/]+) is down`,
|
||||
ResourceUIDExtractRegex: `Interface (?P<resource_uid>GigabitEthernet[0-9/]+) is down`,
|
||||
}
|
||||
|
||||
match := syslogRuleMatchDetails(&rule, "h3c-core-01", "Interface GigabitEthernet1/0/1 is down", "")
|
||||
if !match.Matched {
|
||||
t.Fatal("expected rule to match")
|
||||
}
|
||||
if match.ResourceUID != "network:GigabitEthernet1/0/1" {
|
||||
t.Fatalf("unexpected resource uid: %q", match.ResourceUID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildReplayRawEventPayloadMarksReplayed(t *testing.T) {
|
||||
ev := models.LogEvent{
|
||||
ID: 12,
|
||||
SourceKind: "syslog",
|
||||
SourceIP: "10.1.2.3",
|
||||
RemoteAddr: "10.1.2.3:514",
|
||||
DeviceName: "h3c-core-01",
|
||||
RawPayload: "<189>Jun 24 10:00:01 h3c-core-01 IFNET/4/LINK_DOWN: Interface GigabitEthernet1/0/1 is down",
|
||||
NormalizedSummary: "h3c-core-01: Interface GigabitEthernet1/0/1 is down",
|
||||
SeverityCode: "warning",
|
||||
DispatchStatus: "pending",
|
||||
}
|
||||
|
||||
body, err := BuildReplayRawEventPayload(ev)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildReplayRawEventPayload returned error: %v", err)
|
||||
}
|
||||
if body.SourceType != "syslog" {
|
||||
t.Fatalf("unexpected source type: %q", body.SourceType)
|
||||
}
|
||||
if body.ParseStatus != "replayed" {
|
||||
t.Fatalf("unexpected parse status: %q", body.ParseStatus)
|
||||
}
|
||||
if body.Labels["replay_of_log_event_id"] != "12" {
|
||||
t.Fatalf("missing replay label: %#v", body.Labels)
|
||||
}
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(body.RawPayload, &raw); err != nil {
|
||||
t.Fatalf("raw payload should be json: %v", err)
|
||||
}
|
||||
if raw["raw_packet"] == "" {
|
||||
t.Fatalf("raw packet missing: %#v", raw)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user