Files
ai-ops/internal/service/notification_service.go
2026-05-12 17:48:22 +08:00

249 lines
6.6 KiB
Go

package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"time"
"github.com/company/ai-ops/internal/domain/model"
"github.com/company/ai-ops/internal/domain/repository"
)
// NotificationTask 是通知任务
type NotificationTask struct {
Event *model.AlertEvent
ChannelIDs []string
Priority string // P0, P1, P2, P3
}
// NotificationService 是通知服务
type NotificationService struct {
channelRepo repository.ChannelRepository
logRepo repository.NotificationLogRepository
client *http.Client
queue chan NotificationTask
stopCh chan struct{}
}
// NewNotificationService 创建通知服务
func NewNotificationService(cr repository.ChannelRepository, logRepos ...repository.NotificationLogRepository) *NotificationService {
var logRepo repository.NotificationLogRepository
if len(logRepos) > 0 {
logRepo = logRepos[0]
}
ns := &NotificationService{
channelRepo: cr,
logRepo: logRepo,
client: &http.Client{Timeout: 10 * time.Second},
queue: make(chan NotificationTask, 1000),
stopCh: make(chan struct{}),
}
go ns.worker()
return ns
}
// Stop 停止通知服务
func (s *NotificationService) Stop() {
close(s.stopCh)
}
// Enqueue 将通知任务入队列
func (s *NotificationService) Enqueue(event *model.AlertEvent, channelIDs []string) {
task := NotificationTask{
Event: event,
ChannelIDs: channelIDs,
Priority: event.Level,
}
select {
case s.queue <- task:
slog.Info("notification_enqueued", "event_id", event.ID, "priority", event.Level)
default:
slog.Warn("notification_queue_full", "event_id", event.ID)
}
}
func (s *NotificationService) worker() {
for {
select {
case task := <-s.queue:
s.processTask(context.Background(), task)
case <-s.stopCh:
return
}
}
}
func (s *NotificationService) processTask(ctx context.Context, task NotificationTask) {
// 根据优先级设置发送超时
timeout := 120 * time.Second
if task.Priority == "P0" || task.Priority == "P1" {
timeout = 30 * time.Second
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
channels, err := s.channelRepo.List(ctx)
if err != nil {
slog.Error("list_channels_failed", "error", err, "event_id", task.Event.ID)
return
}
// 按优先级排序渠道
ordered := s.filterAndOrderChannels(channels, task.ChannelIDs)
// 发送通知,失败时自动切换备用渠道
sent := false
for _, ch := range ordered {
logID := s.createSendLog(ctx, task.Event, ch)
if err := s.sendToChannel(ctx, task.Event, ch); err != nil {
s.markSendFailed(ctx, logID, 1, err)
slog.Error("notify_channel_failed",
"event_id", task.Event.ID,
"channel_id", ch.ID,
"channel_type", ch.ChannelType,
"error", err,
)
continue
}
s.markSendSent(ctx, logID)
sent = true
slog.Info("notify_sent",
"event_id", task.Event.ID,
"channel_id", ch.ID,
"channel_type", ch.ChannelType,
)
break
}
if !sent {
slog.Error("notify_all_channels_failed", "event_id", task.Event.ID)
}
}
func (s *NotificationService) createSendLog(ctx context.Context, event *model.AlertEvent, ch *model.NotificationChannel) string {
if s.logRepo == nil {
return ""
}
log := &model.NotificationLog{
EventID: event.ID,
ChannelID: ch.ID,
ChannelType: ch.ChannelType,
Status: "pending",
}
if err := s.logRepo.CreateLog(ctx, log); err != nil {
slog.Error("create_notification_log_failed", "event_id", event.ID, "channel_id", ch.ID, "error", err)
return ""
}
return log.ID
}
func (s *NotificationService) markSendSent(ctx context.Context, logID string) {
if s.logRepo == nil || logID == "" {
return
}
if err := s.logRepo.MarkSent(ctx, logID); err != nil {
slog.Error("mark_notification_sent_failed", "log_id", logID, "error", err)
}
}
func (s *NotificationService) markSendFailed(ctx context.Context, logID string, retryCount int, err error) {
if s.logRepo == nil || logID == "" {
return
}
if markErr := s.logRepo.MarkFailed(ctx, logID, retryCount, err.Error()); markErr != nil {
slog.Error("mark_notification_failed_failed", "log_id", logID, "error", markErr)
}
}
func (s *NotificationService) filterAndOrderChannels(all []model.NotificationChannel, ids []string) []*model.NotificationChannel {
idSet := make(map[string]bool)
for _, id := range ids {
idSet[id] = true
}
var filtered []*model.NotificationChannel
for i := range all {
if idSet[all[i].ID] {
filtered = append(filtered, &all[i])
}
}
// 按优先级排序(高优先级在前)
for i := 0; i < len(filtered)-1; i++ {
for j := i + 1; j < len(filtered); j++ {
if filtered[j].Priority > filtered[i].Priority {
filtered[i], filtered[j] = filtered[j], filtered[i]
}
}
}
return filtered
}
func (s *NotificationService) sendToChannel(ctx context.Context, event *model.AlertEvent, ch *model.NotificationChannel) error {
switch ch.ChannelType {
case "webhook":
return s.sendWebhook(ctx, event, ch)
case "email":
return s.sendEmail(ctx, event, ch)
case "feishu":
return s.sendFeishu(ctx, event, ch)
case "wechat":
return s.sendWechat(ctx, event, ch)
default:
return fmt.Errorf("unsupported channel type: %s", ch.ChannelType)
}
}
func (s *NotificationService) sendWebhook(ctx context.Context, event *model.AlertEvent, ch *model.NotificationChannel) error {
url, ok := ch.Config["webhook_url"].(string)
if !ok || url == "" {
return fmt.Errorf("webhook_url not configured")
}
payload := map[string]any{
"alert_id": event.ID,
"rule_id": event.RuleID,
"level": event.Level,
"status": event.Status,
"resource": event.ResourceID,
"value": event.CurrentValue,
"threshold": event.ThresholdValue,
"timestamp": time.Now().Format(time.RFC3339),
}
body, _ := json.Marshal(payload)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.client.Do(req)
if err != nil {
return fmt.Errorf("webhook request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("webhook returned status %d", resp.StatusCode)
}
return nil
}
func (s *NotificationService) sendEmail(ctx context.Context, event *model.AlertEvent, ch *model.NotificationChannel) error {
return fmt.Errorf("email channel not yet implemented")
}
func (s *NotificationService) sendFeishu(ctx context.Context, event *model.AlertEvent, ch *model.NotificationChannel) error {
return fmt.Errorf("feishu channel not yet implemented")
}
func (s *NotificationService) sendWechat(ctx context.Context, event *model.AlertEvent, ch *model.NotificationChannel) error {
return fmt.Errorf("wechat channel not yet implemented")
}