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> <br>
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small> <small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
<br><br> <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>
<br> <br>
<p>:trollface: <b>哪吒监控</b> 一站式轻监控轻运维系统。支持系统状态、HTTP(SSL 证书变更、即将到期、到期)、TCP、Ping 监控报警,计划任务和在线终端。</p> <p>:trollface: <b>哪吒监控</b> 一站式轻监控轻运维系统。支持系统状态、HTTP(SSL 证书变更、即将到期、到期)、TCP、Ping 监控报警,计划任务和在线终端。</p>

View File

@ -217,6 +217,7 @@ type monitorForm struct {
Type uint8 Type uint8
Cover uint8 Cover uint8
Notify string Notify string
NotificationTag string
SkipServersRaw string SkipServersRaw string
Duration uint64 Duration uint64
} }
@ -233,10 +234,15 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
m.SkipServersRaw = mf.SkipServersRaw m.SkipServersRaw = mf.SkipServersRaw
m.Cover = mf.Cover m.Cover = mf.Cover
m.Notify = mf.Notify == "on" m.Notify = mf.Notify == "on"
m.NotificationTag = mf.NotificationTag
m.Duration = mf.Duration m.Duration = mf.Duration
err = m.InitSkipServers() err = m.InitSkipServers()
} }
if err == nil { if err == nil {
// 保证NotificationTag不为空
if m.NotificationTag == "" {
m.NotificationTag = "default"
}
if m.ID == 0 { if m.ID == 0 {
err = singleton.DB.Create(&m).Error err = singleton.DB.Create(&m).Error
} else { } else {
@ -266,6 +272,7 @@ type cronForm struct {
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,12 +285,17 @@ 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)
} }
tx := singleton.DB.Begin() tx := singleton.DB.Begin()
if err == nil { if err == nil {
// 保证NotificationTag不为空
if cr.NotificationTag == "" {
cr.NotificationTag = "default"
}
if cf.ID == 0 { if cf.ID == 0 {
err = tx.Create(&cr).Error err = tx.Create(&cr).Error
} else { } else {
@ -376,6 +388,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 +403,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 +415,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 +432,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,
}) })
@ -424,6 +442,7 @@ type alertRuleForm struct {
ID uint64 ID uint64
Name string Name string
RulesRaw string RulesRaw string
NotificationTag string
Enable string Enable string
} }
@ -464,9 +483,14 @@ func (ma *memberAPI) addOrEditAlertRule(c *gin.Context) {
if err == nil { if err == nil {
r.Name = arf.Name r.Name = arf.Name
r.RulesRaw = arf.RulesRaw r.RulesRaw = arf.RulesRaw
r.NotificationTag = arf.NotificationTag
enable := arf.Enable == "on" enable := arf.Enable == "on"
r.Enable = &enable r.Enable = &enable
r.ID = arf.ID r.ID = arf.ID
//保证NotificationTag不为空
if r.NotificationTag == "" {
r.NotificationTag = "default"
}
if r.ID == 0 { if r.ID == 0 {
err = singleton.DB.Create(&r).Error err = singleton.DB.Create(&r).Error
} else { } else {
@ -523,6 +547,7 @@ type settingForm struct {
CustomCode string CustomCode string
ViewPassword string ViewPassword string
IgnoredIPNotification string IgnoredIPNotification string
IPChangeNotificationTag string // IP变更提醒的通知组
GRPCHost string GRPCHost string
Cover uint8 Cover uint8
@ -544,11 +569,16 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
singleton.Conf.Cover = sf.Cover singleton.Conf.Cover = sf.Cover
singleton.Conf.GRPCHost = sf.GRPCHost singleton.Conf.GRPCHost = sf.GRPCHost
singleton.Conf.IgnoredIPNotification = sf.IgnoredIPNotification singleton.Conf.IgnoredIPNotification = sf.IgnoredIPNotification
singleton.Conf.IPChangeNotificationTag = sf.IPChangeNotificationTag
singleton.Conf.Site.Brand = sf.Title singleton.Conf.Site.Brand = sf.Title
singleton.Conf.Site.Theme = sf.Theme singleton.Conf.Site.Theme = sf.Theme
singleton.Conf.Site.CustomCode = sf.CustomCode singleton.Conf.Site.CustomCode = sf.CustomCode
singleton.Conf.Site.ViewPassword = sf.ViewPassword singleton.Conf.Site.ViewPassword = sf.ViewPassword
singleton.Conf.Oauth2.Admin = sf.Admin singleton.Conf.Oauth2.Admin = sf.Admin
// 保证NotificationTag不为空
if singleton.Conf.IPChangeNotificationTag == "" {
singleton.Conf.IPChangeNotificationTag = "default"
}
if err := singleton.Conf.Save(); err != nil { if err := singleton.Conf.Save(); err != nil {
c.JSON(http.StatusOK, model.Response{ c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,

View File

@ -23,6 +23,7 @@ type AlertRule struct {
Name string Name string
RulesRaw string RulesRaw string
Enable *bool Enable *bool
NotificationTag string // 该报警规则所在的通知组
Rules []Rule `gorm:"-" json:"-"` Rules []Rule `gorm:"-" json:"-"`
} }

View File

@ -71,10 +71,11 @@ type Config struct {
ProxyGRPCPort uint ProxyGRPCPort uint
TLS bool TLS bool
EnableIPChangeNotification bool EnablePlainIPInNotification bool // 通知信息IP不打码
EnablePlainIPInNotification bool
// IP变更提醒 // IP变更提醒
EnableIPChangeNotification bool
IPChangeNotificationTag string
Cover uint8 // 覆盖范围0:提醒未被 IgnoredIPNotification 包含的所有服务器; 1:仅提醒被 IgnoredIPNotification 包含的服务器; Cover uint8 // 覆盖范围0:提醒未被 IgnoredIPNotification 包含的所有服务器; 1:仅提醒被 IgnoredIPNotification 包含的服务器;
IgnoredIPNotification string // 特定服务器IP多个服务器用逗号分隔 IgnoredIPNotification string // 特定服务器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

@ -20,6 +20,7 @@ type Cron struct {
Command string Command string
Servers []uint64 `gorm:"-"` Servers []uint64 `gorm:"-"`
PushSuccessful bool // 推送成功的通知 PushSuccessful bool // 推送成功的通知
NotificationTag string // 指定通知方式的分组
LastExecutedAt time.Time // 最后一次执行时间 LastExecutedAt time.Time // 最后一次执行时间
LastResult bool // 最后一次执行结果 LastResult bool // 最后一次执行结果
Cover uint8 // 计划任务覆盖范围 (0:仅覆盖特定服务器 1:仅忽略特定服务器) Cover uint8 // 计划任务覆盖范围 (0:仅覆盖特定服务器 1:仅忽略特定服务器)

View File

@ -44,6 +44,7 @@ type Monitor struct {
SkipServersRaw string SkipServersRaw string
Duration uint64 Duration uint64
Notify bool Notify bool
NotificationTag string // 当前服务监控所属的通知组
Cover uint8 Cover uint8
SkipServers map[uint64]bool `gorm:"-" json:"-"` SkipServers map[uint64]bool `gorm:"-" json:"-"`

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

@ -114,6 +114,7 @@ function addOrEditAlertRule(rule) {
modal.find("input[name=ID]").val(rule ? rule.ID : null); modal.find("input[name=ID]").val(rule ? rule.ID : null);
modal.find("input[name=Name]").val(rule ? rule.Name : null); modal.find("input[name=Name]").val(rule ? rule.Name : null);
modal.find("textarea[name=RulesRaw]").val(rule ? rule.RulesRaw : 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) { if (rule && rule.Enable) {
modal.find(".ui.rule-enable.checkbox").checkbox("set checked"); modal.find(".ui.rule-enable.checkbox").checkbox("set checked");
} else { } else {
@ -134,6 +135,7 @@ function addOrEditNotification(notification) {
); );
modal.find("input[name=ID]").val(notification ? notification.ID : null); modal.find("input[name=ID]").val(notification ? notification.ID : null);
modal.find("input[name=Name]").val(notification ? notification.Name : 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("input[name=URL]").val(notification ? notification.URL : null);
modal modal
.find("textarea[name=RequestHeader]") .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("input[name=Duration]").val(monitor && monitor.Duration ? monitor.Duration : 30);
modal.find("select[name=Type]").val(monitor ? monitor.Type : 1); modal.find("select[name=Type]").val(monitor ? monitor.Type : 1);
modal.find("select[name=Cover]").val(monitor ? monitor.Cover : 0); modal.find("select[name=Cover]").val(monitor ? monitor.Cover : 0);
modal.find("input[name=NotificationTag]").val(monitor ? monitor.NotificationTag : null);
if (monitor && monitor.Notify) { if (monitor && monitor.Notify) {
modal.find(".ui.nb-notify.checkbox").checkbox("set checked"); modal.find(".ui.nb-notify.checkbox").checkbox("set checked");
} else { } else {
@ -261,6 +264,7 @@ function addOrEditCron(cron) {
); );
modal.find("input[name=ID]").val(cron ? cron.ID : null); modal.find("input[name=ID]").val(cron ? cron.ID : null);
modal.find("input[name=Name]").val(cron ? cron.Name : 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("input[name=Scheduler]").val(cron ? cron.Scheduler : null);
modal.find("a.ui.label.visible").each((i, el) => { modal.find("a.ui.label.visible").each((i, el) => {
el.remove(); el.remove();

View File

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

View File

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

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" placeholder="default">
</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

@ -12,6 +12,10 @@
<label>规则</label> <label>规则</label>
<textarea name="RulesRaw"></textarea> <textarea name="RulesRaw"></textarea>
</div> </div>
<div class="field">
<label>通知方式组</label>
<input type="text" name="NotificationTag" placeholder="default">
</div>
<div class="field"> <div class="field">
<div class="ui rule-enable checkbox"> <div class="ui rule-enable checkbox">
<input name="Enable" type="checkbox" tabindex="0" class="hidden"> <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>
<th>成功推送</th> <th>成功推送</th>
<th>覆盖范围</th> <th>覆盖范围</th>
<th>特定服务器</th> <th>特定服务器</th>
@ -32,6 +33,7 @@
<td>{{$cron.Name}}</td> <td>{{$cron.Name}}</td>
<td>{{$cron.Scheduler}}</td> <td>{{$cron.Scheduler}}</td>
<td>{{$cron.Command}}</td> <td>{{$cron.Command}}</td>
<td>{{$cron.NotificationTag}}</td>
<td>{{$cron.PushSuccessful}}</td> <td>{{$cron.PushSuccessful}}</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>

View File

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

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>
@ -55,6 +57,7 @@
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>名称</th> <th>名称</th>
<th>通知方式组</th>
<th>规则</th> <th>规则</th>
<th>启用</th> <th>启用</th>
<th>管理</th> <th>管理</th>
@ -65,6 +68,7 @@
<tr> <tr>
<td>{{$rule.ID}}</td> <td>{{$rule.ID}}</td>
<td>{{$rule.Name}}</td> <td>{{$rule.Name}}</td>
<td>{{$rule.NotificationTag}}</td>
<td>{{$rule.RulesRaw}}</td> <td>{{$rule.RulesRaw}}</td>
<td>{{$rule.Enable}}</td> <td>{{$rule.Enable}}</td>
<td> <td>

View File

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

@ -21,11 +21,13 @@ type NotificationHistory struct {
} }
// 报警规则 // 报警规则
var AlertsLock sync.RWMutex var (
var Alerts []*model.AlertRule AlertsLock sync.RWMutex
var alertsStore map[uint64]map[uint64][][]interface{} // [alert_id][server_id] -> 对应报警规则的检查结果 Alerts []*model.AlertRule
var alertsPrevState map[uint64]map[uint64]uint // [alert_id][server_id] -> 对应报警规则的上一次报警状态 alertsStore map[uint64]map[uint64][][]interface{} // [alert_id][server_id] -> 对应报警规则的检查结果
var AlertsCycleTransferStatsStore map[uint64]*model.CycleTransferStats // [alert_id] -> 对应报警规则的周期流量统计 alertsPrevState map[uint64]map[uint64]uint // [alert_id][server_id] -> 对应报警规则的上一次报警状态
AlertsCycleTransferStatsStore map[uint64]*model.CycleTransferStats // [alert_id] -> 对应报警规则的周期流量统计
)
// addCycleTransferStatsInfo 向AlertsCycleTransferStatsStore中添加周期流量报警统计信息 // addCycleTransferStatsInfo 向AlertsCycleTransferStatsStore中添加周期流量报警统计信息
func addCycleTransferStatsInfo(alert *model.AlertRule) { func addCycleTransferStatsInfo(alert *model.AlertRule) {
@ -62,10 +64,15 @@ func AlertSentinelStart() {
if err := DB.Find(&Alerts).Error; err != nil { if err := DB.Find(&Alerts).Error; err != nil {
panic(err) panic(err)
} }
for i := 0; i < len(Alerts); i++ { for _, alert := range Alerts {
alertsStore[Alerts[i].ID] = make(map[uint64][][]interface{}) // 旧版本可能不存在通知组 为其添加默认值
alertsPrevState[Alerts[i].ID] = make(map[uint64]uint) if alert.NotificationTag == "" {
addCycleTransferStatsInfo(Alerts[i]) alert.NotificationTag = "default"
DB.Save(alert)
}
alertsStore[alert.ID] = make(map[uint64][][]interface{})
alertsPrevState[alert.ID] = make(map[uint64]uint)
addCycleTransferStatsInfo(alert)
} }
AlertsLock.Unlock() AlertsLock.Unlock()
@ -143,11 +150,11 @@ func checkStatus() {
if !passed { if !passed {
alertsPrevState[alert.ID][server.ID] = _RuleCheckFail alertsPrevState[alert.ID][server.ID] = _RuleCheckFail
message := fmt.Sprintf("[主机故障] %s(%s) 规则:%s", server.Name, IPDesensitize(server.Host.IP), alert.Name) 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 { } else {
if alertsPrevState[alert.ID][server.ID] == _RuleCheckFail { if alertsPrevState[alert.ID][server.ID] == _RuleCheckFail {
message := fmt.Sprintf("[主机恢复] %s(%s) 规则:%s", server.Name, IPDesensitize(server.Host.IP), alert.Name) 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 alertsPrevState[alert.ID][server.ID] = _RuleCheckPass
} }

View File

@ -13,7 +13,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
) )
@ -28,24 +28,32 @@ func LoadCronTasks() {
var crons []model.Cron var crons []model.Cron
DB.Find(&crons) DB.Find(&crons)
var err error var err error
errMsg := new(bytes.Buffer) var notificationTagList []string
notificationMsgMap := make(map[string]*bytes.Buffer)
for i := 0; i < len(crons); i++ { 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 { if err == nil {
Crons[cr.ID] = &cr Crons[crons[i].ID] = &crons[i]
} else { } 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() for _, tag := range notificationTagList {
SendNotification(msg[:len(msg)-1]+"] 这些任务将无法正常执行,请进入后点重新修改保存。", false) notificationMsgMap[tag].WriteString("] 这些任务将无法正常执行,请进入后点重新修改保存。")
SendNotification(tag, notificationMsgMap[tag].String(), false)
} }
Cron.Start() Cron.Start()
} }
@ -76,7 +84,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,97 @@ 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 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() notificationsLock.Lock()
defer notificationsLock.Unlock() defer notificationsLock.Unlock()
var isEdit bool var isEdit bool
for i := 0; i < len(notifications); i++ { if _, ok := NotificationIDToTag[n.ID]; ok {
if notifications[i].ID == n.ID {
notifications[i] = n
isEdit = true isEdit = true
} }
}
if !isEdit { 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) { 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,17 +131,22 @@ 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++ { for _, n := range NotificationList[notificationTag] {
if err := notifications[i].Send(desc); err != nil { log.Println("尝试通知", n.Name)
log.Println("NEZHA>> 发送通知失败:", err) }
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() ss.monitorsLock.Lock()
defer ss.monitorsLock.Unlock() defer ss.monitorsLock.Unlock()
for i := 0; i < len(monitors); i++ { for i := 0; i < len(monitors); i++ {
// 旧版本可能不存在通知组 为其设置默认组
if monitors[i].NotificationTag == "" {
monitors[i].NotificationTag = "default"
DB.Save(monitors[i])
}
task := *monitors[i] task := *monitors[i]
// 通过cron定时将服务监控任务传递给任务调度管道 // 通过cron定时将服务监控任务传递给任务调度管道
monitors[i].CronJobID, err = Cron.AddFunc(task.CronSpec(), func() { 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 isNeedSendNotification := (ss.lastStatus[mh.MonitorID] != "" || stateStr == "故障") && ss.monitors[mh.MonitorID].Notify
ss.lastStatus[mh.MonitorID] = stateStr ss.lastStatus[mh.MonitorID] = stateStr
if isNeedSendNotification { 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() ss.monitorsLock.RUnlock()
} }
@ -400,7 +405,7 @@ func (ss *ServiceSentinel) worker() {
if errMsg != "" { if errMsg != "" {
ss.monitorsLock.RLock() ss.monitorsLock.RLock()
if ss.monitors[mh.MonitorID].Notify { 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() ss.monitorsLock.RUnlock()
} }

View File

@ -12,7 +12,7 @@ import (
"github.com/naiba/nezha/pkg/utils" "github.com/naiba/nezha/pkg/utils"
) )
var Version = "v0.12.19" // !!记得修改 README 中的 badge 版本!! var Version = "v0.12.20" // !!记得修改 README 中的 badge 版本!!
var ( var (
Conf *model.Config Conf *model.Config
@ -107,22 +107,22 @@ func CleanMonitorHistory() {
var specialServerIDs []uint64 var specialServerIDs []uint64
var alerts []model.AlertRule var alerts []model.AlertRule
DB.Find(&alerts) DB.Find(&alerts)
for i := 0; i < len(alerts); i++ { for _, alert := range alerts {
for j := 0; j < len(alerts[i].Rules); j++ { for _, rule := range alert.Rules {
// 是不是流量记录规则 // 是不是流量记录规则
if !alerts[i].Rules[j].IsTransferDurationRule() { if !rule.IsTransferDurationRule() {
continue 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) { if allServerKeep.IsZero() || allServerKeep.After(dataCouldRemoveBefore) {
allServerKeep = dataCouldRemoveBefore allServerKeep = dataCouldRemoveBefore
} }
} else { } else {
// 更新特定机器可以清理数据点 // 更新特定机器可以清理数据点
for id := range alerts[i].Rules[j].Ignore { for id := range rule.Ignore {
if specialServerKeep[id].IsZero() || specialServerKeep[id].After(dataCouldRemoveBefore) { if specialServerKeep[id].IsZero() || specialServerKeep[id].After(dataCouldRemoveBefore) {
specialServerKeep[id] = dataCouldRemoveBefore specialServerKeep[id] = dataCouldRemoveBefore
specialServerIDs = append(specialServerIDs, id) specialServerIDs = append(specialServerIDs, id)