mirror of
https://github.com/nezhahq/nezha.git
synced 2025-01-22 20:58:14 -05:00
feat: add network monitor hitory (#316) · 三网ping
* feat: add network monitor hitory * fix: revert proto change and add indexStore * fix: update monitor delete unuse monitor history * fix: delete unuse monitor type --------- Co-authored-by: LvGJ <lvgj1998@gmail.com>
This commit is contained in:
parent
c9bcba6f28
commit
e8b8e59bd7
@ -1,11 +1,13 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/naiba/nezha/pkg/mygin"
|
|
||||||
"github.com/naiba/nezha/service/singleton"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/naiba/nezha/pkg/mygin"
|
||||||
|
"github.com/naiba/nezha/service/singleton"
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiV1 struct {
|
type apiV1 struct {
|
||||||
@ -25,7 +27,8 @@ func (v *apiV1) serve() {
|
|||||||
}))
|
}))
|
||||||
r.GET("/server/list", v.serverList)
|
r.GET("/server/list", v.serverList)
|
||||||
r.GET("/server/details", v.serverDetails)
|
r.GET("/server/details", v.serverDetails)
|
||||||
|
mr := v.r.Group("monitor")
|
||||||
|
mr.GET("/:id", v.monitorHistoriesById)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverList 获取服务器列表 不传入Query参数则获取全部
|
// serverList 获取服务器列表 不传入Query参数则获取全部
|
||||||
@ -65,3 +68,21 @@ func (v *apiV1) serverDetails(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.JSON(200, singleton.ServerAPI.GetAllStatus())
|
c.JSON(200, singleton.ServerAPI.GetAllStatus())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *apiV1) monitorHistoriesById(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(400, gin.H{"code": 400, "message": "id参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
server, ok := singleton.ServerList[id]
|
||||||
|
if !ok {
|
||||||
|
c.AbortWithStatusJSON(404, gin.H{
|
||||||
|
"code": 404,
|
||||||
|
"message": "id不存在",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": server.ID}))
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -48,6 +49,9 @@ func (cp *commonPage) serve() {
|
|||||||
cr.Use(cp.checkViewPassword) // 前端查看密码鉴权
|
cr.Use(cp.checkViewPassword) // 前端查看密码鉴权
|
||||||
cr.GET("/", cp.home)
|
cr.GET("/", cp.home)
|
||||||
cr.GET("/service", cp.service)
|
cr.GET("/service", cp.service)
|
||||||
|
// TODO: 界面直接跳转使用该接口
|
||||||
|
cr.GET("/network/:id", cp.network)
|
||||||
|
cr.GET("/network", cp.network)
|
||||||
cr.GET("/ws", cp.ws)
|
cr.GET("/ws", cp.ws)
|
||||||
cr.POST("/terminal", cp.createTerminal)
|
cr.POST("/terminal", cp.createTerminal)
|
||||||
}
|
}
|
||||||
@ -127,6 +131,112 @@ func (p *commonPage) service(c *gin.Context) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cp *commonPage) network(c *gin.Context) {
|
||||||
|
var (
|
||||||
|
monitorHistory *model.MonitorHistory
|
||||||
|
servers []*model.Server
|
||||||
|
serverIdsWithMonitor []uint64
|
||||||
|
monitorInfos = []byte("{}")
|
||||||
|
id uint64
|
||||||
|
)
|
||||||
|
if len(singleton.SortedServerList) > 0 {
|
||||||
|
id = singleton.SortedServerList[0].ID
|
||||||
|
}
|
||||||
|
if err := singleton.DB.Model(&model.MonitorHistory{}).Select("monitor_id, server_id").
|
||||||
|
Where("monitor_id != 0 and server_id != 0").Limit(1).First(&monitorHistory).Error; err != nil {
|
||||||
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Title: "请求失败",
|
||||||
|
Msg: "请求参数有误:" + "server monitor history not found",
|
||||||
|
Link: "/",
|
||||||
|
Btn: "返回重试",
|
||||||
|
}, true)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
if monitorHistory == nil || monitorHistory.ServerID == 0 {
|
||||||
|
if len(singleton.SortedServerList) > 0 {
|
||||||
|
id = singleton.SortedServerList[0].ID
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id = monitorHistory.ServerID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
if idStr != "" {
|
||||||
|
var err error
|
||||||
|
id, err = strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Title: "请求失败",
|
||||||
|
Msg: "请求参数有误:" + err.Error(),
|
||||||
|
Link: "/",
|
||||||
|
Btn: "返回重试",
|
||||||
|
}, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, ok := singleton.ServerList[id]
|
||||||
|
if !ok {
|
||||||
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Title: "请求失败",
|
||||||
|
Msg: "请求参数有误:" + "server id not found",
|
||||||
|
Link: "/",
|
||||||
|
Btn: "返回重试",
|
||||||
|
}, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
monitorHistories := singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": id})
|
||||||
|
monitorInfos, _ = utils.Json.Marshal(monitorHistories)
|
||||||
|
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||||
|
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
|
||||||
|
|
||||||
|
if err := singleton.DB.Model(&model.MonitorHistory{}).
|
||||||
|
Select("distinct(server_id)").
|
||||||
|
Where("server_id != 0").
|
||||||
|
Find(&serverIdsWithMonitor).
|
||||||
|
Error; err != nil {
|
||||||
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Title: "请求失败",
|
||||||
|
Msg: "请求参数有误:" + "no server with monitor histories",
|
||||||
|
Link: "/",
|
||||||
|
Btn: "返回重试",
|
||||||
|
}, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isMember || isViewPasswordVerfied {
|
||||||
|
for _, server := range singleton.SortedServerList {
|
||||||
|
for _, id := range serverIdsWithMonitor {
|
||||||
|
if server.ID == id {
|
||||||
|
servers = append(servers, server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, server := range singleton.SortedServerListForGuest {
|
||||||
|
for _, id := range serverIdsWithMonitor {
|
||||||
|
if server.ID == id {
|
||||||
|
servers = append(servers, server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serversBytes, _ := utils.Json.Marshal(Data{
|
||||||
|
Now: time.Now().Unix() * 1000,
|
||||||
|
Servers: servers,
|
||||||
|
})
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/network", mygin.CommonEnvironment(c, gin.H{
|
||||||
|
"Servers": string(serversBytes),
|
||||||
|
"MonitorInfos": string(monitorInfos),
|
||||||
|
"CustomCode": singleton.Conf.Site.CustomCode,
|
||||||
|
"MaxTCPPingValue": singleton.Conf.MaxTCPPingValue,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
|
func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
|
||||||
v, err, _ := cp.requestGroup.Do("serverStats", func() (any, error) {
|
v, err, _ := cp.requestGroup.Do("serverStats", func() (any, error) {
|
||||||
singleton.SortedServerLock.RLock()
|
singleton.SortedServerLock.RLock()
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/naiba/nezha/model"
|
"github.com/naiba/nezha/model"
|
||||||
"github.com/naiba/nezha/pkg/mygin"
|
"github.com/naiba/nezha/pkg/mygin"
|
||||||
@ -185,7 +186,17 @@ func (ma *memberAPI) delete(c *gin.Context) {
|
|||||||
var err error
|
var err error
|
||||||
switch c.Param("model") {
|
switch c.Param("model") {
|
||||||
case "server":
|
case "server":
|
||||||
|
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
err = singleton.DB.Unscoped().Delete(&model.Server{}, "id = ?", id).Error
|
err = singleton.DB.Unscoped().Delete(&model.Server{}, "id = ?", id).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "server_id = ?", id).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// 删除服务器
|
// 删除服务器
|
||||||
singleton.ServerLock.Lock()
|
singleton.ServerLock.Lock()
|
||||||
@ -427,6 +438,11 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
|
|||||||
err = singleton.DB.Save(&m).Error
|
err = singleton.DB.Save(&m).Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 {
|
if err == nil {
|
||||||
err = singleton.ServiceSentinelShared.OnMonitorUpdate(m)
|
err = singleton.ServiceSentinelShared.OnMonitorUpdate(m)
|
||||||
|
@ -49,10 +49,20 @@ func DispatchTask(serviceSentinelDispatchBus <-chan model.Monitor) {
|
|||||||
workedServerIndex++
|
workedServerIndex++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// 找到合适机器执行任务,跳出循环
|
if task.Cover == model.MonitorCoverIgnoreAll && task.SkipServers[singleton.SortedServerList[workedServerIndex].ID] {
|
||||||
singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
|
singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
|
||||||
workedServerIndex++
|
workedServerIndex++
|
||||||
break
|
continue
|
||||||
|
}
|
||||||
|
if task.Cover == model.MonitorCoverAll && !task.SkipServers[singleton.SortedServerList[workedServerIndex].ID] {
|
||||||
|
singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
|
||||||
|
workedServerIndex++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 找到合适机器执行任务,跳出循环
|
||||||
|
// singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
|
||||||
|
// workedServerIndex++
|
||||||
|
// break
|
||||||
}
|
}
|
||||||
singleton.SortedServerLock.RUnlock()
|
singleton.SortedServerLock.RUnlock()
|
||||||
}
|
}
|
||||||
|
@ -110,6 +110,8 @@ type Config struct {
|
|||||||
|
|
||||||
v *viper.Viper
|
v *viper.Viper
|
||||||
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
|
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
|
||||||
|
MaxTCPPingValue int32
|
||||||
|
AvgPingCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read 读取配置文件并应用
|
// Read 读取配置文件并应用
|
||||||
@ -144,6 +146,12 @@ func (c *Config) Read(path string) error {
|
|||||||
if c.Location == "" {
|
if c.Location == "" {
|
||||||
c.Location = "Asia/Shanghai"
|
c.Location = "Asia/Shanghai"
|
||||||
}
|
}
|
||||||
|
if c.MaxTCPPingValue == 0 {
|
||||||
|
c.MaxTCPPingValue = 300
|
||||||
|
}
|
||||||
|
if c.AvgPingCount == 0 {
|
||||||
|
c.AvgPingCount = 2
|
||||||
|
}
|
||||||
|
|
||||||
c.updateIgnoredIPNotificationID()
|
c.updateIgnoredIPNotificationID()
|
||||||
return nil
|
return nil
|
||||||
|
@ -4,10 +4,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/naiba/nezha/pkg/utils"
|
|
||||||
pb "github.com/naiba/nezha/proto"
|
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
|
pb "github.com/naiba/nezha/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -1,9 +1,18 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
const (
|
||||||
|
Cycle = iota
|
||||||
|
Hour
|
||||||
|
Day
|
||||||
|
Week
|
||||||
|
Month
|
||||||
|
)
|
||||||
|
|
||||||
// MonitorHistory 历史监控记录
|
// MonitorHistory 历史监控记录
|
||||||
type MonitorHistory struct {
|
type MonitorHistory struct {
|
||||||
Common
|
Common
|
||||||
MonitorID uint64
|
MonitorID uint64
|
||||||
|
ServerID uint64
|
||||||
AvgDelay float32 // 平均延迟,毫秒
|
AvgDelay float32 // 平均延迟,毫秒
|
||||||
Up uint64 // 检查状态良好计数
|
Up uint64 // 检查状态良好计数
|
||||||
Down uint64 // 检查状态异常计数
|
Down uint64 // 检查状态异常计数
|
||||||
|
3
resource/l10n/en-US.toml
vendored
3
resource/l10n/en-US.toml
vendored
@ -609,3 +609,6 @@ other = "Hide for Guest"
|
|||||||
|
|
||||||
[Menu]
|
[Menu]
|
||||||
other = "Menu"
|
other = "Menu"
|
||||||
|
|
||||||
|
[NetworkSpiter]
|
||||||
|
other = "Network Monitor"
|
3
resource/l10n/es-ES.toml
vendored
3
resource/l10n/es-ES.toml
vendored
@ -609,3 +609,6 @@ other = "Ocultar para Invitados"
|
|||||||
|
|
||||||
[Menu]
|
[Menu]
|
||||||
other = "Menú"
|
other = "Menú"
|
||||||
|
|
||||||
|
[NetworkSpiter]
|
||||||
|
other = "Supervisión De Redes"
|
||||||
|
3
resource/l10n/zh-CN.toml
vendored
3
resource/l10n/zh-CN.toml
vendored
@ -609,3 +609,6 @@ other = "对游客隐藏"
|
|||||||
|
|
||||||
[Menu]
|
[Menu]
|
||||||
other = "菜单"
|
other = "菜单"
|
||||||
|
|
||||||
|
[NetworkSpiter]
|
||||||
|
other = "网络监控"
|
3
resource/l10n/zh-TW.toml
vendored
3
resource/l10n/zh-TW.toml
vendored
@ -609,3 +609,6 @@ other = "對遊客隱藏"
|
|||||||
|
|
||||||
[Menu]
|
[Menu]
|
||||||
other = "菜單"
|
other = "菜單"
|
||||||
|
|
||||||
|
[NetworkSpiter]
|
||||||
|
other = "網絡監控"
|
1
resource/template/common/menu.html
vendored
1
resource/template/common/menu.html
vendored
@ -15,6 +15,7 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<a class='item{{if eq .MatchedPath "/"}} active{{end}}' href="/"><i class="home icon"></i>{{tr "Home"}}</a>
|
<a class='item{{if eq .MatchedPath "/"}} active{{end}}' href="/"><i class="home icon"></i>{{tr "Home"}}</a>
|
||||||
<a class='item{{if eq .MatchedPath "/service"}} active{{end}}' href="/service"><i class="rss icon"></i>{{tr "Services"}}</a>
|
<a class='item{{if eq .MatchedPath "/service"}} active{{end}}' href="/service"><i class="rss icon"></i>{{tr "Services"}}</a>
|
||||||
|
<a class='item{{if eq .MatchedPath "/network"}} active{{end}}' href="/network"><i class="server icon"></i>{{tr "NetworkSpiter"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="right menu">
|
<div class="right menu">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
|
1
resource/template/theme-daynight/home.html
vendored
1
resource/template/theme-daynight/home.html
vendored
@ -39,6 +39,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">{{tr "Home"}}</a></li>
|
<li><a href="/">{{tr "Home"}}</a></li>
|
||||||
<li><a href="/service">{{tr "Services"}}</a></li>
|
<li><a href="/service">{{tr "Services"}}</a></li>
|
||||||
|
<li><a href="/network">{{tr "NetworkSpiter"}}</a></li>
|
||||||
{{if .Admin}}
|
{{if .Admin}}
|
||||||
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
|
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
268
resource/template/theme-daynight/network.html
vendored
Normal file
268
resource/template/theme-daynight/network.html
vendored
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
{{define "theme-daynight/network"}}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="{{.Conf.Language}}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/theme-daynight/css/main.css?v202108042286">
|
||||||
|
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
|
||||||
|
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
|
||||||
|
|
||||||
|
{{if ts .CustomCode}}
|
||||||
|
{{.CustomCode|safe}}
|
||||||
|
{{end}}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body data-theme="light" data-gridlist="grid">
|
||||||
|
<header>
|
||||||
|
<section class="nav-bar clearfix">
|
||||||
|
<figure class="logo">
|
||||||
|
<a href="/">
|
||||||
|
<img src="/static/logo.svg?v20210804" alt='{{tr "NezhaMonitoring"}}' width="50" height="50">
|
||||||
|
</a>
|
||||||
|
<a href="/">{{.Conf.Site.Brand}}</a>
|
||||||
|
</figure>
|
||||||
|
<div class="icon-container">
|
||||||
|
<div class="row cf">
|
||||||
|
<div class="three col">
|
||||||
|
<div class="hamburger" id="hamburger-icon"><span class="line"></span><span
|
||||||
|
class="line"></span><span class="line"></span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<ul>
|
||||||
|
<li><a href="/">{{tr "Home"}}</a></li>
|
||||||
|
<li><a href="/service">{{tr "Services"}}</a></li>
|
||||||
|
<li><a href="/network">{{tr "NetworkSpiter"}}</a></li>
|
||||||
|
{{if .Admin}}
|
||||||
|
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
|
||||||
|
{{else}}
|
||||||
|
<li><a href="/login">{{tr "Login"}}</a></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</section>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div id="network">
|
||||||
|
<div class="server-info-container" v-for='server in servers' :id="server.ID" style="font-size: .6em">
|
||||||
|
<div class="info-body" @click="redirectNetwork(server.ID)">
|
||||||
|
<ul class="server-info-body-container">
|
||||||
|
<li>
|
||||||
|
<h3>@#server.Name#@</h3>
|
||||||
|
</li>
|
||||||
|
<li><img :src="'/static/theme-daynight/img/flag/'+(server.Host&&server.Host.CountryCode?server.Host.CountryCode.toUpperCase():'CN')+'.png'"
|
||||||
|
:title="server.Host.CountryCode.toUpperCase()" /></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="network-chart" style="height: 800px;overflow: hidden">
|
||||||
|
<div id="monitor-info-container" style="height: 520px;max-width: 1400px">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-container">
|
||||||
|
<ul>
|
||||||
|
<li><i class="fas fa-sun" title='{{tr "LightMode"}}'></i><span>{{tr "LightMode"}}</span></li>
|
||||||
|
<li><i class="fas fa-moon" title='{{tr "DarkMode"}}'></i><span>{{tr "DarkMode"}}</span></li>
|
||||||
|
<li><i class="fas fa-th" title='{{tr "GridLayout"}}'></i><span>{{tr "GridLayout"}}</span></li>
|
||||||
|
<li><i class="fas fa-list-ul" title='{{tr "ListLayout"}}'></i><span>{{tr "ListLayout"}}</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<section class="dark-light-toggle">
|
||||||
|
<label class="switcher">
|
||||||
|
<input type="checkbox" name="theme" id="dark-light" />
|
||||||
|
<div>
|
||||||
|
<i class="fas fa-adjust"></i>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Back to top button -->
|
||||||
|
<a id="back-to-top"></a>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="footer-container">
|
||||||
|
<div><a href="https://github.com/naiba/nezha" target="_blank">Powered by {{tr "NezhaMonitoring"}} · {{.Version}}</a>
|
||||||
|
<p>© <span id="copyright-date">
|
||||||
|
<script>document.getElementById('copyright-date').appendChild(document.createTextNode(new Date().getFullYear()))</script>
|
||||||
|
</span> · <a href="https://blog.jackiesung.com" target="_blank">Theme designed by Jackie Sung</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/static/theme-daynight/js/main.js?v202102012266"></script>
|
||||||
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||||
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/limonte-sweetalert2/11.4.4/sweetalert2.all.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
|
||||||
|
const initData = JSON.parse('{{.Servers}}').servers;
|
||||||
|
let MaxTCPPingValue = {{.MaxTCPPingValue}};
|
||||||
|
if (MaxTCPPingValue == null) {
|
||||||
|
MaxTCPPingValue = 300;
|
||||||
|
}
|
||||||
|
// 基于准备好的dom,初始化echarts实例
|
||||||
|
var myChart = echarts.init(document.getElementById('monitor-info-container'));
|
||||||
|
// 使用刚指定的配置项和数据显示图表。
|
||||||
|
var statusCards = new Vue({
|
||||||
|
el: '#network',
|
||||||
|
delimiters: ['@#', '#@'],
|
||||||
|
data: {
|
||||||
|
servers: initData,
|
||||||
|
cache: [],
|
||||||
|
option: {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
position: function (pt) {
|
||||||
|
return [pt[0], '10%'];
|
||||||
|
},
|
||||||
|
formatter: function(params){
|
||||||
|
let result = params[0].axisValueLabel + "<br />";
|
||||||
|
params.forEach(function(item){
|
||||||
|
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
|
||||||
|
})
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
confine: true,
|
||||||
|
transitionDuration: 0
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
text: "",
|
||||||
|
textStyle: {}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: '5%',
|
||||||
|
data: [],
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
dataZoom: {
|
||||||
|
yAxisIndex: 'none'
|
||||||
|
},
|
||||||
|
restore: {},
|
||||||
|
saveAsImage: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
start: 94,
|
||||||
|
end: 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xAxis: {
|
||||||
|
type: 'time',
|
||||||
|
boundaryGap: false
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
boundaryGap: [0, '100%']
|
||||||
|
},
|
||||||
|
series: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.DarkMode();
|
||||||
|
this.parseMonitorInfo(monitorInfo);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
DarkMode() {
|
||||||
|
const hour = new Date(Date.now()).getHours()
|
||||||
|
if (hour > 17 || hour < 4) {
|
||||||
|
document.querySelector("input[name=theme]").checked = true;
|
||||||
|
document.getElementsByTagName("BODY")[0].setAttribute('data-theme', 'dark');
|
||||||
|
document.getElementById("monitor-info-container").style.backgroundColor = "#1E1E1E";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
redirectNetwork(id) {
|
||||||
|
this.getMonitorHistory(id)
|
||||||
|
.then(function(monitorInfo) {
|
||||||
|
var vm = network.__vue__;
|
||||||
|
vm.parseMonitorInfo(monitorInfo);
|
||||||
|
})
|
||||||
|
.catch(function(error){
|
||||||
|
window.location.href = "/404";
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getMonitorHistory(id) {
|
||||||
|
return $.ajax({
|
||||||
|
url: "/api/v1/monitor/"+id,
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
parseMonitorInfo(monitorInfo) {
|
||||||
|
let tSeries = [];
|
||||||
|
let tLegendData = [];
|
||||||
|
for (let i = 0; i < monitorInfo.result.length; i++) {
|
||||||
|
let loss = 0;
|
||||||
|
let data = [];
|
||||||
|
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
|
||||||
|
avgDelay = monitorInfo.result[i].avg_delay[j];
|
||||||
|
if (avgDelay > 0.9 * MaxTCPPingValue) {
|
||||||
|
loss += 1
|
||||||
|
}
|
||||||
|
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
|
||||||
|
}
|
||||||
|
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
|
||||||
|
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
|
||||||
|
tLegendData.push(legendName);
|
||||||
|
tSeries.push({
|
||||||
|
name: legendName,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.option.title.text = monitorInfo.result[0].server_name;
|
||||||
|
this.option.series = tSeries;
|
||||||
|
this.option.legend.data = tLegendData;
|
||||||
|
const hour = new Date(Date.now()).getHours()
|
||||||
|
if (hour > 17 || hour < 4) {
|
||||||
|
this.option.legend.textStyle.color = "#F1F1F2";
|
||||||
|
this.option.title.textStyle.color = "#ccc";
|
||||||
|
}
|
||||||
|
myChart.clear();
|
||||||
|
myChart.setOption(this.option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
<style>
|
||||||
|
#network {
|
||||||
|
display: grid;
|
||||||
|
/*grid-template-columns: repeat(5, 1fr);*/
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
|
grid-gap: 1em;
|
||||||
|
width: 100%;
|
||||||
|
width: calc(100vw - 6em);
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 1em auto;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
#monitor-info-container{
|
||||||
|
margin: 0em auto;
|
||||||
|
align-content: start;
|
||||||
|
background-color: #F1F1F2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
{{end}}
|
||||||
|
|
@ -43,6 +43,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">{{tr "Home"}}</a></li>
|
<li><a href="/">{{tr "Home"}}</a></li>
|
||||||
<li><a href="/service">{{tr "Services"}}</a></li>
|
<li><a href="/service">{{tr "Services"}}</a></li>
|
||||||
|
<li><a href="/network">{{tr "NetworkSpiter"}}</a></li>
|
||||||
{{if .Admin}}
|
{{if .Admin}}
|
||||||
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
|
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
219
resource/template/theme-default/network.html
vendored
Normal file
219
resource/template/theme-default/network.html
vendored
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
{{define "theme-default/network"}}
|
||||||
|
{{template "common/header" .}}
|
||||||
|
{{if ts .CustomCode}}
|
||||||
|
{{.CustomCode|safe}}
|
||||||
|
{{end}}
|
||||||
|
{{template "common/menu" .}}
|
||||||
|
<div class="nb-container" id="app">
|
||||||
|
<div class="ui container">
|
||||||
|
<div class="service-status">
|
||||||
|
<table class="ui celled table">
|
||||||
|
<button class="ui nezha-primary-btn button"
|
||||||
|
v-for="server in servers"
|
||||||
|
style="margin-top: 3px"
|
||||||
|
@click="redirectNetwork(server.ID)">
|
||||||
|
@#server.Name#@ <i :class="server.Host.CountryCode + ' flag'"></i>
|
||||||
|
</button>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui container" style="max-width: 95vw">
|
||||||
|
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "common/footer" .}}
|
||||||
|
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
|
||||||
|
const initData = JSON.parse('{{.Servers}}').servers;
|
||||||
|
let MaxTCPPingValue = {{.MaxTCPPingValue}};
|
||||||
|
if (MaxTCPPingValue == null) {
|
||||||
|
MaxTCPPingValue = 300;
|
||||||
|
}
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
delimiters: ['@#', '#@'],
|
||||||
|
data: {
|
||||||
|
servers: initData,
|
||||||
|
option: {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
position: function (pt) {
|
||||||
|
return [pt[0], '10%'];
|
||||||
|
},
|
||||||
|
formatter: function(params){
|
||||||
|
let result = params[0].axisValueLabel + "<br />";
|
||||||
|
params.forEach(function(item){
|
||||||
|
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
|
||||||
|
})
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
confine: true,
|
||||||
|
transitionDuration: 0
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
text: "",
|
||||||
|
textStyle: {}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: '5%',
|
||||||
|
data: [],
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
dataZoom: {
|
||||||
|
yAxisIndex: 'none'
|
||||||
|
},
|
||||||
|
restore: {},
|
||||||
|
saveAsImage: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
start: 94,
|
||||||
|
end: 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xAxis: {
|
||||||
|
type: 'time',
|
||||||
|
boundaryGap: false
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
boundaryGap: [0, '100%']
|
||||||
|
},
|
||||||
|
series: [],
|
||||||
|
},
|
||||||
|
chartOnOff: true,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.renderChart();
|
||||||
|
this.parseMonitorInfo(monitorInfo);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getFontLogoClass(str) {
|
||||||
|
if (["almalinux",
|
||||||
|
"alpine",
|
||||||
|
"aosc",
|
||||||
|
"apple",
|
||||||
|
"archlinux",
|
||||||
|
"archlabs",
|
||||||
|
"artix",
|
||||||
|
"budgie",
|
||||||
|
"centos",
|
||||||
|
"coreos",
|
||||||
|
"debian",
|
||||||
|
"deepin",
|
||||||
|
"devuan",
|
||||||
|
"docker",
|
||||||
|
"elementary",
|
||||||
|
"fedora",
|
||||||
|
"ferris",
|
||||||
|
"flathub",
|
||||||
|
"freebsd",
|
||||||
|
"gentoo",
|
||||||
|
"gnu-guix",
|
||||||
|
"illumos",
|
||||||
|
"kali-linux",
|
||||||
|
"linuxmint",
|
||||||
|
"mageia",
|
||||||
|
"mandriva",
|
||||||
|
"manjaro",
|
||||||
|
"nixos",
|
||||||
|
"openbsd",
|
||||||
|
"opensuse",
|
||||||
|
"pop-os",
|
||||||
|
"raspberry-pi",
|
||||||
|
"redhat",
|
||||||
|
"rocky-linux",
|
||||||
|
"sabayon",
|
||||||
|
"slackware",
|
||||||
|
"snappy",
|
||||||
|
"solus",
|
||||||
|
"tux",
|
||||||
|
"ubuntu",
|
||||||
|
"void",
|
||||||
|
"zorin"].indexOf(str)
|
||||||
|
> -1) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
if (['openwrt', 'linux', "immortalwrt"].indexOf(str) > -1) {
|
||||||
|
return 'tux';
|
||||||
|
}
|
||||||
|
if (str == 'amazon') {
|
||||||
|
return 'redhat';
|
||||||
|
}
|
||||||
|
if (str == 'arch') {
|
||||||
|
return 'archlinux';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
redirectNetwork(id) {
|
||||||
|
this.getMonitorHistory(id)
|
||||||
|
.then(function(monitorInfo) {
|
||||||
|
var vm = app.__vue__;
|
||||||
|
vm.parseMonitorInfo(monitorInfo);
|
||||||
|
})
|
||||||
|
.catch(function(error){
|
||||||
|
window.location.href = "/404";
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getMonitorHistory(id) {
|
||||||
|
return $.ajax({
|
||||||
|
url: "/api/v1/monitor/"+id,
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
parseMonitorInfo(monitorInfo) {
|
||||||
|
let tSeries = [];
|
||||||
|
let tLegendData = [];
|
||||||
|
for (let i = 0; i < monitorInfo.result.length; i++) {
|
||||||
|
let loss = 0;
|
||||||
|
let data = [];
|
||||||
|
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
|
||||||
|
avgDelay = monitorInfo.result[i].avg_delay[j];
|
||||||
|
if (avgDelay > 0.9 * MaxTCPPingValue) {
|
||||||
|
loss += 1
|
||||||
|
}
|
||||||
|
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
|
||||||
|
}
|
||||||
|
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
|
||||||
|
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
|
||||||
|
tLegendData.push(legendName);
|
||||||
|
tSeries.push({
|
||||||
|
name: legendName,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.option.title.text = monitorInfo.result[0].server_name;
|
||||||
|
this.option.series = tSeries;
|
||||||
|
this.option.legend.data = tLegendData;
|
||||||
|
this.myChart.clear();
|
||||||
|
this.myChart.setOption(this.option);
|
||||||
|
},
|
||||||
|
isWindowsPlatform(str) {
|
||||||
|
return str.includes('Windows')
|
||||||
|
},
|
||||||
|
renderChart() {
|
||||||
|
this.myChart = echarts.init(this.$refs.chartDom);
|
||||||
|
this.myChart.setOption(this.option);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.myChart.dispose();
|
||||||
|
this.myChart = null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{end}}
|
@ -18,6 +18,7 @@
|
|||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
<li><a href="/">{{tr "Home" }}</a></li>
|
<li><a href="/">{{tr "Home" }}</a></li>
|
||||||
<li><a href="/service">{{tr "Services" }}</a></li>
|
<li><a href="/service">{{tr "Services" }}</a></li>
|
||||||
|
<li><a href="/network">{{tr "NetworkSpiter" }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
@ -45,3 +46,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
<script src="/static/theme-server-status/js/bootstrap.min.js"></script>
|
<script src="/static/theme-server-status/js/bootstrap.min.js"></script>
|
||||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||||
<script src="/static/theme-server-status/js/mixin.js"></script>
|
<script src="/static/theme-server-status/js/mixin.js"></script>
|
||||||
|
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{end}}
|
{{end}}
|
217
resource/template/theme-server-status/network.html
vendored
Normal file
217
resource/template/theme-server-status/network.html
vendored
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
{{define "theme-server-status/network"}}
|
||||||
|
{{template "theme-server-status/header" .}}
|
||||||
|
<div id="app">
|
||||||
|
{{template "theme-server-status/content-nav" .}}
|
||||||
|
<div class="container table-responsive content" style="max-width: 95vw">
|
||||||
|
<table class="table table-striped table-condensed table-hover">
|
||||||
|
<button class="ui nezha-primary-btn button"
|
||||||
|
v-for="server in servers"
|
||||||
|
style="margin-top: 3px"
|
||||||
|
@click="redirectNetwork(server.ID)">
|
||||||
|
@#server.Name#@ <i :class="'fi fi-' + server.Host.CountryCode"></i><span class="node-cell-location-text text-uppercase"> @#server.Host.CountryCode#@</span>
|
||||||
|
</button>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="container table-responsive content" style="max-width: 95vw">
|
||||||
|
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
|
||||||
|
</div>
|
||||||
|
{{template "theme-server-status/content-footer" .}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
|
||||||
|
const initData = JSON.parse('{{.Servers}}').servers;
|
||||||
|
let MaxTCPPingValue = {{.MaxTCPPingValue}};
|
||||||
|
if (MaxTCPPingValue == null) {
|
||||||
|
MaxTCPPingValue = 300;
|
||||||
|
}
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
delimiters: ['@#', '#@'],
|
||||||
|
data: {
|
||||||
|
servers: initData,
|
||||||
|
option: {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
position: function (pt) {
|
||||||
|
return [pt[0], '10%'];
|
||||||
|
},
|
||||||
|
formatter: function(params){
|
||||||
|
let result = params[0].axisValueLabel + "<br />";
|
||||||
|
params.forEach(function(item){
|
||||||
|
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
|
||||||
|
})
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
confine: true,
|
||||||
|
transitionDuration: 0
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
text: "",
|
||||||
|
textStyle: {}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: '5%',
|
||||||
|
data: [],
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
dataZoom: {
|
||||||
|
yAxisIndex: 'none'
|
||||||
|
},
|
||||||
|
restore: {},
|
||||||
|
saveAsImage: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
start: 94,
|
||||||
|
end: 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xAxis: {
|
||||||
|
type: 'time',
|
||||||
|
boundaryGap: false
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
boundaryGap: [0, '100%']
|
||||||
|
},
|
||||||
|
series: [],
|
||||||
|
},
|
||||||
|
chartOnOff: true,
|
||||||
|
},
|
||||||
|
mixins: [mixinsVue],
|
||||||
|
created() {
|
||||||
|
this.initTheme();
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.renderChart();
|
||||||
|
this.parseMonitorInfo(monitorInfo);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getFontLogoClass(str) {
|
||||||
|
if (["almalinux",
|
||||||
|
"alpine",
|
||||||
|
"aosc",
|
||||||
|
"apple",
|
||||||
|
"archlinux",
|
||||||
|
"archlabs",
|
||||||
|
"artix",
|
||||||
|
"budgie",
|
||||||
|
"centos",
|
||||||
|
"coreos",
|
||||||
|
"debian",
|
||||||
|
"deepin",
|
||||||
|
"devuan",
|
||||||
|
"docker",
|
||||||
|
"elementary",
|
||||||
|
"fedora",
|
||||||
|
"ferris",
|
||||||
|
"flathub",
|
||||||
|
"freebsd",
|
||||||
|
"gentoo",
|
||||||
|
"gnu-guix",
|
||||||
|
"illumos",
|
||||||
|
"kali-linux",
|
||||||
|
"linuxmint",
|
||||||
|
"mageia",
|
||||||
|
"mandriva",
|
||||||
|
"manjaro",
|
||||||
|
"nixos",
|
||||||
|
"openbsd",
|
||||||
|
"opensuse",
|
||||||
|
"pop-os",
|
||||||
|
"raspberry-pi",
|
||||||
|
"redhat",
|
||||||
|
"rocky-linux",
|
||||||
|
"sabayon",
|
||||||
|
"slackware",
|
||||||
|
"snappy",
|
||||||
|
"solus",
|
||||||
|
"tux",
|
||||||
|
"ubuntu",
|
||||||
|
"void",
|
||||||
|
"zorin"].indexOf(str)
|
||||||
|
> -1) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
if (['openwrt', 'linux', "immortalwrt"].indexOf(str) > -1) {
|
||||||
|
return 'tux';
|
||||||
|
}
|
||||||
|
if (str == 'amazon') {
|
||||||
|
return 'redhat';
|
||||||
|
}
|
||||||
|
if (str == 'arch') {
|
||||||
|
return 'archlinux';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
redirectNetwork(id) {
|
||||||
|
this.getMonitorHistory(id)
|
||||||
|
.then(function(monitorInfo) {
|
||||||
|
var vm = app.__vue__;
|
||||||
|
vm.parseMonitorInfo(monitorInfo);
|
||||||
|
})
|
||||||
|
.catch(function(error){
|
||||||
|
window.location.href = "/404";
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getMonitorHistory(id) {
|
||||||
|
return $.ajax({
|
||||||
|
url: "/api/v1/monitor/"+id,
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
parseMonitorInfo(monitorInfo) {
|
||||||
|
let tSeries = [];
|
||||||
|
let tLegendData = [];
|
||||||
|
for (let i = 0; i < monitorInfo.result.length; i++) {
|
||||||
|
let loss = 0;
|
||||||
|
let data = [];
|
||||||
|
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
|
||||||
|
avgDelay = monitorInfo.result[i].avg_delay[j];
|
||||||
|
if (avgDelay > 0.9 * MaxTCPPingValue) {
|
||||||
|
loss += 1
|
||||||
|
}
|
||||||
|
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
|
||||||
|
}
|
||||||
|
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
|
||||||
|
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
|
||||||
|
tLegendData.push(legendName);
|
||||||
|
tSeries.push({
|
||||||
|
name: legendName,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.option.title.text = monitorInfo.result[0].server_name;
|
||||||
|
this.option.series = tSeries;
|
||||||
|
this.option.legend.data = tLegendData;
|
||||||
|
this.myChart.clear();
|
||||||
|
this.myChart.setOption(this.option);
|
||||||
|
},
|
||||||
|
isWindowsPlatform(str) {
|
||||||
|
return str.includes('Windows')
|
||||||
|
},
|
||||||
|
renderChart() {
|
||||||
|
this.myChart = echarts.init(this.$refs.chartDom);
|
||||||
|
this.myChart.setOption(this.option);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.myChart.dispose();
|
||||||
|
this.myChart = null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{template "theme-server-status/footer" .}}
|
||||||
|
{{end}}
|
||||||
|
|
@ -1,9 +1,11 @@
|
|||||||
package singleton
|
package singleton
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/naiba/nezha/model"
|
"github.com/naiba/nezha/model"
|
||||||
"github.com/naiba/nezha/pkg/utils"
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -12,6 +14,9 @@ var (
|
|||||||
ApiLock sync.RWMutex
|
ApiLock sync.RWMutex
|
||||||
|
|
||||||
ServerAPI = &ServerAPIService{}
|
ServerAPI = &ServerAPIService{}
|
||||||
|
MonitorAPI = &MonitorAPIService{}
|
||||||
|
|
||||||
|
once = &sync.Once{}
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerAPIService struct{}
|
type ServerAPIService struct{}
|
||||||
@ -51,6 +56,23 @@ type ServerInfoResponse struct {
|
|||||||
Result []*CommonServerInfo `json:"result"`
|
Result []*CommonServerInfo `json:"result"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MonitorAPIService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorInfoResponse struct {
|
||||||
|
CommonResponse
|
||||||
|
Result []*MonitorInfo `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorInfo struct {
|
||||||
|
MonitorID uint64 `json:"monitor_id"`
|
||||||
|
ServerID uint64 `json:"server_id"`
|
||||||
|
MonitorName string `json:"monitor_name"`
|
||||||
|
ServerName string `json:"server_name"`
|
||||||
|
CreatedAt []int64 `json:"created_at"`
|
||||||
|
AvgDelay []float32 `json:"avg_delay"`
|
||||||
|
}
|
||||||
|
|
||||||
func InitAPI() {
|
func InitAPI() {
|
||||||
ApiTokenList = make(map[string]*model.ApiToken)
|
ApiTokenList = make(map[string]*model.ApiToken)
|
||||||
UserIDToApiTokenList = make(map[uint64][]string)
|
UserIDToApiTokenList = make(map[uint64][]string)
|
||||||
@ -203,3 +225,45 @@ func (s *ServerAPIService) GetAllList() *ServerInfoResponse {
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MonitorAPIService) GetMonitorHistories(query map[string]any) *MonitorInfoResponse {
|
||||||
|
var (
|
||||||
|
resultMap = make(map[uint64]*MonitorInfo)
|
||||||
|
monitorHistories []*model.MonitorHistory
|
||||||
|
sortedMonitorIDs []uint64
|
||||||
|
)
|
||||||
|
res := &MonitorInfoResponse{
|
||||||
|
CommonResponse: CommonResponse{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := DB.Model(&model.MonitorHistory{}).Select("monitor_id, created_at, server_id, avg_delay").
|
||||||
|
Where(query).Where("created_at >= ?", time.Now().Add(-24*time.Hour)).Order("monitor_id, created_at").
|
||||||
|
Scan(&monitorHistories).Error; err != nil {
|
||||||
|
res.CommonResponse = CommonResponse{
|
||||||
|
Code: 500,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, history := range monitorHistories {
|
||||||
|
infos, ok := resultMap[history.MonitorID]
|
||||||
|
if !ok {
|
||||||
|
infos = &MonitorInfo{
|
||||||
|
MonitorID: history.MonitorID,
|
||||||
|
ServerID: history.ServerID,
|
||||||
|
MonitorName: ServiceSentinelShared.monitors[history.MonitorID].Name,
|
||||||
|
ServerName: ServerList[history.ServerID].Name,
|
||||||
|
}
|
||||||
|
resultMap[history.MonitorID] = infos
|
||||||
|
sortedMonitorIDs = append(sortedMonitorIDs, history.MonitorID)
|
||||||
|
}
|
||||||
|
infos.CreatedAt = append(infos.CreatedAt, history.CreatedAt.Truncate(time.Minute).Unix()*1000)
|
||||||
|
infos.AvgDelay = append(infos.AvgDelay, history.AvgDelay)
|
||||||
|
}
|
||||||
|
for _, monitorID := range sortedMonitorIDs {
|
||||||
|
res.Result = append(res.Result, resultMap[monitorID])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
@ -8,9 +8,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
|
||||||
"github.com/naiba/nezha/model"
|
"github.com/naiba/nezha/model"
|
||||||
pb "github.com/naiba/nezha/proto"
|
pb "github.com/naiba/nezha/proto"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -36,12 +37,13 @@ func NewServiceSentinel(serviceSentinelDispatchBus chan<- model.Monitor) {
|
|||||||
ServiceSentinelShared = &ServiceSentinel{
|
ServiceSentinelShared = &ServiceSentinel{
|
||||||
serviceReportChannel: make(chan ReportData, 200),
|
serviceReportChannel: make(chan ReportData, 200),
|
||||||
serviceStatusToday: make(map[uint64]*_TodayStatsOfMonitor),
|
serviceStatusToday: make(map[uint64]*_TodayStatsOfMonitor),
|
||||||
serviceCurrentStatusIndex: make(map[uint64]int),
|
serviceCurrentStatusIndex: make(map[uint64]*indexStore),
|
||||||
serviceCurrentStatusData: make(map[uint64][]*pb.TaskResult),
|
serviceCurrentStatusData: make(map[uint64][]*pb.TaskResult),
|
||||||
lastStatus: make(map[uint64]int),
|
lastStatus: make(map[uint64]int),
|
||||||
serviceResponseDataStoreCurrentUp: make(map[uint64]uint64),
|
serviceResponseDataStoreCurrentUp: make(map[uint64]uint64),
|
||||||
serviceResponseDataStoreCurrentDown: make(map[uint64]uint64),
|
serviceResponseDataStoreCurrentDown: make(map[uint64]uint64),
|
||||||
serviceResponseDataStoreCurrentAvgDelay: make(map[uint64]float32),
|
serviceResponseDataStoreCurrentAvgDelay: make(map[uint64]float32),
|
||||||
|
serviceResponsePing: make(map[uint64]map[uint64]*pingStore),
|
||||||
monitors: make(map[uint64]*model.Monitor),
|
monitors: make(map[uint64]*model.Monitor),
|
||||||
sslCertCache: make(map[uint64]string),
|
sslCertCache: make(map[uint64]string),
|
||||||
// 30天数据缓存
|
// 30天数据缓存
|
||||||
@ -95,11 +97,12 @@ type ServiceSentinel struct {
|
|||||||
|
|
||||||
serviceResponseDataStoreLock sync.RWMutex
|
serviceResponseDataStoreLock sync.RWMutex
|
||||||
serviceStatusToday map[uint64]*_TodayStatsOfMonitor // [monitor_id] -> _TodayStatsOfMonitor
|
serviceStatusToday map[uint64]*_TodayStatsOfMonitor // [monitor_id] -> _TodayStatsOfMonitor
|
||||||
serviceCurrentStatusIndex map[uint64]int // [monitor_id] -> 该监控ID对应的 serviceCurrentStatusData 的最新索引下标
|
serviceCurrentStatusIndex map[uint64]*indexStore // [monitor_id] -> 该监控ID对应的 serviceCurrentStatusData 的最新索引下标
|
||||||
serviceCurrentStatusData map[uint64][]*pb.TaskResult // [monitor_id] -> []model.MonitorHistory
|
serviceCurrentStatusData map[uint64][]*pb.TaskResult // [monitor_id] -> []model.MonitorHistory
|
||||||
serviceResponseDataStoreCurrentUp map[uint64]uint64 // [monitor_id] -> 当前服务在线计数
|
serviceResponseDataStoreCurrentUp map[uint64]uint64 // [monitor_id] -> 当前服务在线计数
|
||||||
serviceResponseDataStoreCurrentDown map[uint64]uint64 // [monitor_id] -> 当前服务离线计数
|
serviceResponseDataStoreCurrentDown map[uint64]uint64 // [monitor_id] -> 当前服务离线计数
|
||||||
serviceResponseDataStoreCurrentAvgDelay map[uint64]float32 // [monitor_id] -> 当前服务离线计数
|
serviceResponseDataStoreCurrentAvgDelay map[uint64]float32 // [monitor_id] -> 当前服务离线计数
|
||||||
|
serviceResponsePing map[uint64]map[uint64]*pingStore // [monitor_id] -> ClientID -> delay
|
||||||
lastStatus map[uint64]int
|
lastStatus map[uint64]int
|
||||||
sslCertCache map[uint64]string
|
sslCertCache map[uint64]string
|
||||||
|
|
||||||
@ -111,6 +114,16 @@ type ServiceSentinel struct {
|
|||||||
monthlyStatus map[uint64]*model.ServiceItemResponse // [monitor_id] -> model.ServiceItemResponse
|
monthlyStatus map[uint64]*model.ServiceItemResponse // [monitor_id] -> model.ServiceItemResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type indexStore struct {
|
||||||
|
index int
|
||||||
|
t time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type pingStore struct {
|
||||||
|
count int
|
||||||
|
ping float32
|
||||||
|
}
|
||||||
|
|
||||||
func (ss *ServiceSentinel) refreshMonthlyServiceStatus() {
|
func (ss *ServiceSentinel) refreshMonthlyServiceStatus() {
|
||||||
// 刷新数据防止无人访问
|
// 刷新数据防止无人访问
|
||||||
ss.LoadStats()
|
ss.LoadStats()
|
||||||
@ -326,6 +339,34 @@ func (ss *ServiceSentinel) worker() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mh := r.Data
|
mh := r.Data
|
||||||
|
if mh.Type == model.TaskTypeTCPPing || mh.Type == model.TaskTypeICMPPing {
|
||||||
|
monitorTcpMap, ok := ss.serviceResponsePing[mh.GetId()]
|
||||||
|
if !ok {
|
||||||
|
monitorTcpMap = make(map[uint64]*pingStore)
|
||||||
|
ss.serviceResponsePing[mh.GetId()] = monitorTcpMap
|
||||||
|
}
|
||||||
|
ts, ok := monitorTcpMap[r.Reporter]
|
||||||
|
if !ok {
|
||||||
|
ts = &pingStore{}
|
||||||
|
}
|
||||||
|
ts.count++
|
||||||
|
ts.ping = (ts.ping*float32(ts.count-1) + mh.Delay) / float32(ts.count)
|
||||||
|
if ts.count == Conf.AvgPingCount {
|
||||||
|
if ts.ping > float32(Conf.MaxTCPPingValue) {
|
||||||
|
ts.ping = float32(Conf.MaxTCPPingValue)
|
||||||
|
}
|
||||||
|
ts.count = 0
|
||||||
|
if err := DB.Create(&model.MonitorHistory{
|
||||||
|
MonitorID: mh.GetId(),
|
||||||
|
AvgDelay: ts.ping,
|
||||||
|
Data: mh.Data,
|
||||||
|
ServerID: r.Reporter,
|
||||||
|
}).Error; err != nil {
|
||||||
|
log.Println("NEZHA>> 服务监控数据持久化失败:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
monitorTcpMap[r.Reporter] = ts
|
||||||
|
}
|
||||||
ss.serviceResponseDataStoreLock.Lock()
|
ss.serviceResponseDataStoreLock.Lock()
|
||||||
// 写入当天状态
|
// 写入当天状态
|
||||||
if mh.Successful {
|
if mh.Successful {
|
||||||
@ -336,9 +377,20 @@ func (ss *ServiceSentinel) worker() {
|
|||||||
} else {
|
} else {
|
||||||
ss.serviceStatusToday[mh.GetId()].Down++
|
ss.serviceStatusToday[mh.GetId()].Down++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentTime := time.Now()
|
||||||
|
if ss.serviceCurrentStatusIndex[mh.GetId()] == nil {
|
||||||
|
ss.serviceCurrentStatusIndex[mh.GetId()] = &indexStore{
|
||||||
|
t: currentTime,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
// 写入当前数据
|
// 写入当前数据
|
||||||
ss.serviceCurrentStatusData[mh.GetId()][ss.serviceCurrentStatusIndex[mh.GetId()]] = mh
|
if ss.serviceCurrentStatusIndex[mh.GetId()].t.Before(currentTime) {
|
||||||
ss.serviceCurrentStatusIndex[mh.GetId()]++
|
ss.serviceCurrentStatusIndex[mh.GetId()].t = currentTime.Add(30 * time.Second)
|
||||||
|
ss.serviceCurrentStatusData[mh.GetId()][ss.serviceCurrentStatusIndex[mh.GetId()].index] = mh
|
||||||
|
ss.serviceCurrentStatusIndex[mh.GetId()].index++
|
||||||
|
}
|
||||||
|
|
||||||
// 更新当前状态
|
// 更新当前状态
|
||||||
ss.serviceResponseDataStoreCurrentUp[mh.GetId()] = 0
|
ss.serviceResponseDataStoreCurrentUp[mh.GetId()] = 0
|
||||||
@ -365,8 +417,11 @@ func (ss *ServiceSentinel) worker() {
|
|||||||
stateCode := GetStatusCode(upPercent)
|
stateCode := GetStatusCode(upPercent)
|
||||||
|
|
||||||
// 数据持久化
|
// 数据持久化
|
||||||
if ss.serviceCurrentStatusIndex[mh.GetId()] == _CurrentStatusSize {
|
if ss.serviceCurrentStatusIndex[mh.GetId()].index == _CurrentStatusSize {
|
||||||
ss.serviceCurrentStatusIndex[mh.GetId()] = 0
|
ss.serviceCurrentStatusIndex[mh.GetId()] = &indexStore{
|
||||||
|
index: 0,
|
||||||
|
t: currentTime,
|
||||||
|
}
|
||||||
if err := DB.Create(&model.MonitorHistory{
|
if err := DB.Create(&model.MonitorHistory{
|
||||||
MonitorID: mh.GetId(),
|
MonitorID: mh.GetId(),
|
||||||
AvgDelay: ss.serviceResponseDataStoreCurrentAvgDelay[mh.GetId()],
|
AvgDelay: ss.serviceResponseDataStoreCurrentAvgDelay[mh.GetId()],
|
||||||
|
@ -99,6 +99,10 @@ func RecordTransferHourlyUsage() {
|
|||||||
func CleanMonitorHistory() {
|
func CleanMonitorHistory() {
|
||||||
// 清理已被删除的服务器的监控记录与流量记录
|
// 清理已被删除的服务器的监控记录与流量记录
|
||||||
DB.Unscoped().Delete(&model.MonitorHistory{}, "created_at < ? OR monitor_id NOT IN (SELECT `id` FROM monitors)", time.Now().AddDate(0, 0, -30))
|
DB.Unscoped().Delete(&model.MonitorHistory{}, "created_at < ? OR monitor_id NOT IN (SELECT `id` FROM monitors)", time.Now().AddDate(0, 0, -30))
|
||||||
|
// 由于网络监控记录的数据较多,并且前端仅使用了 1 天的数据
|
||||||
|
// 考虑到 sqlite 数据量问题,仅保留一天数据,
|
||||||
|
// server_id = 0 的数据会用于/service页面的可用性展示
|
||||||
|
DB.Unscoped().Delete(&model.MonitorHistory{}, "(created_at < ? AND server_id != 0) OR monitor_id NOT IN (SELECT `id` FROM monitors)", time.Now().AddDate(0, 0, -1))
|
||||||
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (SELECT `id` FROM servers)")
|
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (SELECT `id` FROM servers)")
|
||||||
// 计算可清理流量记录的时长
|
// 计算可清理流量记录的时长
|
||||||
var allServerKeep time.Time
|
var allServerKeep time.Time
|
||||||
|
Loading…
Reference in New Issue
Block a user