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 "" }