2026-06-26 12:51:50 +08:00
package ingest
import (
"encoding/json"
2026-07-21 02:32:18 +08:00
"errors"
2026-06-26 12:51:50 +08:00
"fmt"
2026-07-21 02:32:18 +08:00
"log"
"os"
2026-06-26 12:51:50 +08:00
"strings"
"time"
"git.apinb.com/ops/logs/internal/config"
"git.apinb.com/ops/logs/internal/impl"
"git.apinb.com/ops/logs/internal/models"
2026-07-21 02:32:18 +08:00
"gorm.io/gorm"
"gorm.io/gorm/clause"
2026-06-26 12:51:50 +08:00
)
const (
2026-07-21 02:32:18 +08:00
outboxStatusPending = "pending"
outboxStatusProcessing = "processing"
outboxStatusRetrying = "retrying"
outboxStatusSent = "sent"
outboxStatusDead = "dead"
2026-06-26 12:51:50 +08:00
)
2026-07-21 02:32:18 +08:00
func enqueueAlertWithDB ( db * gorm . DB , logEventID uint , body AlertReceiveBody ) ( uint , error ) {
2026-06-26 12:51:50 +08:00
payload , err := json . Marshal ( body )
if err != nil {
2026-07-21 02:32:18 +08:00
return 0 , err
2026-06-26 12:51:50 +08:00
}
2026-07-21 02:32:18 +08:00
return enqueuePayloadWithDB ( db , logEventID , string ( payload ) )
2026-06-26 12:51:50 +08:00
}
2026-07-21 02:32:18 +08:00
func enqueueRawEventWithDB ( db * gorm . DB , logEventID uint , body AlertReceiveBody , parseStatus string ) ( uint , error ) {
2026-06-26 12:51:50 +08:00
payload , err := json . Marshal ( buildRawEventIngestBody ( body , parseStatus ) )
if err != nil {
2026-07-21 02:32:18 +08:00
return 0 , err
2026-06-26 12:51:50 +08:00
}
2026-07-21 02:32:18 +08:00
return enqueuePayloadWithDB ( db , logEventID , string ( payload ) )
2026-06-26 12:51:50 +08:00
}
2026-07-21 02:32:18 +08:00
func enqueuePayloadWithDB ( db * gorm . DB , logEventID uint , payloadJSON string ) ( uint , error ) {
if db == nil {
return 0 , fmt . Errorf ( "database is not initialized" )
}
now , err := databaseNow ( db )
if err != nil {
return 0 , err
}
2026-06-26 12:51:50 +08:00
row := models . AlertOutbox {
LogEventID : logEventID ,
PayloadJSON : payloadJSON ,
Status : outboxStatusPending ,
RetryCount : 0 ,
2026-07-21 02:32:18 +08:00
NextRetryAt : now ,
2026-06-26 12:51:50 +08:00
LastError : "" ,
}
2026-07-21 02:32:18 +08:00
if err := db . Create ( & row ) . Error ; err != nil {
return 0 , err
}
return row . ID , nil
2026-06-26 12:51:50 +08:00
}
func StartAlertDispatcher ( ) {
2026-07-21 02:32:18 +08:00
owner := dispatcherOwner ( )
2026-06-26 12:51:50 +08:00
go func ( ) {
ticker := time . NewTicker ( 2 * time . Second )
defer ticker . Stop ( )
for range ticker . C {
2026-07-21 02:32:18 +08:00
if _ , err := ProcessAlertOutboxBatch ( impl . DBService , 20 , owner ) ; err != nil {
log . Printf ( "logs: alert outbox dispatch: %v" , err )
}
2026-06-26 12:51:50 +08:00
}
} ( )
}
2026-07-21 02:32:18 +08:00
func dispatcherOwner ( ) string {
host , _ := os . Hostname ( )
return fmt . Sprintf ( "%s:%d:%d" , host , os . Getpid ( ) , time . Now ( ) . UnixNano ( ) )
}
// ProcessAlertOutboxBatch 原子领取并处理一批任务,可安全用于多实例 worker。
func ProcessAlertOutboxBatch ( db * gorm . DB , limit int , owner string ) ( int , error ) {
if db == nil {
return 0 , fmt . Errorf ( "database is not initialized" )
}
2026-06-26 12:51:50 +08:00
if limit <= 0 {
limit = 20
}
2026-07-21 02:32:18 +08:00
owner = strings . TrimSpace ( owner )
if owner == "" {
return 0 , fmt . Errorf ( "outbox lease owner is required" )
2026-06-26 12:51:50 +08:00
}
2026-07-21 02:32:18 +08:00
processed := 0
for processed < limit {
rows , err := claimAlertOutboxBatch ( db , 1 , owner )
if err != nil {
return processed , err
}
if len ( rows ) == 0 {
break
}
if err := processOneOutbox ( db , rows [ 0 ] ) ; err != nil {
return processed , err
}
processed ++
2026-06-26 12:51:50 +08:00
}
2026-07-21 02:32:18 +08:00
return processed , nil
2026-06-26 12:51:50 +08:00
}
2026-07-21 02:32:18 +08:00
func claimAlertOutboxBatch ( db * gorm . DB , limit int , owner string ) ( [ ] models . AlertOutbox , error ) {
var rows [ ] models . AlertOutbox
now , err := databaseNow ( db )
if err != nil {
return nil , err
}
leaseUntil := now . Add ( 30 * time . Second )
err = db . Transaction ( func ( tx * gorm . DB ) error {
if err := tx . Clauses ( clause . Locking { Strength : "UPDATE" , Options : "SKIP LOCKED" } ) .
Where ( "(status IN ? AND next_retry_at <= ?) OR (status = ? AND lease_until IS NOT NULL AND lease_until <= ?)" , [ ] string { outboxStatusPending , outboxStatusRetrying } , now , outboxStatusProcessing , now ) .
Order ( "id asc" ) . Limit ( limit ) . Find ( & rows ) . Error ; err != nil {
return err
}
if len ( rows ) == 0 {
return nil
}
ids := make ( [ ] uint , 0 , len ( rows ) )
for i := range rows {
ids = append ( ids , rows [ i ] . ID )
}
if err := tx . Model ( & models . AlertOutbox { } ) . Where ( "id IN ?" , ids ) . Updates ( map [ string ] interface { } {
"status" : outboxStatusProcessing ,
"lease_until" : leaseUntil ,
"lease_owner" : owner ,
} ) . Error ; err != nil {
return err
}
for i := range rows {
rows [ i ] . Status = outboxStatusProcessing
rows [ i ] . LeaseUntil = & leaseUntil
rows [ i ] . LeaseOwner = owner
}
return nil
} )
return rows , err
}
func processOneOutbox ( db * gorm . DB , row models . AlertOutbox ) error {
2026-06-26 12:51:50 +08:00
var body AlertReceiveBody
if err := json . Unmarshal ( [ ] byte ( row . PayloadJSON ) , & body ) ; err != nil {
2026-07-21 02:32:18 +08:00
now , nowErr := databaseNow ( db )
if nowErr != nil {
return nowErr
}
return markOutboxDead ( db , row , row . RetryCount , "invalid_payload: " + err . Error ( ) , now )
2026-06-26 12:51:50 +08:00
}
2026-07-21 02:32:18 +08:00
forwardErr := forwardOutboxPayload ( row . PayloadJSON , body )
now , err := databaseNow ( db )
if err != nil {
return err
2026-06-26 12:51:50 +08:00
}
2026-07-21 02:32:18 +08:00
if forwardErr != nil {
if errors . Is ( forwardErr , errAlertForwardDisabled ) {
return markOutboxDead ( db , row , row . RetryCount , forwardErr . Error ( ) , now )
}
return markOutboxRetry ( db , row , forwardErr . Error ( ) , now )
}
return markOutboxSent ( db , row , now )
}
func databaseNow ( db * gorm . DB ) ( time . Time , error ) {
var now time . Time
if err := db . Raw ( "SELECT CURRENT_TIMESTAMP" ) . Scan ( & now ) . Error ; err != nil {
return time . Time { } , err
}
return now . UTC ( ) , nil
2026-06-26 12:51:50 +08:00
}
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 == "" {
2026-07-21 02:32:18 +08:00
return errAlertForwardDisabled
2026-06-26 12:51:50 +08:00
}
if len ( body . RawPayload ) == 0 {
return fmt . Errorf ( "raw_payload 不能为空" )
}
raw , err := json . Marshal ( body )
if err != nil {
return err
}
2026-07-21 02:32:18 +08:00
result , err := postAlertPayload ( cfg , "/Alert/v1/raw-events/ingest" , raw )
if err != nil {
return err
}
var details struct {
ID uint ` json:"id" `
RawEventID uint ` json:"raw_event_id" `
AlertRecordID uint ` json:"alert_record_id" `
IncidentID uint ` json:"incident_id" `
ParseStatus string ` json:"parse_status" `
Status string ` json:"status" `
Fingerprint string ` json:"fingerprint" `
}
if err := json . Unmarshal ( result . Details , & details ) ; err != nil {
return fmt . Errorf ( "Alert 原始事件响应 details 无效:%v; 请稍后重试" , err )
}
if body . ParseStatus == "unparsed" {
if details . ID == 0 || ( details . ParseStatus != "unparsed" && details . ParseStatus != "replayed" ) {
return fmt . Errorf ( "Alert 原始事件响应缺少持久化结果,无法确认转发成功;请稍后重试" )
}
return nil
}
rawEventID := details . RawEventID
if rawEventID == 0 {
rawEventID = details . ID
}
if rawEventID == 0 || details . AlertRecordID == 0 || details . IncidentID == 0 ||
( details . ID != 0 && details . RawEventID != 0 && details . ID != details . RawEventID ) ||
( details . Status != "firing" && details . Status != "resolved" ) || ! validFingerprint ( details . Fingerprint ) {
return fmt . Errorf ( "Alert 原始事件响应缺少完整处理结果,无法确认转发成功;请稍后重试" )
}
return nil
}
func markOutboxSent ( db * gorm . DB , row models . AlertOutbox , now time . Time ) error {
return db . Transaction ( func ( tx * gorm . DB ) error {
result := tx . Model ( & models . AlertOutbox { } ) .
Where ( "id = ? AND status = ? AND lease_owner = ?" , row . ID , outboxStatusProcessing , row . LeaseOwner ) .
Updates ( map [ string ] interface { } { "status" : outboxStatusSent , "last_error" : "" , "next_retry_at" : now , "lease_until" : nil , "lease_owner" : "" } )
if result . Error != nil {
return result . Error
}
if result . RowsAffected == 0 {
return nil
}
return tx . Model ( & models . LogEvent { } ) . Where ( "id = ? AND dispatch_outbox_id = ?" , row . LogEventID , row . ID ) . Updates ( map [ string ] interface { } {
"alert_sent" : true , "dispatch_status" : "sent" ,
} ) . Error
} )
2026-06-26 12:51:50 +08:00
}
2026-07-21 02:32:18 +08:00
func markOutboxRetry ( db * gorm . DB , row models . AlertOutbox , msg string , now time . Time ) error {
2026-06-26 12:51:50 +08:00
retry := row . RetryCount + 1
const maxRetry = 5
if retry > maxRetry {
2026-07-21 02:32:18 +08:00
return markOutboxDead ( db , row , retry , msg , now )
2026-06-26 12:51:50 +08:00
}
backoff := time . Duration ( retry * retry ) * time . Second
if backoff > 60 * time . Second {
backoff = 60 * time . Second
}
2026-07-21 02:32:18 +08:00
return updateClaimedOutbox ( db , row , map [ string ] interface { } {
"status" : outboxStatusRetrying , "retry_count" : retry , "next_retry_at" : now . Add ( backoff ) ,
"last_error" : truncateError ( msg , 1024 ) , "lease_until" : nil , "lease_owner" : "" ,
} , "retrying" )
2026-06-26 12:51:50 +08:00
}
2026-07-21 02:32:18 +08:00
func markOutboxDead ( db * gorm . DB , row models . AlertOutbox , retry int , msg string , now time . Time ) error {
return updateClaimedOutbox ( db , row , map [ string ] interface { } {
"status" : outboxStatusDead , "retry_count" : retry , "next_retry_at" : now ,
"last_error" : truncateError ( msg , 1024 ) , "lease_until" : nil , "lease_owner" : "" ,
} , "dead" )
}
func updateClaimedOutbox ( db * gorm . DB , row models . AlertOutbox , updates map [ string ] interface { } , eventStatus string ) error {
return db . Transaction ( func ( tx * gorm . DB ) error {
result := tx . Model ( & models . AlertOutbox { } ) .
Where ( "id = ? AND status = ? AND lease_owner = ?" , row . ID , outboxStatusProcessing , row . LeaseOwner ) .
Updates ( updates )
if result . Error != nil {
return result . Error
}
if result . RowsAffected == 0 {
return nil
}
return tx . Model ( & models . LogEvent { } ) . Where ( "id = ? AND dispatch_outbox_id = ?" , row . LogEventID , row . ID ) . Update ( "dispatch_status" , eventStatus ) . Error
} )
2026-06-26 12:51:50 +08:00
}
func truncateError ( s string , n int ) string {
s = strings . TrimSpace ( s )
2026-07-21 02:32:18 +08:00
if n <= 0 {
return ""
}
runes := [ ] rune ( s )
if len ( runes ) <= n {
2026-06-26 12:51:50 +08:00
return s
}
2026-07-21 02:32:18 +08:00
return string ( runes [ : n ] )
2026-06-26 12:51:50 +08:00
}