diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index bc2d3ba..0a3df45 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -62,6 +62,11 @@ func routers(r *gin.Engine) { auth.POST("/user", commonHandler(createUser)) auth.POST("/batch-delete/user", commonHandler(batchDeleteUser)) + auth.GET("/monitor", commonHandler(listMonitor)) + auth.POST("/monitor", commonHandler(createMonitor)) + auth.PATCH("/monitor/:id", commonHandler(updateMonitor)) + auth.POST("/batch-delete/monitor", commonHandler(batchDeleteMonitor)) + auth.POST("/server-group", commonHandler(createServerGroup)) auth.PATCH("/server-group/:id", commonHandler(updateServerGroup)) auth.POST("/batch-delete/server-group", commonHandler(batchDeleteServerGroup)) diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go index dfc66c7..fa03fda 100644 --- a/cmd/dashboard/controller/member_api.go +++ b/cmd/dashboard/controller/member_api.go @@ -32,7 +32,6 @@ func (ma *memberAPI) serve() { // Btn: "点此登录", // Redirect: "/login", // })) - mr.POST("/monitor", ma.addOrEditMonitor) mr.POST("/cron", ma.addOrEditCron) mr.GET("/cron/:id/manual", ma.manualTrigger) mr.POST("/force-update", ma.forceUpdate) @@ -179,12 +178,7 @@ func (ma *memberAPI) delete(c *gin.Context) { if err == nil { singleton.OnNATUpdate() } - case "monitor": - err = singleton.DB.Unscoped().Delete(&model.Monitor{}, "id = ?", id).Error - if err == nil { - singleton.ServiceSentinelShared.OnMonitorDelete(id) - err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ?", id).Error - } + case "cron": err = singleton.DB.Unscoped().Delete(&model.Cron{}, "id = ?", id).Error if err == nil { @@ -214,87 +208,6 @@ func (ma *memberAPI) delete(c *gin.Context) { }) } -type monitorForm struct { - ID uint64 - Name string - Target string - Type uint8 - Cover uint8 - Notify string - //NotificationTag string - SkipServersRaw string - Duration uint64 - MinLatency float32 - MaxLatency float32 - LatencyNotify string - EnableTriggerTask string - EnableShowInService string - FailTriggerTasksRaw string - RecoverTriggerTasksRaw string -} - -func (ma *memberAPI) addOrEditMonitor(c *gin.Context) { - var mf monitorForm - var m model.Monitor - err := c.ShouldBindJSON(&mf) - if err == nil { - m.Name = mf.Name - m.Target = strings.TrimSpace(mf.Target) - m.Type = mf.Type - m.ID = mf.ID - m.SkipServersRaw = mf.SkipServersRaw - m.Cover = mf.Cover - m.Notify = mf.Notify == "on" - //m.NotificationTag = mf.NotificationTag - m.Duration = mf.Duration - m.LatencyNotify = mf.LatencyNotify == "on" - m.MinLatency = mf.MinLatency - m.MaxLatency = mf.MaxLatency - m.EnableShowInService = mf.EnableShowInService == "on" - m.EnableTriggerTask = mf.EnableTriggerTask == "on" - m.RecoverTriggerTasksRaw = mf.RecoverTriggerTasksRaw - m.FailTriggerTasksRaw = mf.FailTriggerTasksRaw - err = m.InitSkipServers() - } - if err == nil { - // 保证NotificationTag不为空 - //if m.NotificationTag == "" { - // m.NotificationTag = "default" - //} - err = utils.Json.Unmarshal([]byte(mf.FailTriggerTasksRaw), &m.FailTriggerTasks) - } - if err == nil { - err = utils.Json.Unmarshal([]byte(mf.RecoverTriggerTasksRaw), &m.RecoverTriggerTasks) - } - if err == nil { - if m.ID == 0 { - err = singleton.DB.Create(&m).Error - } else { - err = singleton.DB.Save(&m).Error - } - } - if err == nil { - if m.Cover == 0 { - err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error - } else { - err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id not in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error - } - } - if err == nil { - err = singleton.ServiceSentinelShared.OnMonitorUpdate(m) - } - if err != nil { - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusBadRequest, - Message: fmt.Sprintf("请求错误:%s", err), - }) - return - } - c.JSON(http.StatusOK, model.Response{ - Code: http.StatusOK, - }) -} - type cronForm struct { ID uint64 TaskType uint8 // 0:计划任务 1:触发任务 diff --git a/cmd/dashboard/controller/member_page.go b/cmd/dashboard/controller/member_page.go index 6718ee1..8cf4fb5 100644 --- a/cmd/dashboard/controller/member_page.go +++ b/cmd/dashboard/controller/member_page.go @@ -21,8 +21,6 @@ func (mp *memberPage) serve() { // // Btn: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}), // Redirect: "/login", // })) - mr.GET("/server", mp.server) - mr.GET("/monitor", mp.monitor) mr.GET("/cron", mp.cron) mr.GET("/notification", mp.notification) mr.GET("/ddns", mp.ddns) @@ -40,22 +38,6 @@ func (mp *memberPage) api(c *gin.Context) { }) } -func (mp *memberPage) server(c *gin.Context) { - singleton.SortedServerLock.RLock() - defer singleton.SortedServerLock.RUnlock() - c.HTML(http.StatusOK, "dashboard-", gin.H{ - // "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServersManagement"}), - "Servers": singleton.SortedServerList, - }) -} - -func (mp *memberPage) monitor(c *gin.Context) { - c.HTML(http.StatusOK, "dashboard-", gin.H{ - // "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesManagement"}), - "Monitors": singleton.ServiceSentinelShared.Monitors(), - }) -} - func (mp *memberPage) cron(c *gin.Context) { var crons []model.Cron singleton.DB.Find(&crons) diff --git a/cmd/dashboard/controller/monitor.go b/cmd/dashboard/controller/monitor.go new file mode 100644 index 0000000..954262e --- /dev/null +++ b/cmd/dashboard/controller/monitor.go @@ -0,0 +1,173 @@ +package controller + +import ( + "fmt" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "github.com/naiba/nezha/model" + "github.com/naiba/nezha/service/singleton" + "gorm.io/gorm" +) + +// List monitor +// @Summary List monitor +// @Security BearerAuth +// @Schemes +// @Description List monitor +// @Tags auth required +// @Produce json +// @Success 200 {object} model.CommonResponse[[]model.Monitor] +// @Router /monitor [get] +func listMonitor(c *gin.Context) ([]*model.Monitor, error) { + return singleton.ServiceSentinelShared.Monitors(), nil +} + +// Create monitor +// @Summary Create monitor +// @Security BearerAuth +// @Schemes +// @Description Create monitor +// @Tags auth required +// @Accept json +// @param request body model.MonitorForm true "Monitor Request" +// @Produce json +// @Success 200 {object} model.CommonResponse[uint64] +// @Router /monitor [post] +func createMonitor(c *gin.Context) (uint64, error) { + var mf model.MonitorForm + if err := c.ShouldBindJSON(&mf); err != nil { + return 0, err + } + + var m model.Monitor + m.Name = mf.Name + m.Target = strings.TrimSpace(mf.Target) + m.Type = mf.Type + m.SkipServers = mf.SkipServers + m.Cover = mf.Cover + m.Notify = mf.Notify + m.NotificationGroupID = mf.NotificationGroupID + m.Duration = mf.Duration + m.LatencyNotify = mf.LatencyNotify + m.MinLatency = mf.MinLatency + m.MaxLatency = mf.MaxLatency + m.EnableShowInService = mf.EnableShowInService + m.EnableTriggerTask = mf.EnableTriggerTask + m.RecoverTriggerTasks = mf.RecoverTriggerTasks + m.FailTriggerTasks = mf.FailTriggerTasks + + if err := singleton.DB.Create(&m).Error; err != nil { + return 0, err + } + + var skipServers []uint64 + for k := range m.SkipServers { + skipServers = append(skipServers, k) + } + + var err error + if m.Cover == 0 { + err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id in (?)", m.ID, skipServers).Error + } else { + err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id not in (?)", m.ID, skipServers).Error + } + if err != nil { + return 0, err + } + + return m.ID, singleton.ServiceSentinelShared.OnMonitorUpdate(m) +} + +// Update monitor +// @Summary Update monitor +// @Security BearerAuth +// @Schemes +// @Description Update monitor +// @Tags auth required +// @Accept json +// @param id path uint true "Monitor ID" +// @param request body model.MonitorForm true "Monitor Request" +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /monitor/{id} [patch] +func updateMonitor(c *gin.Context) (any, error) { + strID := c.Param("id") + id, err := strconv.ParseUint(strID, 10, 64) + if err != nil { + return nil, err + } + var mf model.MonitorForm + if err := c.ShouldBindJSON(&mf); err != nil { + return nil, err + } + var m model.Monitor + if err := singleton.DB.First(&m, id).Error; err != nil { + return nil, fmt.Errorf("monitor id %d does not exist", id) + } + m.Name = mf.Name + m.Target = strings.TrimSpace(mf.Target) + m.Type = mf.Type + m.SkipServers = mf.SkipServers + m.Cover = mf.Cover + m.Notify = mf.Notify + m.NotificationGroupID = mf.NotificationGroupID + m.Duration = mf.Duration + m.LatencyNotify = mf.LatencyNotify + m.MinLatency = mf.MinLatency + m.MaxLatency = mf.MaxLatency + m.EnableShowInService = mf.EnableShowInService + m.EnableTriggerTask = mf.EnableTriggerTask + m.RecoverTriggerTasks = mf.RecoverTriggerTasks + m.FailTriggerTasks = mf.FailTriggerTasks + + if err := singleton.DB.Save(&m).Error; err != nil { + return nil, err + } + + var skipServers []uint64 + for k := range m.SkipServers { + skipServers = append(skipServers, k) + } + + if m.Cover == 0 { + err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id in (?)", m.ID, skipServers).Error + } else { + err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id not in (?)", m.ID, skipServers).Error + } + if err != nil { + return nil, err + } + + return nil, singleton.ServiceSentinelShared.OnMonitorUpdate(m) +} + +// Batch delete monitor +// @Summary Batch delete monitor +// @Security BearerAuth +// @Schemes +// @Description Batch delete monitor +// @Tags auth required +// @Accept json +// @param request body []uint true "id list" +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /batch-delete/monitor [post] +func batchDeleteMonitor(c *gin.Context) (any, error) { + var ids []uint64 + if err := c.ShouldBindJSON(&ids); err != nil { + return nil, err + } + err := singleton.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Unscoped().Delete(&model.Monitor{}, "id in (?)", ids).Error; err != nil { + return err + } + return tx.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id in (?)", ids).Error + }) + if err != nil { + return nil, err + } + singleton.ServiceSentinelShared.OnMonitorDelete(ids) + return nil, nil +} diff --git a/cmd/dashboard/controller/server.go b/cmd/dashboard/controller/server.go index 3633ee6..557dd65 100644 --- a/cmd/dashboard/controller/server.go +++ b/cmd/dashboard/controller/server.go @@ -20,12 +20,10 @@ import ( // @Produce json // @Success 200 {object} model.CommonResponse[any] // @Router /server [get] -func listServer(c *gin.Context) ([]model.Server, error) { - var servers []model.Server - if err := singleton.DB.Find(&servers).Error; err != nil { - return nil, newGormError("%v", err) - } - return servers, nil +func listServer(c *gin.Context) ([]*model.Server, error) { + singleton.SortedServerLock.RLock() + defer singleton.SortedServerLock.RUnlock() + return singleton.SortedServerList, nil } // Edit server diff --git a/model/monitor.go b/model/monitor.go index df33735..bbaed1e 100644 --- a/model/monitor.go +++ b/model/monitor.go @@ -46,27 +46,28 @@ const ( type Monitor struct { Common - Name string - Type uint8 - Target string - SkipServersRaw string - Duration uint64 - Notify bool - NotificationGroupID uint64 // 当前服务监控所属的通知组 ID - Cover uint8 + Name string `json:"name,omitempty"` + Type uint8 `json:"type,omitempty"` + Target string `json:"target,omitempty"` + SkipServersRaw string `json:"-"` + Duration uint64 `json:"duration,omitempty"` + Notify bool `json:"notify,omitempty"` + NotificationGroupID uint64 `json:"notification_group_id,omitempty"` // 当前服务监控所属的通知组 ID + Cover uint8 `json:"cover,omitempty"` - EnableTriggerTask bool `gorm:"default: false"` - EnableShowInService bool `gorm:"default: false"` - FailTriggerTasksRaw string `gorm:"default:'[]'"` - RecoverTriggerTasksRaw string `gorm:"default:'[]'"` - FailTriggerTasks []uint64 `gorm:"-" json:"-"` // 失败时执行的触发任务id - RecoverTriggerTasks []uint64 `gorm:"-" json:"-"` // 恢复时执行的触发任务id + EnableTriggerTask bool `gorm:"default: false" json:"enable_trigger_task,omitempty"` + EnableShowInService bool `gorm:"default: false" json:"enable_show_in_service,omitempty"` + FailTriggerTasksRaw string `gorm:"default:'[]'" json:"-"` + RecoverTriggerTasksRaw string `gorm:"default:'[]'" json:"-"` - MinLatency float32 - MaxLatency float32 - LatencyNotify bool + FailTriggerTasks []uint64 `gorm:"-" json:"fail_trigger_tasks"` // 失败时执行的触发任务id + RecoverTriggerTasks []uint64 `gorm:"-" json:"recover_trigger_tasks"` // 恢复时执行的触发任务id - SkipServers map[uint64]bool `gorm:"-" json:"-"` + MinLatency float32 `json:"min_latency,omitempty"` + MaxLatency float32 `json:"max_latency,omitempty"` + LatencyNotify bool `json:"latency_notify,omitempty"` + + SkipServers map[uint64]bool `gorm:"-" json:"skip_servers"` CronJobID cron.EntryID `gorm:"-" json:"-"` } @@ -88,7 +89,11 @@ func (m *Monitor) CronSpec() string { } func (m *Monitor) BeforeSave(tx *gorm.DB) error { - + if data, err := utils.Json.Marshal(m.SkipServers); err != nil { + return err + } else { + m.SkipServersRaw = string(data) + } if data, err := utils.Json.Marshal(m.FailTriggerTasks); err != nil { return err } else { @@ -104,14 +109,10 @@ func (m *Monitor) BeforeSave(tx *gorm.DB) error { func (m *Monitor) AfterFind(tx *gorm.DB) error { m.SkipServers = make(map[uint64]bool) - var skipServers []uint64 - if err := utils.Json.Unmarshal([]byte(m.SkipServersRaw), &skipServers); err != nil { + if err := utils.Json.Unmarshal([]byte(m.SkipServersRaw), &m.SkipServers); err != nil { log.Println("NEZHA>> Monitor.AfterFind:", err) return nil } - for i := 0; i < len(skipServers); i++ { - m.SkipServers[skipServers[i]] = true - } // 加载触发任务列表 if err := utils.Json.Unmarshal([]byte(m.FailTriggerTasksRaw), &m.FailTriggerTasks); err != nil { @@ -128,15 +129,3 @@ func (m *Monitor) AfterFind(tx *gorm.DB) error { func IsServiceSentinelNeeded(t uint64) bool { return t != TaskTypeCommand && t != TaskTypeTerminalGRPC && t != TaskTypeUpgrade } - -func (m *Monitor) InitSkipServers() error { - var skipServers []uint64 - if err := utils.Json.Unmarshal([]byte(m.SkipServersRaw), &skipServers); err != nil { - return err - } - m.SkipServers = make(map[uint64]bool) - for i := 0; i < len(skipServers); i++ { - m.SkipServers[skipServers[i]] = true - } - return nil -} diff --git a/model/monitor_api.go b/model/monitor_api.go new file mode 100644 index 0000000..205b4f1 --- /dev/null +++ b/model/monitor_api.go @@ -0,0 +1,19 @@ +package model + +type MonitorForm struct { + Name string `json:"name,omitempty"` + Target string `json:"target,omitempty"` + Type uint8 `json:"type,omitempty"` + Cover uint8 `json:"cover,omitempty"` + Notify bool `json:"notify,omitempty"` + Duration uint64 `json:"duration,omitempty"` + MinLatency float32 `json:"min_latency,omitempty"` + MaxLatency float32 `json:"max_latency,omitempty"` + LatencyNotify bool `json:"latency_notify,omitempty"` + EnableTriggerTask bool `json:"enable_trigger_task,omitempty"` + EnableShowInService bool `json:"enable_show_in_service,omitempty"` + FailTriggerTasks []uint64 `json:"fail_trigger_tasks,omitempty"` + RecoverTriggerTasks []uint64 `json:"recover_trigger_tasks,omitempty"` + SkipServers map[uint64]bool `json:"skip_servers,omitempty"` + NotificationGroupID uint64 `json:"notification_group_id,omitempty"` +} diff --git a/service/singleton/servicesentinel.go b/service/singleton/servicesentinel.go index 193a99e..bfb109e 100644 --- a/service/singleton/servicesentinel.go +++ b/service/singleton/servicesentinel.go @@ -264,7 +264,7 @@ func (ss *ServiceSentinel) OnMonitorUpdate(m model.Monitor) error { return nil } -func (ss *ServiceSentinel) OnMonitorDelete(id uint64) { +func (ss *ServiceSentinel) OnMonitorDelete(ids []uint64) { ss.serviceResponseDataStoreLock.Lock() defer ss.serviceResponseDataStoreLock.Unlock() ss.monthlyStatusLock.Lock() @@ -272,20 +272,22 @@ func (ss *ServiceSentinel) OnMonitorDelete(id uint64) { ss.monitorsLock.Lock() defer ss.monitorsLock.Unlock() - delete(ss.serviceCurrentStatusIndex, id) - delete(ss.serviceCurrentStatusData, id) - delete(ss.lastStatus, id) - delete(ss.serviceResponseDataStoreCurrentUp, id) - delete(ss.serviceResponseDataStoreCurrentDown, id) - delete(ss.serviceResponseDataStoreCurrentAvgDelay, id) - delete(ss.sslCertCache, id) - delete(ss.serviceStatusToday, id) + for _, id := range ids { + delete(ss.serviceCurrentStatusIndex, id) + delete(ss.serviceCurrentStatusData, id) + delete(ss.lastStatus, id) + delete(ss.serviceResponseDataStoreCurrentUp, id) + delete(ss.serviceResponseDataStoreCurrentDown, id) + delete(ss.serviceResponseDataStoreCurrentAvgDelay, id) + delete(ss.sslCertCache, id) + delete(ss.serviceStatusToday, id) - // 停掉定时任务 - Cron.Remove(ss.monitors[id].CronJobID) - delete(ss.monitors, id) + // 停掉定时任务 + Cron.Remove(ss.monitors[id].CronJobID) + delete(ss.monitors, id) - delete(ss.monthlyStatus, id) + delete(ss.monthlyStatus, id) + } } func (ss *ServiceSentinel) LoadStats() map[uint64]*model.ServiceItemResponse {