WIP: 通知方式分组 支持将不同的报警|监控|计划任务的通知 发送到指定的通知分组

This commit is contained in:
Akkia 2022-04-14 21:06:42 +08:00
parent 8ab62858f9
commit 9c1d495eb0
No known key found for this signature in database
GPG Key ID: DABE9A4AB2DD7EF3
11 changed files with 143 additions and 49 deletions

View File

@ -259,13 +259,14 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
} }
type cronForm struct { type cronForm struct {
ID uint64 ID uint64
Name string Name string
Scheduler string Scheduler string
Command string Command string
ServersRaw string ServersRaw string
Cover uint8 Cover uint8
PushSuccessful string PushSuccessful string
NotificationTag string
} }
func (ma *memberAPI) addOrEditCron(c *gin.Context) { func (ma *memberAPI) addOrEditCron(c *gin.Context) {
@ -278,6 +279,7 @@ func (ma *memberAPI) addOrEditCron(c *gin.Context) {
cr.Command = cf.Command cr.Command = cf.Command
cr.ServersRaw = cf.ServersRaw cr.ServersRaw = cf.ServersRaw
cr.PushSuccessful = cf.PushSuccessful == "on" cr.PushSuccessful = cf.PushSuccessful == "on"
cr.NotificationTag = cf.NotificationTag
cr.ID = cf.ID cr.ID = cf.ID
cr.Cover = cf.Cover cr.Cover = cf.Cover
err = utils.Json.Unmarshal([]byte(cf.ServersRaw), &cr.Servers) err = utils.Json.Unmarshal([]byte(cf.ServersRaw), &cr.Servers)
@ -376,6 +378,7 @@ func (ma *memberAPI) forceUpdate(c *gin.Context) {
type notificationForm struct { type notificationForm struct {
ID uint64 ID uint64
Name string Name string
Tag string // 分组名
URL string URL string
RequestMethod int RequestMethod int
RequestType int RequestType int
@ -390,6 +393,7 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
err := c.ShouldBindJSON(&nf) err := c.ShouldBindJSON(&nf)
if err == nil { if err == nil {
n.Name = nf.Name n.Name = nf.Name
n.Tag = nf.Tag
n.RequestMethod = nf.RequestMethod n.RequestMethod = nf.RequestMethod
n.RequestType = nf.RequestType n.RequestType = nf.RequestType
n.RequestHeader = nf.RequestHeader n.RequestHeader = nf.RequestHeader
@ -401,6 +405,10 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
err = n.Send("这是测试消息") err = n.Send("这是测试消息")
} }
if err == nil { if err == nil {
// 保证Tag不为空
if n.Tag == "" {
n.Tag = "default"
}
if n.ID == 0 { if n.ID == 0 {
err = singleton.DB.Create(&n).Error err = singleton.DB.Create(&n).Error
} else { } else {
@ -414,7 +422,7 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
}) })
return return
} }
singleton.OnRefreshOrAddNotification(n) singleton.OnRefreshOrAddNotification(&n)
c.JSON(http.StatusOK, model.Response{ c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK, Code: http.StatusOK,
}) })

View File

@ -72,6 +72,7 @@ type Config struct {
TLS bool TLS bool
EnableIPChangeNotification bool EnableIPChangeNotification bool
IPChangeNotificationTag string
EnablePlainIPInNotification bool EnablePlainIPInNotification bool
// IP变更提醒 // IP变更提醒
@ -102,6 +103,9 @@ func (c *Config) Read(path string) error {
if c.GRPCPort == 0 { if c.GRPCPort == 0 {
c.GRPCPort = 5555 c.GRPCPort = 5555
} }
if c.EnableIPChangeNotification && c.IPChangeNotificationTag == "" {
c.IPChangeNotificationTag = "default"
}
c.updateIgnoredIPNotificationID() c.updateIgnoredIPNotificationID()
return nil return nil

View File

@ -15,14 +15,15 @@ const (
type Cron struct { type Cron struct {
Common Common
Name string Name string
Scheduler string //分钟 小时 天 月 星期 Scheduler string //分钟 小时 天 月 星期
Command string Command string
Servers []uint64 `gorm:"-"` Servers []uint64 `gorm:"-"`
PushSuccessful bool // 推送成功的通知 PushSuccessful bool // 推送成功的通知
LastExecutedAt time.Time // 最后一次执行时间 NotificationTag string // 指定通知方式的分组
LastResult bool // 最后一次执行结果 LastExecutedAt time.Time // 最后一次执行时间
Cover uint8 // 计划任务覆盖范围 (0:仅覆盖特定服务器 1:仅忽略特定服务器) LastResult bool // 最后一次执行结果
Cover uint8 // 计划任务覆盖范围 (0:仅覆盖特定服务器 1:仅忽略特定服务器)
CronJobID cron.EntryID `gorm:"-"` CronJobID cron.EntryID `gorm:"-"`
ServersRaw string ServersRaw string

View File

@ -28,6 +28,7 @@ const (
type Notification struct { type Notification struct {
Common Common
Name string Name string
Tag string // 分组名
URL string URL string
RequestMethod int RequestMethod int
RequestType int RequestType int

View File

@ -38,6 +38,10 @@
<label>推送成功的消息</label> <label>推送成功的消息</label>
</div> </div>
</div> </div>
<div class="field">
<label>通知方式组</label>
<input type="text" name="NotificationTag" placeholder="default">
</div>
</form> </form>
<div class="ui warning message"> <div class="ui warning message">
<p> <p>

View File

@ -8,6 +8,10 @@
<label>名称</label> <label>名称</label>
<input type="text" name="Name"> <input type="text" name="Name">
</div> </div>
<div class="field">
<label>分组</label>
<input type="text" name="Tag">
</div>
<div class="field"> <div class="field">
<label>URL</label> <label>URL</label>
<input type="text" name="URL"> <input type="text" name="URL">

View File

@ -18,6 +18,7 @@
<th>计划</th> <th>计划</th>
<th>命令</th> <th>命令</th>
<th>成功推送</th> <th>成功推送</th>
<th>通知方式组</th>
<th>覆盖范围</th> <th>覆盖范围</th>
<th>特定服务器</th> <th>特定服务器</th>
<th>最后执行</th> <th>最后执行</th>
@ -33,6 +34,7 @@
<td>{{$cron.Scheduler}}</td> <td>{{$cron.Scheduler}}</td>
<td>{{$cron.Command}}</td> <td>{{$cron.Command}}</td>
<td>{{$cron.PushSuccessful}}</td> <td>{{$cron.PushSuccessful}}</td>
<td>{$cron.NotificationTag}</td>
<td>{{if eq $cron.Cover 0}}忽略所有{{else}}覆盖所有{{end}}</td> <td>{{if eq $cron.Cover 0}}忽略所有{{else}}覆盖所有{{end}}</td>
<td>{{$cron.ServersRaw}}</td> <td>{{$cron.ServersRaw}}</td>
<td>{{$cron.LastExecutedAt|tf}}</td> <td>{{$cron.LastExecutedAt|tf}}</td>

View File

@ -15,6 +15,7 @@
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>名称</th> <th>名称</th>
<th>分组</th>
<th>URL</th> <th>URL</th>
<th>验证SSL</th> <th>验证SSL</th>
<th>管理</th> <th>管理</th>
@ -25,6 +26,7 @@
<tr> <tr>
<td>{{$notification.ID}}</td> <td>{{$notification.ID}}</td>
<td>{{$notification.Name}}</td> <td>{{$notification.Name}}</td>
<td>{{$notification.Tag}}</td>
<td>{{$notification.URL}}</td> <td>{{$notification.URL}}</td>
<td>{{$notification.VerifySSL}}</td> <td>{{$notification.VerifySSL}}</td>
<td> <td>

View File

@ -29,10 +29,10 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece
singleton.ServerLock.RLock() singleton.ServerLock.RLock()
defer singleton.ServerLock.RUnlock() defer singleton.ServerLock.RUnlock()
if cr.PushSuccessful && r.GetSuccessful() { if cr.PushSuccessful && r.GetSuccessful() {
singleton.SendNotification(fmt.Sprintf("[任务成功] %s ,服务器:%s日志\n%s", cr.Name, singleton.ServerList[clientID].Name, r.GetData()), false) singleton.SendNotification(cr.NotificationTag, fmt.Sprintf("[任务成功] %s ,服务器:%s日志\n%s", cr.Name, singleton.ServerList[clientID].Name, r.GetData()), false)
} }
if !r.GetSuccessful() { if !r.GetSuccessful() {
singleton.SendNotification(fmt.Sprintf("[任务失败] %s ,服务器:%s日志\n%s", cr.Name, singleton.ServerList[clientID].Name, r.GetData()), false) singleton.SendNotification(cr.NotificationTag, fmt.Sprintf("[任务失败] %s ,服务器:%s日志\n%s", cr.Name, singleton.ServerList[clientID].Name, r.GetData()), false)
} }
singleton.DB.Model(cr).Updates(model.Cron{ singleton.DB.Model(cr).Updates(model.Cron{
LastExecutedAt: time.Now().Add(time.Second * -1 * time.Duration(r.GetDelay())), LastExecutedAt: time.Now().Add(time.Second * -1 * time.Duration(r.GetDelay())),
@ -103,7 +103,7 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
singleton.ServerList[clientID].Host.IP != "" && singleton.ServerList[clientID].Host.IP != "" &&
host.IP != "" && host.IP != "" &&
singleton.ServerList[clientID].Host.IP != host.IP { singleton.ServerList[clientID].Host.IP != host.IP {
singleton.SendNotification(fmt.Sprintf( singleton.SendNotification(singleton.Conf.IPChangeNotificationTag, fmt.Sprintf(
"[IP变更] %s 旧IP%s新IP%s。", "[IP变更] %s 旧IP%s新IP%s。",
singleton.ServerList[clientID].Name, singleton.IPDesensitize(singleton.ServerList[clientID].Host.IP), singleton.IPDesensitize(host.IP)), true) singleton.ServerList[clientID].Name, singleton.IPDesensitize(singleton.ServerList[clientID].Host.IP), singleton.IPDesensitize(host.IP)), true)
} }

View File

@ -11,7 +11,7 @@ import (
var ( var (
Cron *cron.Cron Cron *cron.Cron
Crons map[uint64]*model.Cron Crons map[uint64]*model.Cron // [CrondID] -> *model.Cron
CronLock sync.RWMutex CronLock sync.RWMutex
) )
@ -27,9 +27,12 @@ func LoadCronTasks() {
DB.Find(&crons) DB.Find(&crons)
var err error var err error
errMsg := new(bytes.Buffer) errMsg := new(bytes.Buffer)
for i := 0; i < len(crons); i++ { var notificationTagList []string
cr := crons[i] for _, cr := range crons {
// 旧版本计划任务可能不存在通知组 为其添加默认通知组
if cr.NotificationTag == "" {
AddDefaultCronNotificationTag(&cr)
}
// 注册计划任务 // 注册计划任务
cr.CronJobID, err = Cron.AddFunc(cr.Scheduler, CronTrigger(cr)) cr.CronJobID, err = Cron.AddFunc(cr.Scheduler, CronTrigger(cr))
if err == nil { if err == nil {
@ -39,15 +42,30 @@ func LoadCronTasks() {
errMsg.WriteString("调度失败的计划任务:[") errMsg.WriteString("调度失败的计划任务:[")
} }
errMsg.WriteString(fmt.Sprintf("%d,", cr.ID)) errMsg.WriteString(fmt.Sprintf("%d,", cr.ID))
notificationTagList = append(notificationTagList, cr.NotificationTag)
} }
} }
if errMsg.Len() > 0 { if errMsg.Len() > 0 {
msg := errMsg.String() msg := errMsg.String() + "] 这些任务将无法正常执行,请进入后点重新修改保存。"
SendNotification(msg[:len(msg)-1]+"] 这些任务将无法正常执行,请进入后点重新修改保存。", false) for _, tag := range notificationTagList {
// 向调度错误的计划任务所包含的所有通知组发送通知
SendNotification(tag, msg, false)
}
} }
Cron.Start() Cron.Start()
} }
// AddDefaultCronNotificationTag 添加默认的计划任务通知组
func AddDefaultCronNotificationTag(c *model.Cron) {
CronLock.Lock()
defer CronLock.Unlock()
if c.NotificationTag == "" {
c.NotificationTag = "default"
}
DB.Save(c)
}
func ManualTrigger(c model.Cron) { func ManualTrigger(c model.Cron) {
CronTrigger(c)() CronTrigger(c)()
} }
@ -74,7 +92,7 @@ func CronTrigger(cr model.Cron) func() {
Type: model.TaskTypeCommand, Type: model.TaskTypeCommand,
}) })
} else { } else {
SendNotification(fmt.Sprintf("[任务失败] %s服务器 %s 离线,无法执行。", cr.Name, s.Name), false) SendNotification(cr.NotificationTag, fmt.Sprintf("[任务失败] %s服务器 %s 离线,无法执行。", cr.Name, s.Name), false)
} }
} }
} }

View File

@ -13,46 +13,95 @@ import (
const firstNotificationDelay = time.Minute * 15 const firstNotificationDelay = time.Minute * 15
// 通知方式 // 通知方式
var notifications []model.Notification var (
var notificationsLock sync.RWMutex NotificationList map[string]map[uint64]*model.Notification // [NotificationMethodTag][NotificationID] -> model.Notification
NotificationIDToTag map[uint64]string // [NotificationID] -> NotificationTag
notificationsLock sync.RWMutex
)
// LoadNotifications 从 DB 加载通知方式到 singleton.notifications 变量 // InitNotification 初始化 Tag <-> ID <-> Notification 的映射
func InitNotification() {
NotificationList = make(map[string]map[uint64]*model.Notification)
NotificationIDToTag = make(map[uint64]string)
}
// LoadNotifications 从 DB 初始化通知方式相关参数
func LoadNotifications() { func LoadNotifications() {
InitNotification()
notificationsLock.Lock() notificationsLock.Lock()
defer notificationsLock.Unlock()
var notifications []model.Notification
if err := DB.Find(&notifications).Error; err != nil { if err := DB.Find(&notifications).Error; err != nil {
panic(err) panic(err)
} }
notificationsLock.Unlock() for _, n := range notifications {
// 旧版本的Tag可能不存在 自动设置为默认值
if n.Tag == "" {
SetDefaultNotificationTagInDB(&n)
}
AddNotificationToList(&n)
}
} }
func OnRefreshOrAddNotification(n model.Notification) { // SetDefaultNotificationTagInDB 设置默认通知方式的 Tag
func SetDefaultNotificationTagInDB(n *model.Notification) {
n.Tag = "default"
if err := DB.Save(n).Error; err != nil {
log.Println("[ERROR]", err)
}
}
// OnRefreshOrAddNotification 刷新通知方式相关参数
func OnRefreshOrAddNotification(n *model.Notification) {
notificationsLock.Lock() notificationsLock.Lock()
defer notificationsLock.Unlock() defer notificationsLock.Unlock()
var isEdit bool var isEdit bool
for i := 0; i < len(notifications); i++ { if _, ok := NotificationList[n.Tag][n.ID]; ok {
if notifications[i].ID == n.ID { isEdit = true
notifications[i] = n
isEdit = true
}
} }
if !isEdit { if !isEdit {
notifications = append(notifications, n) AddNotificationToList(n)
} else {
UpdateNotificationInList(n)
} }
} }
// AddNotificationToList 添加通知方式到map中
func AddNotificationToList(n *model.Notification) {
notificationsLock.Lock()
defer notificationsLock.Unlock()
// 当前 Tag 不存在,创建对应该 Tag 的 子 map 后再添加
if _, ok := NotificationList[n.Tag]; !ok {
NotificationList[n.Tag] = make(map[uint64]*model.Notification)
}
NotificationList[n.Tag][n.ID] = n
NotificationIDToTag[n.ID] = n.Tag
}
// UpdateNotificationInList 在 map 中更新通知方式
func UpdateNotificationInList(n *model.Notification) {
notificationsLock.Lock()
defer notificationsLock.Unlock()
NotificationList[n.Tag][n.ID] = n
}
// OnDeleteNotification 在map中删除通知方式
func OnDeleteNotification(id uint64) { func OnDeleteNotification(id uint64) {
notificationsLock.Lock() notificationsLock.Lock()
defer notificationsLock.Unlock() defer notificationsLock.Unlock()
for i := 0; i < len(notifications); i++ {
if notifications[i].ID == id { delete(NotificationList[NotificationIDToTag[id]], id)
notifications = append(notifications[:i], notifications[i+1:]...) delete(NotificationIDToTag, id)
i--
}
}
} }
func SendNotification(desc string, muteable bool) { // SendNotification 向指定的通知方式组的所有通知方式发送通知
if muteable { func SendNotification(notificationTag string, desc string, mutable bool) {
if mutable {
// 通知防骚扰策略 // 通知防骚扰策略
nID := hex.EncodeToString(md5.New().Sum([]byte(desc))) // #nosec nID := hex.EncodeToString(md5.New().Sum([]byte(desc))) // #nosec
var flag bool var flag bool
@ -80,16 +129,17 @@ func SendNotification(desc string, muteable bool) {
if !flag { if !flag {
if Conf.Debug { if Conf.Debug {
log.Println("NEZHA>> 静音的重复通知:", desc, muteable) log.Println("NEZHA>> 静音的重复通知:", desc, mutable)
} }
return return
} }
} }
// 发出通知 // 向该通知方式组的所有通知方式发出通知
notificationsLock.RLock() notificationsLock.RLock()
defer notificationsLock.RUnlock() defer notificationsLock.RUnlock()
for i := 0; i < len(notifications); i++ {
if err := notifications[i].Send(desc); err != nil { for _, n := range NotificationList[notificationTag] {
if err := n.Send(desc); err != nil {
log.Println("NEZHA>> 发送通知失败:", err) log.Println("NEZHA>> 发送通知失败:", err)
} }
} }