[dashboard v0.3.9] 手动即时触发计划任务

This commit is contained in:
naiba 2021-01-24 09:41:35 +08:00
parent 175eb996d3
commit a957b21265
10 changed files with 195 additions and 116 deletions

View File

@ -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。 系统状态监控报警、API(SSL证书变更、即将到期、到期)/TCP端口存活/PING 监控、计划任务(可以定时在Agent上执行命令备份、重启、What ever you want)、极省资源64M 服务器也能装 agent。

View File

@ -15,7 +15,6 @@ import (
"github.com/naiba/nezha/pkg/mygin" "github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/pkg/utils" "github.com/naiba/nezha/pkg/utils"
pb "github.com/naiba/nezha/proto" pb "github.com/naiba/nezha/proto"
"github.com/naiba/nezha/service/alertmanager"
"github.com/naiba/nezha/service/dao" "github.com/naiba/nezha/service/dao"
) )
@ -37,6 +36,7 @@ func (ma *memberAPI) serve() {
mr.POST("/server", ma.addOrEditServer) mr.POST("/server", ma.addOrEditServer)
mr.POST("/monitor", ma.addOrEditMonitor) mr.POST("/monitor", ma.addOrEditMonitor)
mr.POST("/cron", ma.addOrEditCron) mr.POST("/cron", ma.addOrEditCron)
mr.GET("/cron/:id/manual", ma.manualTrigger)
mr.POST("/notification", ma.addOrEditNotification) mr.POST("/notification", ma.addOrEditNotification)
mr.POST("/alert-rule", ma.addOrEditAlertRule) mr.POST("/alert-rule", ma.addOrEditAlertRule)
mr.POST("/setting", ma.updateSetting) mr.POST("/setting", ma.updateSetting)
@ -67,7 +67,7 @@ func (ma *memberAPI) delete(c *gin.Context) {
case "notification": case "notification":
err = dao.DB.Delete(&model.Notification{}, "id = ?", id).Error err = dao.DB.Delete(&model.Notification{}, "id = ?", id).Error
if err == nil { if err == nil {
alertmanager.OnDeleteNotification(id) dao.OnDeleteNotification(id)
} }
case "monitor": case "monitor":
err = dao.DB.Delete(&model.Monitor{}, "id = ?", id).Error err = dao.DB.Delete(&model.Monitor{}, "id = ?", id).Error
@ -88,7 +88,7 @@ func (ma *memberAPI) delete(c *gin.Context) {
case "alert-rule": case "alert-rule":
err = dao.DB.Delete(&model.AlertRule{}, "id = ?", id).Error err = dao.DB.Delete(&model.AlertRule{}, "id = ?", id).Error
if err == nil { if err == nil {
alertmanager.OnDeleteAlert(id) dao.OnDeleteAlert(id)
} }
} }
if err != nil { if err != nil {
@ -281,7 +281,7 @@ func (ma *memberAPI) addOrEditCron(c *gin.Context) {
Type: model.TaskTypeCommand, Type: model.TaskTypeCommand,
}) })
} else { } 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 { type notificationForm struct {
ID uint64 ID uint64
Name string Name string
@ -333,7 +350,7 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
}) })
return return
} }
alertmanager.OnRefreshOrAddNotification(n) dao.OnRefreshOrAddNotification(n)
c.JSON(http.StatusOK, model.Response{ c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK, Code: http.StatusOK,
}) })
@ -384,7 +401,7 @@ func (ma *memberAPI) addOrEditAlertRule(c *gin.Context) {
}) })
return return
} }
alertmanager.OnRefreshOrAddAlert(r) dao.OnRefreshOrAddAlert(r)
c.JSON(http.StatusOK, model.Response{ c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK, Code: http.StatusOK,
}) })

View File

@ -13,7 +13,6 @@ import (
"github.com/naiba/nezha/cmd/dashboard/rpc" "github.com/naiba/nezha/cmd/dashboard/rpc"
"github.com/naiba/nezha/model" "github.com/naiba/nezha/model"
pb "github.com/naiba/nezha/proto" pb "github.com/naiba/nezha/proto"
"github.com/naiba/nezha/service/alertmanager"
"github.com/naiba/nezha/service/dao" "github.com/naiba/nezha/service/dao"
) )
@ -90,7 +89,7 @@ func loadCrons() {
Type: model.TaskTypeCommand, Type: model.TaskTypeCommand,
}) })
} else { } 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 controller.ServeWeb(dao.Conf.HTTPPort)
go rpc.ServeRPC(5555) go rpc.ServeRPC(5555)
go rpc.DispatchTask(time.Minute * 3) go rpc.DispatchTask(time.Minute * 3)
alertmanager.Start() dao.AlertSentinelStart()
} }

View File

