182 lines
6.1 KiB
Go
182 lines
6.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"ezops/model"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// dispatchCategoryAlert 按告警集合策略发送告警通知 // Logan.Li
|
|
func dispatchCategoryAlert(db *gorm.DB, category string, ruleName, hostname, metric, severity, message string, value, threshold float64) {
|
|
var policy model.AlertNotifyPolicy
|
|
if err := db.Where("category = ? AND enabled = ?", category, true).First(&policy).Error; err != nil {
|
|
fallbackNotifyChannels(db, ruleName, hostname, metric, severity, message, value, threshold)
|
|
return
|
|
}
|
|
title := buildNotifyTitle(db, ruleName, hostname, metric, severity)
|
|
content := buildAlertContent(ruleName, hostname, metric, severity, message, value, threshold)
|
|
sent, _ := dispatchToPolicyContacts(db, &policy, title, content, severity, false)
|
|
if sent == 0 {
|
|
fallbackNotifyChannels(db, ruleName, hostname, metric, severity, message, value, threshold)
|
|
}
|
|
}
|
|
|
|
// dispatchCategoryRecovery 按告警集合策略发送恢复通知 // Logan.Li
|
|
func dispatchCategoryRecovery(db *gorm.DB, category, ruleName, hostname, metric, message string) {
|
|
var policy model.AlertNotifyPolicy
|
|
if err := db.Where("category = ? AND enabled = ?", category, true).First(&policy).Error; err != nil {
|
|
fallbackNotifyRecovery(db, ruleName, hostname, metric, message)
|
|
return
|
|
}
|
|
title := fmt.Sprintf("%s — 已恢复正常", ruleName)
|
|
content := fmt.Sprintf("**告警已恢复**\\n\\n目标:%s\\n指标:%s\\n详情:%s\\n恢复时间:%s",
|
|
hostname, metric, message, time.Now().Format("2006-01-02 15:04:05"))
|
|
sent, _ := dispatchToPolicyContacts(db, &policy, title, content, "info", true)
|
|
if sent == 0 {
|
|
fallbackNotifyRecovery(db, ruleName, hostname, metric, message)
|
|
}
|
|
}
|
|
|
|
// resolvePolicyContactIDs 展开策略中的联系组与直接联系人 // Logan.Li
|
|
func resolvePolicyContactIDs(db *gorm.DB, policy *model.AlertNotifyPolicy) []uint {
|
|
idSet := make(map[uint]struct{})
|
|
|
|
var groupIDs []uint
|
|
if policy.ContactGroupIDs != "" && policy.ContactGroupIDs != "null" {
|
|
_ = json.Unmarshal([]byte(policy.ContactGroupIDs), &groupIDs)
|
|
}
|
|
for _, gid := range groupIDs {
|
|
var g model.AlertContactGroup
|
|
if db.First(&g, gid).Error != nil {
|
|
continue
|
|
}
|
|
var cids []uint
|
|
if g.ContactIDs != "" && g.ContactIDs != "null" {
|
|
_ = json.Unmarshal([]byte(g.ContactIDs), &cids)
|
|
}
|
|
for _, cid := range cids {
|
|
idSet[cid] = struct{}{}
|
|
}
|
|
}
|
|
|
|
var directIDs []uint
|
|
if policy.ContactIDs != "" && policy.ContactIDs != "null" {
|
|
_ = json.Unmarshal([]byte(policy.ContactIDs), &directIDs)
|
|
}
|
|
for _, cid := range directIDs {
|
|
idSet[cid] = struct{}{}
|
|
}
|
|
|
|
ids := make([]uint, 0, len(idSet))
|
|
for id := range idSet {
|
|
ids = append(ids, id)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// dispatchToPolicyContacts 向策略解析出的联系人发送通知 // Logan.Li
|
|
func dispatchToPolicyContacts(db *gorm.DB, policy *model.AlertNotifyPolicy, title, content, severity string, isRecovery bool) (sent, skipped int) {
|
|
if policy == nil || !policy.Enabled {
|
|
return 0, 1
|
|
}
|
|
contactIDs := resolvePolicyContactIDs(db, policy)
|
|
if len(contactIDs) == 0 {
|
|
return 0, 1
|
|
}
|
|
|
|
for _, cid := range contactIDs {
|
|
var contact model.AlertContact
|
|
if db.First(&contact, cid).Error != nil || contact.WebhookURL == "" {
|
|
skipped++
|
|
continue
|
|
}
|
|
var err error
|
|
switch contact.ChannelType {
|
|
case "feishu", "":
|
|
if isRecovery {
|
|
err = sendFeishuRecovery(db, feishuConfigFromURL(contact.WebhookURL), title, content)
|
|
} else {
|
|
err = sendFeishuAlert(db, feishuConfigFromURL(contact.WebhookURL), title, content, severity)
|
|
}
|
|
case "dingtalk", "wechat", "webhook":
|
|
err = sendGenericWebhook(contact.WebhookURL, title, content)
|
|
default:
|
|
err = sendGenericWebhook(contact.WebhookURL, title, content)
|
|
}
|
|
if err != nil {
|
|
fmt.Printf("[AlertNotify] 通知失败 (联系人:%s): %v\n", contact.Name, err)
|
|
skipped++
|
|
} else {
|
|
sent++
|
|
}
|
|
}
|
|
return sent, skipped
|
|
}
|
|
|
|
// feishuConfigFromURL 联系人 webhook 转飞书配置 JSON // Logan.Li
|
|
func feishuConfigFromURL(url string) string {
|
|
b, _ := json.Marshal(model.FeishuWebhookConfig{URL: url})
|
|
return string(b)
|
|
}
|
|
|
|
// sendGenericWebhook 钉钉/企微/通用 Webhook 文本通知 // Logan.Li
|
|
func sendGenericWebhook(webhookURL, title, content string) error {
|
|
body := map[string]interface{}{
|
|
"msgtype": "text",
|
|
"text": map[string]string{
|
|
"content": title + "\n" + strings.ReplaceAll(content, "\\n", "\n"),
|
|
},
|
|
}
|
|
raw, _ := json.Marshal(body)
|
|
req, err := http.NewRequest("POST", webhookURL, bytes.NewReader(raw))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
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 {
|
|
b, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(b))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// fallbackNotifyChannels 无策略时回退到全局 NotifyChannel // Logan.Li
|
|
func fallbackNotifyChannels(db *gorm.DB, ruleName, hostname, metric, severity, message string, value, threshold float64) {
|
|
var channels []model.NotifyChannel
|
|
db.Where("enabled = ? AND type = ?", true, "feishu").Find(&channels)
|
|
title := buildNotifyTitle(db, ruleName, hostname, metric, severity)
|
|
content := buildAlertContent(ruleName, hostname, metric, severity, message, value, threshold)
|
|
for _, ch := range channels {
|
|
if err := sendFeishuAlert(db, ch.Config, title, content, severity); err != nil {
|
|
fmt.Printf("[AlertNotify] 飞书回退通知失败 (渠道:%s): %v\n", ch.Name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func fallbackNotifyRecovery(db *gorm.DB, ruleName, hostname, metric, message string) {
|
|
var channels []model.NotifyChannel
|
|
db.Where("enabled = ? AND type = ?", true, "feishu").Find(&channels)
|
|
title := fmt.Sprintf("%s — 已恢复正常", ruleName)
|
|
content := fmt.Sprintf("**告警已恢复**\\n\\n主机:%s\\n指标:%s\\n详情:%s\\n恢复时间:%s",
|
|
hostname, metric, message, time.Now().Format("2006-01-02 15:04:05"))
|
|
for _, ch := range channels {
|
|
if err := sendFeishuRecovery(db, ch.Config, title, content); err != nil {
|
|
fmt.Printf("[AlertNotify] 飞书恢复回退失败 (渠道:%s): %v\n", ch.Name, err)
|
|
}
|
|
}
|
|
}
|