diff --git a/README.md b/README.md index a8918f7..615ff95 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 哪吒面板 -![dashboard](https://img.shields.io/badge/管理面板-v0.3.8-brightgreen?style=for-the-badge&logo=github) ![Agent release](https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&label=Agent&style=for-the-badge&logo=github) +![dashboard](https://img.shields.io/badge/管理面板-v0.3.9-brightgreen?style=for-the-badge&logo=github) ![Agent release](https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&label=Agent&style=for-the-badge&logo=github) 系统状态监控报警、API(SSL证书变更、即将到期、到期)/TCP端口存活/PING 监控、计划任务(可以定时在Agent上执行命令,备份、重启、What ever you want)、极省资源,64M 服务器也能装 agent。 diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go index 16d9b1f..40de3cd 100644 --- a/cmd/dashboard/controller/member_api.go +++ b/cmd/dashboard/controller/member_api.go @@ -15,7 +15,6 @@ import ( "github.com/naiba/nezha/pkg/mygin" "github.com/naiba/nezha/pkg/utils" pb "github.com/naiba/nezha/proto" - "github.com/naiba/nezha/service/alertmanager" "github.com/naiba/nezha/service/dao" ) @@ -37,6 +36,7 @@ func (ma *memberAPI) serve() { mr.POST("/server", ma.addOrEditServer) mr.POST("/monitor", ma.addOrEditMonitor) mr.POST("/cron", ma.addOrEditCron) + mr.GET("/cron/:id/manual", ma.manualTrigger) mr.POST("/notification", ma.addOrEditNotification) mr.POST("/alert-rule", ma.addOrEditAlertRule) mr.POST("/setting", ma.updateSetting) @@ -67,7 +67,7 @@ func (ma *memberAPI) delete(c *gin.Context) { case "notification": err = dao.DB.Delete(&model.Notification{}, "id = ?", id).Error if err == nil { - alertmanager.OnDeleteNotification(id) + dao.OnDeleteNotification(id) } case "monitor": err = dao.DB.Delete(&model.Monitor{}, "id = ?", id).Error @@ -88,7 +88,7 @@ func (ma *memberAPI) delete(c *gin.Context) { case "alert-rule": err = dao.DB.Delete(&model.AlertRule{}, "id = ?", id).Error if err == nil { - alertmanager.OnDeleteAlert(id) + dao.OnDeleteAlert(id) } } if err != nil { @@ -281,7 +281,7 @@ func (ma *memberAPI) addOrEditCron(c *gin.Context) { Type: model.TaskTypeCommand, }) } else { - alertmanager.SendNotification(fmt.Sprintf("计划任务:%s,服务器:%d 离线,无法执行。", cr.Name, cr.Servers[j]), false) + dao.SendNotification(fmt.Sprintf("计划任务:%s,服务器:%d 离线,无法执行。", cr.Name, cr.Servers[j]), false) } } }) @@ -294,6 +294,23 @@ func (ma *memberAPI) addOrEditCron(c *gin.Context) { }) } +func (ma *memberAPI) manualTrigger(c *gin.Context) { + var cr model.Cron + if err := dao.DB.First(&cr, "id = ?", c.Param("id")).Error; err != nil { + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusBadRequest, + Message: err.Error(), + }) + return + } + + dao.CronTrigger(&cr) + + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusOK, + }) +} + type notificationForm struct { ID uint64 Name string @@ -333,7 +350,7 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) { }) return } - alertmanager.OnRefreshOrAddNotification(n) + dao.OnRefreshOrAddNotification(n) c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, }) @@ -384,7 +401,7 @@ func (ma *memberAPI) addOrEditAlertRule(c *gin.Context) { }) return } - alertmanager.OnRefreshOrAddAlert(r) + dao.OnRefreshOrAddAlert(r) c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, }) diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index fe9dce4..e34ce1c 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -13,7 +13,6 @@ import ( "github.com/naiba/nezha/cmd/dashboard/rpc" "github.com/naiba/nezha/model" pb "github.com/naiba/nezha/proto" - "github.com/naiba/nezha/service/alertmanager" "github.com/naiba/nezha/service/dao" ) @@ -90,7 +89,7 @@ func loadCrons() { Type: model.TaskTypeCommand, }) } else { - alertmanager.SendNotification(fmt.Sprintf("计划任务:%s,服务器:%d 离线,无法执行。", cr.Name, cr.Servers[j]), false) + dao.SendNotification(fmt.Sprintf("计划任务:%s,服务器:%d 离线,无法执行。", cr.Name, cr.Servers[j]), false) } } }) @@ -106,5 +105,5 @@ func main() { go controller.ServeWeb(dao.Conf.HTTPPort) go rpc.ServeRPC(5555) go rpc.DispatchTask(time.Minute * 3) - alertmanager.Start() + dao.AlertSentinelStart() } diff --git a/resource/static/main.js b/resource/static/main.js index eb85505..8f93b3f 100644 --- a/resource/static/main.js +++ b/resource/static/main.js @@ -149,7 +149,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=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() }) var servers @@ -192,6 +192,42 @@ function deleteRequest(api) { }); } +function manualTrigger(btn, cronId) { + $(btn).toggleClass('loading') + $.ajax({ + url: '/api/cron/' + cronId + '/manual', + type: 'GET', + }).done(resp => { + $(btn).toggleClass('loading') + if (resp.code == 200) { + $.suiAlert({ + title: '触发成功,等待执行结果', + type: 'success', + description: resp.message, + time: '3', + position: 'top-center', + }); + } else { + $.suiAlert({ + title: '触发失败 ', + type: 'error', + description: resp.code + ':' + resp.message, + time: '3', + position: 'top-center', + }); + } + }).fail(err => { + $(btn).toggleClass('loading') + $.suiAlert({ + title: '触发失败 ', + type: 'error', + description: '网络错误:' + err.responseText, + time: '3', + position: 'top-center', + }); + }); +} + function logout(id) { $.post('/api/logout', JSON.stringify({ id: id })).done(function (resp) { if (resp.code == 200) { diff --git a/resource/template/common/footer.html b/resource/template/common/footer.html index 91c8f06..3c3bbdd 100644 --- a/resource/template/common/footer.html +++ b/resource/template/common/footer.html @@ -8,7 +8,7 @@ - + diff --git a/resource/template/dashboard/cron.html b/resource/template/dashboard/cron.html index ad5cfe8..d5347bb 100644 --- a/resource/template/dashboard/cron.html +++ b/resource/template/dashboard/cron.html @@ -37,6 +37,9 @@ {{$cron.LastResult}}
+ diff --git a/service/alertmanager/alertmanager.go b/service/dao/alertsentinel.go similarity index 51% rename from service/alertmanager/alertmanager.go rename to service/dao/alertsentinel.go index 3481d46..7b8e5f5 100644 --- a/service/alertmanager/alertmanager.go +++ b/service/dao/alertsentinel.go @@ -1,23 +1,14 @@ -package alertmanager +package dao import ( - "crypto/md5" - "encoding/hex" "fmt" "log" "sync" "time" "github.com/naiba/nezha/model" - "github.com/naiba/nezha/service/dao" ) -const firstNotificationDelay = time.Minute * 15 - -// 通知方式 -var notifications []model.Notification -var notificationsLock sync.RWMutex - // 报警规则 var alertsLock sync.RWMutex var alerts []model.AlertRule @@ -28,15 +19,15 @@ type NotificationHistory struct { Until time.Time } -func Start() { +func AlertSentinelStart() { alertsStore = make(map[uint64]map[uint64][][]interface{}) notificationsLock.Lock() - if err := dao.DB.Find(¬ifications).Error; err != nil { + if err := DB.Find(¬ifications).Error; err != nil { panic(err) } notificationsLock.Unlock() alertsLock.Lock() - if err := dao.DB.Find(&alerts).Error; err != nil { + if err := DB.Find(&alerts).Error; err != nil { panic(err) } for i := 0; i < len(alerts); i++ { @@ -56,7 +47,7 @@ func Start() { checkCount = 0 lastPrint = startedAt } - time.Sleep(time.Until(startedAt.Add(time.Second * dao.SnapshotDelay))) + time.Sleep(time.Until(startedAt.Add(time.Second * SnapshotDelay))) } } @@ -89,44 +80,18 @@ func OnDeleteAlert(id uint64) { } } -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 !isEdit { - notifications = append(notifications, n) - } -} - -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-- - } - } -} - func checkStatus() { alertsLock.RLock() defer alertsLock.RUnlock() - dao.ServerLock.RLock() - defer dao.ServerLock.RUnlock() + ServerLock.RLock() + defer ServerLock.RUnlock() for _, alert := range alerts { // 跳过未启用 if alert.Enable == nil || !*alert.Enable { continue } - for _, server := range dao.ServerList { + for _, server := range ServerList { // 监测点 alertsStore[alert.ID][server.ID] = append(alertsStore[alert. ID][server.ID], alert.Snapshot(server)) @@ -143,42 +108,3 @@ func checkStatus() { } } } - -func SendNotification(desc string, muteable bool) { - if muteable { - // 通知防骚扰策略 - nID := hex.EncodeToString(md5.New().Sum([]byte(desc))) - var flag bool - if cacheN, has := dao.Cache.Get(nID); has { - nHistory := cacheN.(NotificationHistory) - // 每次提醒都增加一倍等待时间,最后每天最多提醒一次 - if time.Now().After(nHistory.Until) { - flag = true - nHistory.Duration *= 2 - if nHistory.Duration > time.Hour*24 { - nHistory.Duration = time.Hour * 24 - } - nHistory.Until = time.Now().Add(nHistory.Duration) - // 缓存有效期加 10 分钟 - dao.Cache.Set(nID, nHistory, nHistory.Duration+time.Minute*10) - } - } else { - // 新提醒直接通知 - flag = true - dao.Cache.Set(nID, NotificationHistory{ - Duration: firstNotificationDelay, - Until: time.Now().Add(firstNotificationDelay), - }, firstNotificationDelay+time.Minute*10) - } - - if !flag { - return - } - } - // 发出通知 - notificationsLock.RLock() - defer notificationsLock.RUnlock() - for i := 0; i < len(notifications); i++ { - notifications[i].Send(desc) - } -} diff --git a/service/dao/dao.go b/service/dao/dao.go index f5ee0e8..52b0b25 100644 --- a/service/dao/dao.go +++ b/service/dao/dao.go @@ -1,6 +1,7 @@ package dao import ( + "fmt" "sort" "sync" @@ -9,32 +10,27 @@ import ( "gorm.io/gorm" "github.com/naiba/nezha/model" + pb "github.com/naiba/nezha/proto" ) +var Version = "v0.3.9" + const ( SnapshotDelay = 3 ReportDelay = 2 ) -var Conf *model.Config +var ( + Conf *model.Config + Cache *cache.Cache + DB *gorm.DB -var Cache *cache.Cache + ServerList map[uint64]*model.Server + ServerLock sync.RWMutex -var DB *gorm.DB - -// 服务器监控、状态相关 -var ServerList map[uint64]*model.Server -var ServerLock sync.RWMutex - -var SortedServerList []*model.Server -var SortedServerLock sync.RWMutex - -// 计划任务相关 -var CronLock sync.RWMutex -var Crons map[uint64]*model.Cron -var Cron *cron.Cron - -var Version = "v0.3.8" + SortedServerList []*model.Server + SortedServerLock sync.RWMutex +) func ReSortServer() { ServerLock.RLock() @@ -54,3 +50,25 @@ func ReSortServer() { return SortedServerList[i].DisplayIndex > SortedServerList[j].DisplayIndex }) } + +// =============== Cron Mixin =============== + +var CronLock sync.RWMutex +var Crons map[uint64]*model.Cron +var Cron *cron.Cron + +func CronTrigger(c *model.Cron) { + ServerLock.RLock() + defer ServerLock.RUnlock() + for j := 0; j < len(c.Servers); j++ { + if ServerList[c.Servers[j]].TaskStream != nil { + ServerList[c.Servers[j]].TaskStream.Send(&pb.Task{ + Id: c.ID, + Data: c.Command, + Type: model.TaskTypeCommand, + }) + } else { + SendNotification(fmt.Sprintf("计划任务:%s,服务器:%d 离线,无法执行。", c.Name, c.Servers[j]), false) + } + } +} diff --git a/service/dao/notification.go b/service/dao/notification.go new file mode 100644 index 0000000..3084e61 --- /dev/null +++ b/service/dao/notification.go @@ -0,0 +1,81 @@ +package dao + +import ( + "crypto/md5" + "encoding/hex" + "sync" + "time" + + "github.com/naiba/nezha/model" +) + +const firstNotificationDelay = time.Minute * 15 + +// 通知方式 +var notifications []model.Notification +var notificationsLock sync.RWMutex + +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 !isEdit { + notifications = append(notifications, n) + } +} + +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-- + } + } +} + +func SendNotification(desc string, muteable bool) { + if muteable { + // 通知防骚扰策略 + nID := hex.EncodeToString(md5.New().Sum([]byte(desc))) + var flag bool + if cacheN, has := Cache.Get(nID); has { + nHistory := cacheN.(NotificationHistory) + // 每次提醒都增加一倍等待时间,最后每天最多提醒一次 + if time.Now().After(nHistory.Until) { + flag = true + nHistory.Duration *= 2 + if nHistory.Duration > time.Hour*24 { + nHistory.Duration = time.Hour * 24 + } + nHistory.Until = time.Now().Add(nHistory.Duration) + // 缓存有效期加 10 分钟 + Cache.Set(nID, nHistory, nHistory.Duration+time.Minute*10) + } + } else { + // 新提醒直接通知 + flag = true + Cache.Set(nID, NotificationHistory{ + Duration: firstNotificationDelay, + Until: time.Now().Add(firstNotificationDelay), + }, firstNotificationDelay+time.Minute*10) + } + + if !flag { + return + } + } + // 发出通知 + notificationsLock.RLock() + defer notificationsLock.RUnlock() + for i := 0; i < len(notifications); i++ { + notifications[i].Send(desc) + } +} diff --git a/service/rpc/nezha.go b/service/rpc/nezha.go index b36a512..6fea4bd 100644 --- a/service/rpc/nezha.go +++ b/service/rpc/nezha.go @@ -8,7 +8,6 @@ import ( "github.com/naiba/nezha/model" pb "github.com/naiba/nezha/proto" - "github.com/naiba/nezha/service/alertmanager" "github.com/naiba/nezha/service/dao" ) @@ -52,7 +51,7 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece if errMsg != "" { var monitor model.Monitor dao.DB.First(&monitor, "id = ?", r.GetId()) - alertmanager.SendNotification(fmt.Sprintf("服务监控:%s %s", monitor.Name, errMsg), true) + dao.SendNotification(fmt.Sprintf("服务监控:%s %s", monitor.Name, errMsg), true) } } if r.GetType() == model.TaskTypeCommand { @@ -62,10 +61,10 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece cr := dao.Crons[r.GetId()] if cr != nil { if cr.PushSuccessful && r.GetSuccessful() { - alertmanager.SendNotification(fmt.Sprintf("成功计划任务:%s ,服务器:%d,日志:\n%s", cr.Name, clientID, r.GetData()), false) + dao.SendNotification(fmt.Sprintf("成功计划任务:%s ,服务器:%d,日志:\n%s", cr.Name, clientID, r.GetData()), false) } if !r.GetSuccessful() { - alertmanager.SendNotification(fmt.Sprintf("失败计划任务:%s ,服务器:%d,日志:\n%s", cr.Name, clientID, r.GetData()), false) + dao.SendNotification(fmt.Sprintf("失败计划任务:%s ,服务器:%d,日志:\n%s", cr.Name, clientID, r.GetData()), false) } dao.DB.Model(cr).Updates(model.Cron{ LastExecutedAt: time.Now().Add(time.Second * -1 * time.Duration(r.GetDelay())), @@ -127,7 +126,7 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece dao.ServerList[clientID].Host.IP != "" && host.IP != "" && dao.ServerList[clientID].Host.IP != host.IP { - alertmanager.SendNotification(fmt.Sprintf( + dao.SendNotification(fmt.Sprintf( "IP变更提醒 服务器:%s ,旧IP:%s,新IP:%s。", dao.ServerList[clientID].Name, dao.ServerList[clientID].Host.IP, host.IP), true) }