@ -149,7 +149,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=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()
}) })
var servers 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) { function logout(id) {
$.post('/api/logout', JSON.stringify({ id: id })).done(function (resp) { $.post('/api/logout', JSON.stringify({ id: id })).done(function (resp) {
if (resp.code == 200) { if (resp.code == 200) {

View File

@ -8,7 +8,7 @@
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.1/dist/semantic.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.1/dist/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script> <script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.min.js"></script>
<script src="/static/main.js?v202101220904"></script> <script src="/static/main.js?v202101240939"></script>
</body> </body>
</html> </html>

View File

@ -37,6 +37,9 @@
<td>{{$cron.LastResult}}</td> <td>{{$cron.LastResult}}</td>
<td> <td>
<div class="ui mini icon buttons"> <div class="ui mini icon buttons">
<button class="ui button" onclick="manualTrigger(this, {{$cron.ID}})">
<i class="play icon"></i>
</button>
<button class="ui button" onclick="addOrEditCron({{$cron}})"> <button class="ui button" onclick="addOrEditCron({{$cron}})">
<i class="edit icon"></i> <i class="edit icon"></i>
</button> </button>

View File

@ -1,23 +1,14 @@
package alertmanager package dao
import ( import (
"crypto/md5"
"encoding/hex"
"fmt" "fmt"
"log" "log"
"sync" "sync"
"time" "time"
"github.com/naiba/nezha/model" "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 alertsLock sync.RWMutex
var alerts []model.AlertRule var alerts []model.AlertRule
@ -28,15 +19,15 @@ type NotificationHistory struct {
Until time.Time Until time.Time
} }
func Start() { func AlertSentinelStart() {
alertsStore = make(map[uint64]map[uint64][][]interface{}) alertsStore = make(map[uint64]map[uint64][][]interface{})
notificationsLock.Lock() notificationsLock.Lock()
if err := dao.DB.Find(&notifications).Error; err != nil { if err := DB.Find(&notifications).Error; err != nil {
panic(err) panic(err)
} }
notificationsLock.Unlock() notificationsLock.Unlock()
alertsLock.Lock() alertsLock.Lock()
if err := dao.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 i := 0; i < len(alerts); i++ {
@ -56,7 +47,7 @@ func Start() {
checkCount = 0 checkCount = 0
lastPrint = startedAt 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() { func checkStatus() {
alertsLock.RLock() alertsLock.RLock()
defer alertsLock.RUnlock() defer alertsLock.RUnlock()
dao.ServerLock.RLock() ServerLock.RLock()
defer dao.ServerLock.RUnlock() defer ServerLock.RUnlock()
for _, alert := range alerts { for _, alert := range alerts {
// 跳过未启用 // 跳过未启用
if alert.Enable == nil || !*alert.Enable { if alert.Enable == nil || !*alert.Enable {
continue continue
} }
for _, server := range dao.ServerList { for _, server := range ServerList {
// 监测点 // 监测点
alertsStore[alert.ID][server.ID] = append(alertsStore[alert. alertsStore[alert.ID][server.ID] = append(alertsStore[alert.
ID][server.ID], alert.Snapshot(server)) 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)
}
}

View File

@ -1,6 +1,7 @@
package dao package dao
import ( import (
"fmt"
"sort" "sort"
"sync" "sync"
@ -9,32 +10,27 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"github.com/naiba/nezha/model" "github.com/naiba/nezha/model"
pb "github.com/naiba/nezha/proto"
) )
var Version = "v0.3.9"
const ( const (
SnapshotDelay = 3 SnapshotDelay = 3
ReportDelay = 2 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 SortedServerList []*model.Server
SortedServerLock sync.RWMutex
// 服务器监控、状态相关 )
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"
func ReSortServer() { func ReSortServer() {
ServerLock.RLock() ServerLock.RLock()
@ -54,3 +50,25 @@ func ReSortServer() {
return SortedServerList[i].DisplayIndex > SortedServerList[j].DisplayIndex 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)
}
}
}

View File

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

View File

@ -8,7 +8,6 @@ import (
"github.com/naiba/nezha/model" "github.com/naiba/nezha/model"
pb "github.com/naiba/nezha/proto" pb "github.com/naiba/nezha/proto"
"github.com/naiba/nezha/service/alertmanager"
"github.com/naiba/nezha/service/dao" "github.com/naiba/nezha/service/dao"
) )
@ -52,7 +51,7 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece
if errMsg != "" { if errMsg != "" {
var monitor model.Monitor var monitor model.Monitor
dao.DB.First(&monitor, "id = ?", r.GetId()) 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 { 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()] cr := dao.Crons[r.GetId()]
if cr != nil { if cr != nil {
if cr.PushSuccessful && r.GetSuccessful() { 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() { 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{ dao.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())),
@ -127,7 +126,7 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
dao.ServerList[clientID].Host.IP != "" && dao.ServerList[clientID].Host.IP != "" &&
host.IP != "" && host.IP != "" &&
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。", "IP变更提醒 服务器:%s 旧IP%s新IP%s。",
dao.ServerList[clientID].Name, dao.ServerList[clientID].Host.IP, host.IP), true) dao.ServerList[clientID].Name, dao.ServerList[clientID].Host.IP, host.IP), true)
} }