diff --git a/README.md b/README.md index c2defc1..7281768 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@
LOGO designed by 熊大 .

-    +   

:trollface: 哪吒监控 一站式轻监控轻运维系统。支持系统状态、HTTP(SSL 证书变更、即将到期、到期)、TCP、Ping 监控报警,计划任务和在线终端。

diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go index 126f0d1..88b54e4 100644 --- a/cmd/dashboard/controller/member_api.go +++ b/cmd/dashboard/controller/member_api.go @@ -211,14 +211,15 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) { } type monitorForm struct { - ID uint64 - Name string - Target string - Type uint8 - Cover uint8 - Notify string - SkipServersRaw string - Duration uint64 + ID uint64 + Name string + Target string + Type uint8 + Cover uint8 + Notify string + NotificationTag string + SkipServersRaw string + Duration uint64 } func (ma *memberAPI) addOrEditMonitor(c *gin.Context) { @@ -233,10 +234,15 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) { m.SkipServersRaw = mf.SkipServersRaw m.Cover = mf.Cover m.Notify = mf.Notify == "on" + m.NotificationTag = mf.NotificationTag m.Duration = mf.Duration err = m.InitSkipServers() } if err == nil { + // 保证NotificationTag不为空 + if m.NotificationTag == "" { + m.NotificationTag = "default" + } if m.ID == 0 { err = singleton.DB.Create(&m).Error } else { @@ -259,13 +265,14 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) { } type cronForm struct { - ID uint64 - Name string - Scheduler string - Command string - ServersRaw string - Cover uint8 - PushSuccessful string + ID uint64 + Name string + Scheduler string + Command string + ServersRaw string + Cover uint8 + PushSuccessful string + NotificationTag string } func (ma *memberAPI) addOrEditCron(c *gin.Context) { @@ -278,12 +285,17 @@ func (ma *memberAPI) addOrEditCron(c *gin.Context) { cr.Command = cf.Command cr.ServersRaw = cf.ServersRaw cr.PushSuccessful = cf.PushSuccessful == "on" + cr.NotificationTag = cf.NotificationTag cr.ID = cf.ID cr.Cover = cf.Cover err = utils.Json.Unmarshal([]byte(cf.ServersRaw), &cr.Servers) } tx := singleton.DB.Begin() if err == nil { + // 保证NotificationTag不为空 + if cr.NotificationTag == "" { + cr.NotificationTag = "default" + } if cf.ID == 0 { err = tx.Create(&cr).Error } else { @@ -376,6 +388,7 @@ func (ma *memberAPI) forceUpdate(c *gin.Context) { type notificationForm struct { ID uint64 Name string + Tag string // 分组名 URL string RequestMethod int RequestType int @@ -390,6 +403,7 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) { err := c.ShouldBindJSON(&nf) if err == nil { n.Name = nf.Name + n.Tag = nf.Tag n.RequestMethod = nf.RequestMethod n.RequestType = nf.RequestType n.RequestHeader = nf.RequestHeader @@ -401,6 +415,10 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) { err = n.Send("这是测试消息") } if err == nil { + // 保证Tag不为空 + if n.Tag == "" { + n.Tag = "default" + } if n.ID == 0 { err = singleton.DB.Create(&n).Error } else { @@ -414,17 +432,18 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) { }) return } - singleton.OnRefreshOrAddNotification(n) + singleton.OnRefreshOrAddNotification(&n) c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, }) } type alertRuleForm struct { - ID uint64 - Name string - RulesRaw string - Enable string + ID uint64 + Name string + RulesRaw string + NotificationTag string + Enable string } func (ma *memberAPI) addOrEditAlertRule(c *gin.Context) { @@ -464,9 +483,14 @@ func (ma *memberAPI) addOrEditAlertRule(c *gin.Context) { if err == nil { r.Name = arf.Name r.RulesRaw = arf.RulesRaw + r.NotificationTag = arf.NotificationTag enable := arf.Enable == "on" r.Enable = &enable r.ID = arf.ID + //保证NotificationTag不为空 + if r.NotificationTag == "" { + r.NotificationTag = "default" + } if r.ID == 0 { err = singleton.DB.Create(&r).Error } else { @@ -517,14 +541,15 @@ func (ma *memberAPI) logout(c *gin.Context) { } type settingForm struct { - Title string - Admin string - Theme string - CustomCode string - ViewPassword string - IgnoredIPNotification string - GRPCHost string - Cover uint8 + Title string + Admin string + Theme string + CustomCode string + ViewPassword string + IgnoredIPNotification string + IPChangeNotificationTag string // IP变更提醒的通知组 + GRPCHost string + Cover uint8 EnableIPChangeNotification string EnablePlainIPInNotification string @@ -544,11 +569,16 @@ func (ma *memberAPI) updateSetting(c *gin.Context) { singleton.Conf.Cover = sf.Cover singleton.Conf.GRPCHost = sf.GRPCHost singleton.Conf.IgnoredIPNotification = sf.IgnoredIPNotification + singleton.Conf.IPChangeNotificationTag = sf.IPChangeNotificationTag singleton.Conf.Site.Brand = sf.Title singleton.Conf.Site.Theme = sf.Theme singleton.Conf.Site.CustomCode = sf.CustomCode singleton.Conf.Site.ViewPassword = sf.ViewPassword singleton.Conf.Oauth2.Admin = sf.Admin + // 保证NotificationTag不为空 + if singleton.Conf.IPChangeNotificationTag == "" { + singleton.Conf.IPChangeNotificationTag = "default" + } if err := singleton.Conf.Save(); err != nil { c.JSON(http.StatusOK, model.Response{ Code: http.StatusBadRequest, diff --git a/model/alertrule.go b/model/alertrule.go index 321480d..032e616 100644 --- a/model/alertrule.go +++ b/model/alertrule.go @@ -20,10 +20,11 @@ type CycleTransferStats struct { type AlertRule struct { Common - Name string - RulesRaw string - Enable *bool - Rules []Rule `gorm:"-" json:"-"` + Name string + RulesRaw string + Enable *bool + NotificationTag string // 该报警规则所在的通知组 + Rules []Rule `gorm:"-" json:"-"` } func (r *AlertRule) BeforeSave(tx *gorm.DB) error { diff --git a/model/config.go b/model/config.go index e7cf6b7..e509104 100644 --- a/model/config.go +++ b/model/config.go @@ -71,12 +71,13 @@ type Config struct { ProxyGRPCPort uint TLS bool - EnableIPChangeNotification bool - EnablePlainIPInNotification bool + EnablePlainIPInNotification bool // 通知信息IP不打码 // IP变更提醒 - Cover uint8 // 覆盖范围(0:提醒未被 IgnoredIPNotification 包含的所有服务器; 1:仅提醒被 IgnoredIPNotification 包含的服务器;) - IgnoredIPNotification string // 特定服务器IP(多个服务器用逗号分隔) + EnableIPChangeNotification bool + IPChangeNotificationTag string + Cover uint8 // 覆盖范围(0:提醒未被 IgnoredIPNotification 包含的所有服务器; 1:仅提醒被 IgnoredIPNotification 包含的服务器;) + IgnoredIPNotification string // 特定服务器IP(多个服务器用逗号分隔) v *viper.Viper IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内) @@ -102,6 +103,9 @@ func (c *Config) Read(path string) error { if c.GRPCPort == 0 { c.GRPCPort = 5555 } + if c.EnableIPChangeNotification && c.IPChangeNotificationTag == "" { + c.IPChangeNotificationTag = "default" + } c.updateIgnoredIPNotificationID() return nil diff --git a/model/cron.go b/model/cron.go index fbdf75a..a8e756c 100644 --- a/model/cron.go +++ b/model/cron.go @@ -15,14 +15,15 @@ const ( type Cron struct { Common - Name string - Scheduler string //分钟 小时 天 月 星期 - Command string - Servers []uint64 `gorm:"-"` - PushSuccessful bool // 推送成功的通知 - LastExecutedAt time.Time // 最后一次执行时间 - LastResult bool // 最后一次执行结果 - Cover uint8 // 计划任务覆盖范围 (0:仅覆盖特定服务器 1:仅忽略特定服务器) + Name string + Scheduler string //分钟 小时 天 月 星期 + Command string + Servers []uint64 `gorm:"-"` + PushSuccessful bool // 推送成功的通知 + NotificationTag string // 指定通知方式的分组 + LastExecutedAt time.Time // 最后一次执行时间 + LastResult bool // 最后一次执行结果 + Cover uint8 // 计划任务覆盖范围 (0:仅覆盖特定服务器 1:仅忽略特定服务器) CronJobID cron.EntryID `gorm:"-"` ServersRaw string diff --git a/model/monitor.go b/model/monitor.go index 309de37..0230eeb 100644 --- a/model/monitor.go +++ b/model/monitor.go @@ -38,13 +38,14 @@ const ( type Monitor struct { Common - Name string - Type uint8 - Target string - SkipServersRaw string - Duration uint64 - Notify bool - Cover uint8 + Name string + Type uint8 + Target string + SkipServersRaw string + Duration uint64 + Notify bool + NotificationTag string // 当前服务监控所属的通知组 + Cover uint8 SkipServers map[uint64]bool `gorm:"-" json:"-"` CronJobID cron.EntryID `gorm:"-" json:"-"` diff --git a/model/notification.go b/model/notification.go index 3ea2a9e..fcbfb56 100644 --- a/model/notification.go +++ b/model/notification.go @@ -28,6 +28,7 @@ const ( type Notification struct { Common Name string + Tag string // 分组名 URL string RequestMethod int RequestType int diff --git a/resource/static/main.js b/resource/static/main.js index c185373..55be2a3 100644 --- a/resource/static/main.js +++ b/resource/static/main.js @@ -114,6 +114,7 @@ function addOrEditAlertRule(rule) { modal.find("input[name=ID]").val(rule ? rule.ID : null); modal.find("input[name=Name]").val(rule ? rule.Name : null); modal.find("textarea[name=RulesRaw]").val(rule ? rule.RulesRaw : null); + modal.find("input[name=NotificationTag]").val(rule ? rule.NotificationTag : null); if (rule && rule.Enable) { modal.find(".ui.rule-enable.checkbox").checkbox("set checked"); } else { @@ -134,6 +135,7 @@ function addOrEditNotification(notification) { ); modal.find("input[name=ID]").val(notification ? notification.ID : null); modal.find("input[name=Name]").val(notification ? notification.Name : null); + modal.find("input[name=Tag]").val(notification ? notification.Tag : null); modal.find("input[name=URL]").val(notification ? notification.URL : null); modal .find("textarea[name=RequestHeader]") @@ -225,6 +227,7 @@ function addOrEditMonitor(monitor) { modal.find("input[name=Duration]").val(monitor && monitor.Duration ? monitor.Duration : 30); modal.find("select[name=Type]").val(monitor ? monitor.Type : 1); modal.find("select[name=Cover]").val(monitor ? monitor.Cover : 0); + modal.find("input[name=NotificationTag]").val(monitor ? monitor.NotificationTag : null); if (monitor && monitor.Notify) { modal.find(".ui.nb-notify.checkbox").checkbox("set checked"); } else { @@ -261,6 +264,7 @@ function addOrEditCron(cron) { ); modal.find("input[name=ID]").val(cron ? cron.ID : null); modal.find("input[name=Name]").val(cron ? cron.Name : null); + modal.find("input[name=NotificationTag]").val(cron ? cron.NotificationTag : null); modal.find("input[name=Scheduler]").val(cron ? cron.Scheduler : null); modal.find("a.ui.label.visible").each((i, el) => { el.remove(); diff --git a/resource/template/component/cron.html b/resource/template/component/cron.html index 3ee3da8..0b71952 100644 --- a/resource/template/component/cron.html +++ b/resource/template/component/cron.html @@ -32,6 +32,10 @@ +
+ + +
diff --git a/resource/template/component/monitor.html b/resource/template/component/monitor.html index 332cf69..98fba8a 100644 --- a/resource/template/component/monitor.html +++ b/resource/template/component/monitor.html @@ -44,6 +44,10 @@
+
+ + +
diff --git a/resource/template/component/notification.html b/resource/template/component/notification.html index 9ba5a09..b37d910 100644 --- a/resource/template/component/notification.html +++ b/resource/template/component/notification.html @@ -8,6 +8,10 @@
+
+ + +
diff --git a/resource/template/component/rule.html b/resource/template/component/rule.html index a9dd8ea..8448244 100644 --- a/resource/template/component/rule.html +++ b/resource/template/component/rule.html @@ -12,6 +12,10 @@
+
+ + +
diff --git a/resource/template/dashboard/cron.html b/resource/template/dashboard/cron.html index 92b862d..842d914 100644 --- a/resource/template/dashboard/cron.html +++ b/resource/template/dashboard/cron.html @@ -17,6 +17,7 @@ 名称 计划 命令 + 通知方式组 成功推送 覆盖范围 特定服务器 @@ -32,6 +33,7 @@ {{$cron.Name}} {{$cron.Scheduler}} {{$cron.Command}} + {{$cron.NotificationTag}} {{$cron.PushSuccessful}} {{if eq $cron.Cover 0}}忽略所有{{else}}覆盖所有{{end}} {{$cron.ServersRaw}} diff --git a/resource/template/dashboard/monitor.html b/resource/template/dashboard/monitor.html index bc69aaa..2feff3a 100644 --- a/resource/template/dashboard/monitor.html +++ b/resource/template/dashboard/monitor.html @@ -19,6 +19,7 @@ 特定服务器 类型 请求间隔 + 通知方式组 通知 管理 @@ -36,6 +37,7 @@ 2}} ICMP Ping {{else}} TCP 端口 {{end}} {{$monitor.Duration}}秒 + {{$monitor.NotificationTag}} {{$monitor.Notify}}
diff --git a/resource/template/dashboard/notification.html b/resource/template/dashboard/notification.html index 8ebcc84..d012b8c 100644 --- a/resource/template/dashboard/notification.html +++ b/resource/template/dashboard/notification.html @@ -15,6 +15,7 @@ ID 名称 + 分组 URL 验证SSL 管理 @@ -25,6 +26,7 @@ {{$notification.ID}} {{$notification.Name}} + {{$notification.Tag}} {{$notification.URL}} {{$notification.VerifySSL}} @@ -55,6 +57,7 @@ ID 名称 + 通知方式组 规则 启用 管理 @@ -65,6 +68,7 @@ {{$rule.ID}} {{$rule.Name}} + {{$rule.NotificationTag}} {{$rule.RulesRaw}} {{$rule.Enable}} diff --git a/resource/template/dashboard/setting.html b/resource/template/dashboard/setting.html index 601921b..eeb25b1 100644 --- a/resource/template/dashboard/setting.html +++ b/resource/template/dashboard/setting.html @@ -52,6 +52,10 @@
+
+ + +
diff --git a/service/rpc/nezha.go b/service/rpc/nezha.go index e48a854..2bca2c5 100644 --- a/service/rpc/nezha.go +++ b/service/rpc/nezha.go @@ -29,10 +29,10 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece singleton.ServerLock.RLock() defer singleton.ServerLock.RUnlock() 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() { - 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{ 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 != "" && 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。", singleton.ServerList[clientID].Name, singleton.IPDesensitize(singleton.ServerList[clientID].Host.IP), singleton.IPDesensitize(host.IP)), true) } diff --git a/service/singleton/alertsentinel.go b/service/singleton/alertsentinel.go index 8902700..a97397a 100644 --- a/service/singleton/alertsentinel.go +++ b/service/singleton/alertsentinel.go @@ -21,11 +21,13 @@ type NotificationHistory struct { } // 报警规则 -var AlertsLock sync.RWMutex -var Alerts []*model.AlertRule -var alertsStore map[uint64]map[uint64][][]interface{} // [alert_id][server_id] -> 对应报警规则的检查结果 -var alertsPrevState map[uint64]map[uint64]uint // [alert_id][server_id] -> 对应报警规则的上一次报警状态 -var AlertsCycleTransferStatsStore map[uint64]*model.CycleTransferStats // [alert_id] -> 对应报警规则的周期流量统计 +var ( + AlertsLock sync.RWMutex + Alerts []*model.AlertRule + alertsStore map[uint64]map[uint64][][]interface{} // [alert_id][server_id] -> 对应报警规则的检查结果 + alertsPrevState map[uint64]map[uint64]uint // [alert_id][server_id] -> 对应报警规则的上一次报警状态 + AlertsCycleTransferStatsStore map[uint64]*model.CycleTransferStats // [alert_id] -> 对应报警规则的周期流量统计 +) // addCycleTransferStatsInfo 向AlertsCycleTransferStatsStore中添加周期流量报警统计信息 func addCycleTransferStatsInfo(alert *model.AlertRule) { @@ -62,10 +64,15 @@ func AlertSentinelStart() { if err := DB.Find(&Alerts).Error; err != nil { panic(err) } - for i := 0; i < len(Alerts); i++ { - alertsStore[Alerts[i].ID] = make(map[uint64][][]interface{}) - alertsPrevState[Alerts[i].ID] = make(map[uint64]uint) - addCycleTransferStatsInfo(Alerts[i]) + for _, alert := range Alerts { + // 旧版本可能不存在通知组 为其添加默认值 + if alert.NotificationTag == "" { + alert.NotificationTag = "default" + DB.Save(alert) + } + alertsStore[alert.ID] = make(map[uint64][][]interface{}) + alertsPrevState[alert.ID] = make(map[uint64]uint) + addCycleTransferStatsInfo(alert) } AlertsLock.Unlock() @@ -143,11 +150,11 @@ func checkStatus() { if !passed { alertsPrevState[alert.ID][server.ID] = _RuleCheckFail message := fmt.Sprintf("[主机故障] %s(%s) 规则:%s", server.Name, IPDesensitize(server.Host.IP), alert.Name) - go SendNotification(message, true) + go SendNotification(alert.NotificationTag, message, true) } else { if alertsPrevState[alert.ID][server.ID] == _RuleCheckFail { message := fmt.Sprintf("[主机恢复] %s(%s) 规则:%s", server.Name, IPDesensitize(server.Host.IP), alert.Name) - go SendNotification(message, true) + go SendNotification(alert.NotificationTag, message, true) } alertsPrevState[alert.ID][server.ID] = _RuleCheckPass } diff --git a/service/singleton/crontask.go b/service/singleton/crontask.go index 647d068..54aa65e 100644 --- a/service/singleton/crontask.go +++ b/service/singleton/crontask.go @@ -13,7 +13,7 @@ import ( var ( Cron *cron.Cron - Crons map[uint64]*model.Cron + Crons map[uint64]*model.Cron // [CrondID] -> *model.Cron CronLock sync.RWMutex ) @@ -28,24 +28,32 @@ func LoadCronTasks() { var crons []model.Cron DB.Find(&crons) var err error - errMsg := new(bytes.Buffer) + var notificationTagList []string + notificationMsgMap := make(map[string]*bytes.Buffer) for i := 0; i < len(crons); i++ { - cr := crons[i] - + // 旧版本计划任务可能不存在通知组 为其添加默认通知组 + if crons[i].NotificationTag == "" { + crons[i].NotificationTag = "default" + DB.Save(crons[i]) + } // 注册计划任务 - cr.CronJobID, err = Cron.AddFunc(cr.Scheduler, CronTrigger(cr)) + crons[i].CronJobID, err = Cron.AddFunc(crons[i].Scheduler, CronTrigger(crons[i])) if err == nil { - Crons[cr.ID] = &cr + Crons[crons[i].ID] = &crons[i] } else { - if errMsg.Len() == 0 { - errMsg.WriteString("调度失败的计划任务:[") + // 当前通知组首次出现 将其加入通知组列表并初始化通知组消息缓存 + if _, ok := notificationMsgMap[crons[i].NotificationTag]; !ok { + notificationTagList = append(notificationTagList, crons[i].NotificationTag) + notificationMsgMap[crons[i].NotificationTag] = bytes.NewBufferString("") + notificationMsgMap[crons[i].NotificationTag].WriteString("调度失败的计划任务:[") } - errMsg.WriteString(fmt.Sprintf("%d,", cr.ID)) + notificationMsgMap[crons[i].NotificationTag].WriteString(fmt.Sprintf("%d,", crons[i].ID)) } } - if errMsg.Len() > 0 { - msg := errMsg.String() - SendNotification(msg[:len(msg)-1]+"] 这些任务将无法正常执行,请进入后点重新修改保存。", false) + // 向注册错误的计划任务所在通知组发送通知 + for _, tag := range notificationTagList { + notificationMsgMap[tag].WriteString("] 这些任务将无法正常执行,请进入后点重新修改保存。") + SendNotification(tag, notificationMsgMap[tag].String(), false) } Cron.Start() } @@ -76,7 +84,7 @@ func CronTrigger(cr model.Cron) func() { Type: model.TaskTypeCommand, }) } else { - SendNotification(fmt.Sprintf("[任务失败] %s,服务器 %s 离线,无法执行。", cr.Name, s.Name), false) + SendNotification(cr.NotificationTag, fmt.Sprintf("[任务失败] %s,服务器 %s 离线,无法执行。", cr.Name, s.Name), false) } } } diff --git a/service/singleton/notification.go b/service/singleton/notification.go index 476403f..48c0099 100644 --- a/service/singleton/notification.go +++ b/service/singleton/notification.go @@ -13,46 +13,97 @@ import ( const firstNotificationDelay = time.Minute * 15 // 通知方式 -var notifications []model.Notification -var notificationsLock sync.RWMutex +var ( + 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() { + InitNotification() notificationsLock.Lock() + defer notificationsLock.Unlock() + + var notifications []model.Notification if err := DB.Find(¬ifications).Error; err != nil { panic(err) } - notificationsLock.Unlock() + for i := 0; i < len(notifications); i++ { + // 旧版本的Tag可能不存在 自动设置为默认值 + if notifications[i].Tag == "" { + SetDefaultNotificationTagInDB(¬ifications[i]) + } + AddNotificationToList(¬ifications[i]) + } } -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() defer notificationsLock.Unlock() + var isEdit bool - for i := 0; i < len(notifications); i++ { - if notifications[i].ID == n.ID { - notifications[i] = n - isEdit = true - } + if _, ok := NotificationIDToTag[n.ID]; ok { + isEdit = true } if !isEdit { - notifications = append(notifications, n) + AddNotificationToList(n) + } else { + UpdateNotificationInList(n) } } +// AddNotificationToList 添加通知方式到map中 +func AddNotificationToList(n *model.Notification) { + // 当前 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) { + if n.Tag != NotificationIDToTag[n.ID] { + // 如果 Tag 不一致,则需要先移除原有的映射关系 + delete(NotificationList[NotificationIDToTag[n.ID]], n.ID) + delete(NotificationIDToTag, n.ID) + // 将新的 Tag 中的通知方式添加到 map 中 + AddNotificationToList(n) + } else { + // 如果 Tag 一致,则直接更新 + NotificationList[n.Tag][n.ID] = n + } +} + +// OnDeleteNotification 在map中删除通知方式 func OnDeleteNotification(id uint64) { notificationsLock.Lock() defer notificationsLock.Unlock() - for i := 0; i < len(notifications); i++ { - if notifications[i].ID == id { - notifications = append(notifications[:i], notifications[i+1:]...) - i-- - } - } + + delete(NotificationList[NotificationIDToTag[id]], id) + delete(NotificationIDToTag, id) } -func SendNotification(desc string, muteable bool) { - if muteable { +// SendNotification 向指定的通知方式组的所有通知方式发送通知 +func SendNotification(notificationTag string, desc string, mutable bool) { + if mutable { // 通知防骚扰策略 nID := hex.EncodeToString(md5.New().Sum([]byte(desc))) // #nosec var flag bool @@ -80,17 +131,22 @@ func SendNotification(desc string, muteable bool) { if !flag { if Conf.Debug { - log.Println("NEZHA>> 静音的重复通知:", desc, muteable) + log.Println("NEZHA>> 静音的重复通知:", desc, mutable) } return } } - // 发出通知 + // 向该通知方式组的所有通知方式发出通知 notificationsLock.RLock() defer notificationsLock.RUnlock() - for i := 0; i < len(notifications); i++ { - if err := notifications[i].Send(desc); err != nil { - log.Println("NEZHA>> 发送通知失败:", err) + for _, n := range NotificationList[notificationTag] { + log.Println("尝试通知", n.Name) + } + for _, n := range NotificationList[notificationTag] { + if err := n.Send(desc); err != nil { + log.Println("NEZHA>> 向 ", n.Name, " 发送通知失败:", err) + } else { + log.Println("NEZHA>> 向 ", n.Name, " 发送通知成功:") } } } diff --git a/service/singleton/servicesentinel.go b/service/singleton/servicesentinel.go index cb897d5..5049924 100644 --- a/service/singleton/servicesentinel.go +++ b/service/singleton/servicesentinel.go @@ -150,6 +150,11 @@ func (ss *ServiceSentinel) loadMonitorHistory() { ss.monitorsLock.Lock() defer ss.monitorsLock.Unlock() for i := 0; i < len(monitors); i++ { + // 旧版本可能不存在通知组 为其设置默认组 + if monitors[i].NotificationTag == "" { + monitors[i].NotificationTag = "default" + DB.Save(monitors[i]) + } task := *monitors[i] // 通过cron定时将服务监控任务传递给任务调度管道 monitors[i].CronJobID, err = Cron.AddFunc(task.CronSpec(), func() { @@ -356,7 +361,7 @@ func (ss *ServiceSentinel) worker() { isNeedSendNotification := (ss.lastStatus[mh.MonitorID] != "" || stateStr == "故障") && ss.monitors[mh.MonitorID].Notify ss.lastStatus[mh.MonitorID] = stateStr if isNeedSendNotification { - go SendNotification(fmt.Sprintf("[服务%s] %s", stateStr, ss.monitors[mh.MonitorID].Name), true) + go SendNotification(ss.monitors[mh.MonitorID].NotificationTag, fmt.Sprintf("[服务%s] %s", stateStr, ss.monitors[mh.MonitorID].Name), true) } ss.monitorsLock.RUnlock() } @@ -400,7 +405,7 @@ func (ss *ServiceSentinel) worker() { if errMsg != "" { ss.monitorsLock.RLock() if ss.monitors[mh.MonitorID].Notify { - go SendNotification(fmt.Sprintf("[SSL] %s %s", ss.monitors[mh.MonitorID].Name, errMsg), true) + go SendNotification(ss.monitors[mh.MonitorID].NotificationTag, fmt.Sprintf("[SSL] %s %s", ss.monitors[mh.MonitorID].Name, errMsg), true) } ss.monitorsLock.RUnlock() } diff --git a/service/singleton/singleton.go b/service/singleton/singleton.go index 341bd09..acd54d8 100644 --- a/service/singleton/singleton.go +++ b/service/singleton/singleton.go @@ -12,7 +12,7 @@ import ( "github.com/naiba/nezha/pkg/utils" ) -var Version = "v0.12.19" // !!记得修改 README 中的 badge 版本!! +var Version = "v0.12.20" // !!记得修改 README 中的 badge 版本!! var ( Conf *model.Config @@ -107,22 +107,22 @@ func CleanMonitorHistory() { var specialServerIDs []uint64 var alerts []model.AlertRule DB.Find(&alerts) - for i := 0; i < len(alerts); i++ { - for j := 0; j < len(alerts[i].Rules); j++ { + for _, alert := range alerts { + for _, rule := range alert.Rules { // 是不是流量记录规则 - if !alerts[i].Rules[j].IsTransferDurationRule() { + if !rule.IsTransferDurationRule() { continue } - dataCouldRemoveBefore := alerts[i].Rules[j].GetTransferDurationStart() + dataCouldRemoveBefore := rule.GetTransferDurationStart() // 判断规则影响的机器范围 - if alerts[i].Rules[j].Cover == model.RuleCoverAll { + if rule.Cover == model.RuleCoverAll { // 更新全局可以清理的数据点 if allServerKeep.IsZero() || allServerKeep.After(dataCouldRemoveBefore) { allServerKeep = dataCouldRemoveBefore } } else { // 更新特定机器可以清理数据点 - for id := range alerts[i].Rules[j].Ignore { + for id := range rule.Ignore { if specialServerKeep[id].IsZero() || specialServerKeep[id].After(dataCouldRemoveBefore) { specialServerKeep[id] = dataCouldRemoveBefore specialServerIDs = append(specialServerIDs, id)