Merge pull request #160 from AkkiaS7/enhance-notification

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

Co-authored-by: AkkiaS7 <68485070+AkkiaS7@users.noreply.github.com>
This commit is contained in:
naiba 2022-04-16 09:35:59 +08:00 committed by GitHub
commit 61baa310e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 258 additions and 112 deletions

View File

@ -4,7 +4,7 @@
<br>
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
<br><br>
<img src="https://img.shields.io/github/workflow/status/naiba/nezha/Dashboard%20image?label=Dash%20v0.12.19&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/workflow/status/naiba/nezha/Agent%20release?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.8.2-brightgreen?style=for-the-badge&logo=linux">
<img src="https://img.shields.io/github/workflow/status/naiba/nezha/Dashboard%20image?label=Dash%20v0.12.20&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/workflow/status/naiba/nezha/Agent%20release?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.8.2-brightgreen?style=for-the-badge&logo=linux">
<br>
<br>
<p>:trollface: <b>哪吒监控</b> 一站式轻监控轻运维系统。支持系统状态、HTTP(SSL 证书变更、即将到期、到期)、TCP、Ping 监控报警,计划任务和在线终端。</p>

View File

@ -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,

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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:"-"`

View File

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

View File

@ -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();

View File

@ -32,6 +32,10 @@
<div class="menu"></div>
</div>
</div>
<div class="field">
<label>通知方式组</label>
<input type="text" name="NotificationTag" placeholder="default">
</div>
<div class="field">
<div class="ui push-successful checkbox">
<input name="PushSuccessful" type="checkbox" tabindex="0" class="hidden">

View File

@ -44,6 +44,10 @@
<div class="menu"></div>
</div>
</div>
<div class="field">
<label>通知方式组</label>
<input type="text" name="NotificationTag" placeholder="default" />
</div>
<div class="field">
<div class="ui nb-notify checkbox">
<input name="Notify" type="checkbox" tabindex="0" class="hidden" />

View File

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

View File

@ -12,6 +12,10 @@
<label>规则</label>
<textarea name="RulesRaw"></textarea>
</div>
<div class="field">
<label>通知方式组</label>
<input type="text" name="NotificationTag" placeholder="default">
</div>
<div class="field">
<div class="ui rule-enable checkbox">
<input name="Enable" type="checkbox" tabindex="0" class="hidden">

View File

@ -17,6 +17,7 @@
<th>名称</th>
<th>计划</th>
<th>命令</th>
<th>通知方式组</th>
<th>成功推送</th>
<th>覆盖范围</th>
<th>特定服务器</th>
@ -32,6 +33,7 @@
<td>{{$cron.Name}}</td>
<td>{{$cron.Scheduler}}</td>
<td>{{$cron.Command}}</td>
<td>{{$cron.NotificationTag}}</td>
<td>{{$cron.PushSuccessful}}</td>
<td>{{if eq $cron.Cover 0}}忽略所有{{else}}覆盖所有{{end}}</td>
<td>{{$cron.ServersRaw}}</td>

View File

@ -19,6 +19,7 @@
<th>特定服务器</th>
<th>类型</th>
<th>请求间隔</th>
<th>通知方式组</th>
<th>通知</th>
<th>管理</th>
</tr>
@ -36,6 +37,7 @@
2}} ICMP Ping {{else}} TCP 端口 {{end}}
</td>
<td>{{$monitor.Duration}}秒</td>
<td>{{$monitor.NotificationTag}}</td>
<td>{{$monitor.Notify}}</td>
<td>
<div class="ui mini icon buttons">

View File

@ -15,6 +15,7 @@
<tr>
<th>ID</th>
<th>名称</th>
<th>分组</th>
<th>URL</th>
<th>验证SSL</th>
<th>管理</th>
@ -25,6 +26,7 @@
<tr>
<td>{{$notification.ID}}</td>
<td>{{$notification.Name}}</td>
<td>{{$notification.Tag}}</td>
<td>{{$notification.URL}}</td>
<td>{{$notification.VerifySSL}}</td>
<td>
@ -55,6 +57,7 @@
<tr>
<th>ID</th>
<th>名称</th>
<th>通知方式组</th>
<th>规则</th>
<th>启用</th>
<th>管理</th>
@ -65,6 +68,7 @@
<tr>
<td>{{$rule.ID}}</td>
<td>{{$rule.Name}}</td>
<td>{{$rule.NotificationTag}}</td>
<td>{{$rule.RulesRaw}}</td>
<td>{{$rule.Enable}}</td>
<td>

View File

@ -52,6 +52,10 @@
<input type="text" name="IgnoredIPNotification" placeholder="服务器ID 以逗号隔开 1001,1002,1003"
value="{{.Conf.IgnoredIPNotification}}">
</div>
<div class="field">
<label>提醒发送至指定的通知分组</label>
<input type="text" name="IPChangeNotificationTag" placeholder="" value="{{.Conf.IPChangeNotificationTag}}">
</div>
<div class="field">
<div class="ui nf-ssl checkbox ip-change">
<input name="EnableIPChangeNotification" type="checkbox" tabindex="0" class="hidden">

View File

@ -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)
}

View File

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

View File

@ -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)
}
}
}

View File

@ -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(&notifications).Error; err != nil {
panic(err)
}
notificationsLock.Unlock()
for i := 0; i < len(notifications); i++ {
// 旧版本的Tag可能不存在 自动设置为默认值
if notifications[i].Tag == "" {
SetDefaultNotificationTagInDB(&notifications[i])
}
AddNotificationToList(&notifications[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, " 发送通知成功:")
}
}
}

View File

@ -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()
}

View File

@ -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)