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

@ -211,14 +211,15 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
} }
type monitorForm struct { type monitorForm struct {
ID uint64 ID uint64
Name string Name string
Target string Target string
Type uint8 Type uint8
Cover uint8 Cover uint8
Notify string Notify string
SkipServersRaw string NotificationTag string
Duration uint64 SkipServersRaw string
Duration uint64
} }
func (ma *memberAPI) addOrEditMonitor(c *gin.Context) { func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
@ -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 {
@ -259,13 +265,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,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,17 +432,18 @@ 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,
}) })
} }
type alertRuleForm struct { type alertRuleForm struct {
ID uint64 ID uint64
Name string Name string
RulesRaw string RulesRaw string
Enable string NotificationTag string
Enable string
} }
func (ma *memberAPI) addOrEditAlertRule(c *gin.Context) { func (ma *memberAPI) addOrEditAlertRule(c *gin.Context) {
@ -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 {
@ -517,14 +541,15 @@ func (ma *memberAPI) logout(c *gin.Context) {
} }
type settingForm struct { type settingForm struct {
Title string Title string
Admin string Admin string
Theme string Theme string
CustomCode string CustomCode string
ViewPassword string ViewPassword string
IgnoredIPNotification string IgnoredIPNotification string
GRPCHost string IPChangeNotificationTag string // IP变更提醒的通知组
Cover uint8 GRPCHost string
Cover uint8
EnableIPChangeNotification string EnableIPChangeNotification string
EnablePlainIPInNotification string EnablePlainIPInNotification string
@ -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

@ -20,10 +20,11 @@ type CycleTransferStats struct {
type AlertRule struct { type AlertRule struct {
Common Common
Name string Name string
RulesRaw string RulesRaw string
Enable *bool Enable *bool
Rules []Rule `gorm:"-" json:"-"` NotificationTag string // 该报警规则所在的通知组
Rules []Rule `gorm:"-" json:"-"`
} }
func (r *AlertRule) BeforeSave(tx *gorm.DB) error { func (r *AlertRule) BeforeSave(tx *gorm.DB) error {

View File

@ -71,12 +71,13 @@ type Config struct {
ProxyGRPCPort uint ProxyGRPCPort uint
TLS bool TLS bool
EnableIPChangeNotification bool EnablePlainIPInNotification bool // 通知信息IP不打码
EnablePlainIPInNotification bool
// IP变更提醒 // IP变更提醒
Cover uint8 // 覆盖范围0:提醒未被 IgnoredIPNotification 包含的所有服务器; 1:仅提醒被 IgnoredIPNotification 包含的服务器; EnableIPChangeNotification bool
IgnoredIPNotification string // 特定服务器IP多个服务器用逗号分隔 IPChangeNotificationTag string
Cover uint8 // 覆盖范围0:提醒未被 IgnoredIPNotification 包含的所有服务器; 1:仅提醒被 IgnoredIPNotification 包含的服务器;
IgnoredIPNotification string // 特定服务器IP多个服务器用逗号分隔
v *viper.Viper v *viper.Viper
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内 IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内
@ -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

@ -38,13 +38,14 @@ const (
type Monitor struct { type Monitor struct {
Common Common
Name string Name string
Type uint8 Type uint8
Target string Target string
SkipServersRaw string SkipServersRaw string
Duration uint64 Duration uint64
Notify bool Notify bool
Cover uint8 NotificationTag string // 当前服务监控所属的通知组
Cover uint8
SkipServers map[uint64]bool `gorm:"-" json:"-"` SkipServers map[uint64]bool `gorm:"-" json:"-"`
CronJobID cron.EntryID `gorm:"-" json:"-"` CronJobID cron.EntryID `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 { 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) {
// 当前 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)