Merge branch 'master' of https://github.com/Erope/nezha
This commit is contained in:
commit
e6385773ba
7
.github/workflows/agent.yml
vendored
7
.github/workflows/agent.yml
vendored
@ -10,6 +10,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Download UPX
|
||||
run: |
|
||||
wget https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz
|
||||
tar --strip-components=1 -xf upx-3.95-amd64_linux.tar.xz && sudo mv upx /usr/bin/
|
||||
git reset --hard
|
||||
git clean -f -d
|
||||
upx --version
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
1
.github/workflows/dashboard.yml
vendored
1
.github/workflows/dashboard.yml
vendored
@ -10,6 +10,7 @@ on:
|
||||
- "script/**"
|
||||
- "*.md"
|
||||
- ".*"
|
||||
- ".github/workflows/agent.yml"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
@ -15,11 +15,19 @@ builds:
|
||||
- 386
|
||||
- amd64
|
||||
- mips
|
||||
- mips64
|
||||
gomips:
|
||||
- softfloat
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
main: ./cmd/agent
|
||||
binary: nezha-agent
|
||||
# 为路由器、开发板缩小二进制体积
|
||||
hooks:
|
||||
post:
|
||||
- upx --best "{{.Path}}"
|
||||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
snapshot:
|
||||
|
52
README.md
52
README.md
@ -1,13 +1,14 @@
|
||||
<div align="center" style="background-color: white">
|
||||
<img width="500" style="max-width:100%" src="https://raw.githubusercontent.com/naiba/nezha/master/resource/static/brand.png" title="哪吒监控">
|
||||
<br><br>
|
||||
<img src="https://img.shields.io/github/workflow/status/naiba/nezha/Dashboard%20image?label=Dash%20v0.4.15&logo=github&style=for-the-badge"> <img src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/workflow/status/naiba/nezha/Agent%20release?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.4.10-brightgreen?style=for-the-badge&logo=linux">
|
||||
<img src="https://img.shields.io/github/workflow/status/naiba/nezha/Dashboard%20image?label=Dash%20v0.7.1&logo=github&style=for-the-badge"> <img src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/workflow/status/naiba/nezha/Agent%20release?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.5.0-brightgreen?style=for-the-badge&logo=linux">
|
||||
<br>
|
||||
<p>:trollface: 哪吒监控 一站式轻监控轻运维系统。支持系统状态、HTTP(SSL 证书变更、即将到期、到期)、TCP、Ping 监控报警,命令批量执行和计划任务。</p>
|
||||
</div>
|
||||
|
||||
\>> QQ 交流群: ~~955957790~~ 已解散,**自2021年3月26起不再提供技术支持,接受PR。**<br>
|
||||
\>> 交流论坛:正在选型搭建中…… 欢迎想建设社区的铁子与奶爸取得联系。
|
||||
\>> 交流论坛:[打杂社区](https://daza.net/c/nezha) (Lemmy)
|
||||
|
||||
\>> QQ 交流群:872069346 **加群要求:已搭建好哪吒监控 & 有 2+ 服务器**
|
||||
|
||||
\>> [我们的用户](https://www.google.com/search?q="powered+by+哪吒监控%7C哪吒面板"&filter=0) (Google)
|
||||
|
||||
@ -17,7 +18,7 @@
|
||||
|
||||
## 安装脚本
|
||||
|
||||
**推荐配置:** 安装前解析 _两个域名_ 到面板服务器,一个作为 _公开访问_ ,可以 **接入 CDN**,比如 (status.nai.ba);另外一个作为安装 Agent 时连接 Dashboard 使用,**不能接入 CDN** 直接暴露面板主机 IP,比如(randomdashboard.nai.ba)。
|
||||
**推荐配置:** 安装前准备 _两个域名_,一个可以 **接入 CDN** 作为 _公开访问_,比如 (status.nai.ba);另外一个解析到面板服务器作为 Agent 连接 Dashboard 使用,**不能接入 CDN** 直接暴露面板主机 IP,比如(randomdashboard.nai.ba)。
|
||||
|
||||
```shell
|
||||
curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install.sh -o nezha.sh && chmod +x nezha.sh
|
||||
@ -58,11 +59,13 @@ URL 里面也可放置占位符,请求时会进行简单的字符串替换。
|
||||
1. 添加通知方式
|
||||
|
||||
- server 酱示例
|
||||
|
||||
- 名称:server 酱
|
||||
- URL:https://sc.ftqq.com/SCUrandomkeys.send?text=#NEZHA#
|
||||
- 请求方式: GET
|
||||
- 请求类型: 默认
|
||||
- Body: 空
|
||||
|
||||
- wxpusher 示例,需要关注你的应用
|
||||
|
||||
- 名称: wxpusher
|
||||
@ -72,6 +75,7 @@ URL 里面也可放置占位符,请求时会进行简单的字符串替换。
|
||||
- Body: `{"appToken":"你的appToken","topicIds":[],"content":"#NEZHA#","contentType":"1","uids":["你的uid"]}`
|
||||
|
||||
- telegram 示例 [@haitau](https://github.com/haitau) 贡献
|
||||
|
||||
- 名称:telegram 机器人消息通知
|
||||
- URL:https://api.telegram.org/botXXXXXX/sendMessage?chat_id=YYYYYY&text=#NEZHA#
|
||||
- 请求方式: GET
|
||||
@ -121,7 +125,17 @@ URL 里面也可放置占位符,请求时会进行简单的字符串替换。
|
||||
</style>
|
||||
```
|
||||
|
||||
- 默认主题修改 LOGO、移除版权示例(来自 [@iLay1678](https://github.com/iLay1678),欢迎 PR)
|
||||
- DayNight 主题更改进度条颜色示例(来自 [@hyt-allen-xu](https://github.com/hyt-allen-xu))
|
||||
|
||||
```
|
||||
<style>
|
||||
.ui.fine.progress> .progress-bar {
|
||||
background-color: #00a7d0 !important;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
- 默认主题修改 LOGO、移除版权示例(来自 [@iLay1678](https://github.com/iLay1678))
|
||||
|
||||
```
|
||||
<style>
|
||||
@ -147,15 +161,27 @@ URL 里面也可放置占位符,请求时会进行简单的字符串替换。
|
||||
</script>
|
||||
```
|
||||
|
||||
- DayNight 移除版权示例(来自 [@hyt-allen-xu](https://github.com/hyt-allen-xu))
|
||||
|
||||
```
|
||||
<script>
|
||||
window.onload = function(){
|
||||
var footer=document.querySelector("div.footer-container")
|
||||
footer.innerHTML="©2021 你的名字 & Powered by 你的名字"
|
||||
footer.style.visibility="visible"
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
- hotaru 主题更改背景图片示例
|
||||
|
||||
```
|
||||
<style>
|
||||
.hotaru-cover {
|
||||
background: url(https://s3.ax1x.com/2020/12/08/DzHv6A.jpg) center;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
```
|
||||
<style>
|
||||
.hotaru-cover {
|
||||
background: url(https://s3.ax1x.com/2020/12/08/DzHv6A.jpg) center;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@ -253,4 +279,4 @@ restart() {
|
||||
- [哪吒探针 - Windows 客户端安装](https://nyko.me/2020/12/13/nezha-windows-client.html)
|
||||
- [哪吒监控,一个便携服务器状态监控面板搭建教程,不想拥有一个自己的探针吗?](https://haoduck.com/644.html)
|
||||
- [哪吒监控:小鸡们的最佳探针](https://www.zhujizixun.com/2843.html) _(已过时)_
|
||||
- [>>更多教程](https://www.google.com/search?q="哪吒"%2B"面板%7C监控%7C探针"+"教程") (Google)
|
||||
- [>>更多教程](https://www.google.com/search?q="哪吒"%2B"面板%7C监控%7C探针"+"教程") (Google)
|
@ -9,9 +9,9 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver"
|
||||
@ -35,11 +35,10 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
reporting bool
|
||||
client pb.NezhaServiceClient
|
||||
ctx = context.Background()
|
||||
delayWhenError = time.Second * 10 // Agent 重连间隔
|
||||
updateCh = make(chan struct{}, 0) // Agent 自动更新间隔
|
||||
delayWhenError = time.Second * 10 // Agent 重连间隔
|
||||
updateCh = make(chan struct{}) // Agent 自动更新间隔
|
||||
httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
@ -56,17 +55,16 @@ func doSelfUpdate() {
|
||||
updateCh <- struct{}{}
|
||||
}()
|
||||
v := semver.MustParse(version)
|
||||
log.Println("Check update", v)
|
||||
println("Check update", v)
|
||||
latest, err := selfupdate.UpdateSelf(v, "naiba/nezha")
|
||||
if err != nil {
|
||||
log.Println("Binary update failed:", err)
|
||||
println("Binary update failed:", err)
|
||||
return
|
||||
}
|
||||
if latest.Version.Equals(v) {
|
||||
// latest version is the same as current version. It means current binary is up to date.
|
||||
log.Println("Current binary is the latest version", version)
|
||||
println("Current binary is up to date", version)
|
||||
} else {
|
||||
log.Println("Successfully updated to version", latest.Version)
|
||||
println("Upgrade successfully", latest.Version)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@ -81,7 +79,7 @@ func main() {
|
||||
|
||||
var debug bool
|
||||
flag.String("i", "", "unused 旧Agent配置兼容")
|
||||
flag.BoolVar(&debug, "d", false, "允许不安全连接")
|
||||
flag.BoolVar(&debug, "d", false, "开启调试信息")
|
||||
flag.StringVar(&server, "s", "localhost:5555", "管理面板RPC端口")
|
||||
flag.StringVar(&clientSecret, "p", "", "Agent连接Secret")
|
||||
flag.Parse()
|
||||
@ -121,18 +119,18 @@ func run() {
|
||||
var conn *grpc.ClientConn
|
||||
|
||||
retry := func() {
|
||||
log.Println("Error to close connection ...")
|
||||
println("Error to close connection ...")
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
time.Sleep(delayWhenError)
|
||||
log.Println("Try to reconnect ...")
|
||||
println("Try to reconnect ...")
|
||||
}
|
||||
|
||||
for {
|
||||
conn, err = grpc.Dial(server, grpc.WithInsecure(), grpc.WithPerRPCCredentials(&auth))
|
||||
if err != nil {
|
||||
log.Printf("grpc.Dial err: %v", err)
|
||||
println("grpc.Dial err: ", err)
|
||||
retry()
|
||||
continue
|
||||
}
|
||||
@ -140,26 +138,26 @@ func run() {
|
||||
// 第一步注册
|
||||
_, err = client.ReportSystemInfo(ctx, monitor.GetHost().PB())
|
||||
if err != nil {
|
||||
log.Printf("client.ReportSystemInfo err: %v", err)
|
||||
println("client.ReportSystemInfo err: ", err)
|
||||
retry()
|
||||
continue
|
||||
}
|
||||
// 执行 Task
|
||||
tasks, err := client.RequestTask(ctx, monitor.GetHost().PB())
|
||||
if err != nil {
|
||||
log.Printf("client.RequestTask err: %v", err)
|
||||
println("client.RequestTask err: ", err)
|
||||
retry()
|
||||
continue
|
||||
}
|
||||
err = receiveTasks(tasks)
|
||||
log.Printf("receiveTasks exit to main: %v", err)
|
||||
println("receiveTasks exit to main: ", err)
|
||||
retry()
|
||||
}
|
||||
}
|
||||
|
||||
func receiveTasks(tasks pb.NezhaService_RequestTaskClient) error {
|
||||
var err error
|
||||
defer log.Printf("receiveTasks exit %v => %v", time.Now(), err)
|
||||
defer println("receiveTasks exit", time.Now(), "=>", err)
|
||||
for {
|
||||
var task *pb.Task
|
||||
task, err = tasks.Recv()
|
||||
@ -179,24 +177,32 @@ func doTask(task *pb.Task) {
|
||||
start := time.Now()
|
||||
resp, err := httpClient.Get(task.GetData())
|
||||
if err == nil {
|
||||
result.Delay = float32(time.Now().Sub(start).Microseconds()) / 1000.0
|
||||
// 检查 HTTP Response 状态
|
||||
result.Delay = float32(time.Since(start).Microseconds()) / 1000.0
|
||||
if resp.StatusCode > 399 || resp.StatusCode < 200 {
|
||||
err = errors.New("\n应用错误:" + resp.Status)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
if strings.HasPrefix(task.GetData(), "https://") {
|
||||
c := cert.NewCert(task.GetData()[8:])
|
||||
if c.Error != "" {
|
||||
result.Data = "SSL证书错误:" + c.Error
|
||||
// 检查 SSL 证书信息
|
||||
serviceUrl, err := url.Parse(task.GetData())
|
||||
if err == nil {
|
||||
if serviceUrl.Scheme == "https" {
|
||||
c := cert.NewCert(serviceUrl.Host)
|
||||
if c.Error != "" {
|
||||
result.Data = "SSL证书错误:" + c.Error
|
||||
} else {
|
||||
result.Data = c.Issuer + "|" + c.NotAfter
|
||||
result.Successful = true
|
||||
}
|
||||
} else {
|
||||
result.Data = c.Issuer + "|" + c.NotAfter
|
||||
result.Successful = true
|
||||
}
|
||||
} else {
|
||||
result.Successful = true
|
||||
result.Data = "URL解析错误:" + err.Error()
|
||||
}
|
||||
} else {
|
||||
// HTTP 请求失败
|
||||
result.Data = err.Error()
|
||||
}
|
||||
case model.TaskTypeICMPPing:
|
||||
@ -219,7 +225,7 @@ func doTask(task *pb.Task) {
|
||||
if err == nil {
|
||||
conn.Write([]byte("ping\n"))
|
||||
conn.Close()
|
||||
result.Delay = float32(time.Now().Sub(start).Microseconds()) / 1000.0
|
||||
result.Delay = float32(time.Since(start).Microseconds()) / 1000.0
|
||||
result.Successful = true
|
||||
} else {
|
||||
result.Data = err.Error()
|
||||
@ -260,9 +266,9 @@ func doTask(task *pb.Task) {
|
||||
result.Data = string(output)
|
||||
result.Successful = true
|
||||
}
|
||||
result.Delay = float32(time.Now().Sub(startedAt).Seconds())
|
||||
result.Delay = float32(time.Since(startedAt).Seconds())
|
||||
default:
|
||||
log.Printf("Unknown action: %v", task)
|
||||
println("Unknown action: ", task)
|
||||
}
|
||||
client.ReportTask(ctx, &result)
|
||||
}
|
||||
@ -270,13 +276,13 @@ func doTask(task *pb.Task) {
|
||||
func reportState() {
|
||||
var lastReportHostInfo time.Time
|
||||
var err error
|
||||
defer log.Printf("reportState exit %v => %v", time.Now(), err)
|
||||
defer println("reportState exit", time.Now(), "=>", err)
|
||||
for {
|
||||
if client != nil {
|
||||
monitor.TrackNetworkSpeed()
|
||||
_, err = client.ReportSystemState(ctx, monitor.GetState(dao.ReportDelay).PB())
|
||||
if err != nil {
|
||||
log.Printf("reportState error %v", err)
|
||||
println("reportState error", err)
|
||||
time.Sleep(delayWhenError)
|
||||
}
|
||||
if lastReportHostInfo.Before(time.Now().Add(-10 * time.Minute)) {
|
||||
@ -286,3 +292,9 @@ func reportState() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func println(v ...interface{}) {
|
||||
if dao.Conf.Debug {
|
||||
log.Println(v...)
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package monitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@ -17,12 +18,19 @@ import (
|
||||
)
|
||||
|
||||
var netInSpeed, netOutSpeed, netInTransfer, netOutTransfer, lastUpdate uint64
|
||||
var expectDiskFsTypes = []string{
|
||||
"apfs", "ext4", "ext3", "ext2", "f2fs", "reiserfs", "jfs", "btrfs", "fuseblk", "zfs", "simfs", "ntfs", "fat32", "exfat", "xfs",
|
||||
}
|
||||
var excludeNetInterfaces = []string{
|
||||
"lo", "tun", "docker", "veth", "br-", "vmbr", "vnet", "kube",
|
||||
}
|
||||
var getMacDiskNo = regexp.MustCompile(`\/dev\/disk(\d)s.*`)
|
||||
|
||||
func GetHost() *model.Host {
|
||||
hi, _ := host.Info()
|
||||
var cpuType string
|
||||
if hi.VirtualizationSystem != "" {
|
||||
cpuType = "Virtual"
|
||||
cpuType = "Vrtual"
|
||||
} else {
|
||||
cpuType = "Physical"
|
||||
}
|
||||
@ -37,15 +45,15 @@ func GetHost() *model.Host {
|
||||
}
|
||||
mv, _ := mem.VirtualMemory()
|
||||
ms, _ := mem.SwapMemory()
|
||||
u, _ := disk.Usage("/")
|
||||
diskTotal, _ := getDiskTotalAndUsed()
|
||||
|
||||
return &model.Host{
|
||||
Platform: hi.OS,
|
||||
PlatformVersion: hi.PlatformVersion,
|
||||
CPU: cpus,
|
||||
MemTotal: mv.Total,
|
||||
DiskTotal: u.Total,
|
||||
SwapTotal: ms.Total,
|
||||
DiskTotal: diskTotal,
|
||||
Arch: hi.KernelArch,
|
||||
Virtualization: hi.VirtualizationSystem,
|
||||
BootTime: hi.BootTime,
|
||||
@ -57,23 +65,19 @@ func GetHost() *model.Host {
|
||||
|
||||
func GetState(delay int64) *model.HostState {
|
||||
hi, _ := host.Info()
|
||||
// Memory
|
||||
mv, _ := mem.VirtualMemory()
|
||||
ms, _ := mem.SwapMemory()
|
||||
// CPU
|
||||
var cpuPercent float64
|
||||
cp, err := cpu.Percent(time.Second*time.Duration(delay), false)
|
||||
if err == nil {
|
||||
cpuPercent = cp[0]
|
||||
}
|
||||
// Disk
|
||||
u, _ := disk.Usage("/")
|
||||
|
||||
_, diskUsed := getDiskTotalAndUsed()
|
||||
return &model.HostState{
|
||||
CPU: cpuPercent,
|
||||
MemUsed: mv.Used,
|
||||
SwapUsed: ms.Used,
|
||||
DiskUsed: u.Used,
|
||||
DiskUsed: diskUsed,
|
||||
NetInTransfer: atomic.LoadUint64(&netInTransfer),
|
||||
NetOutTransfer: atomic.LoadUint64(&netOutTransfer),
|
||||
NetInSpeed: atomic.LoadUint64(&netInSpeed),
|
||||
@ -84,10 +88,15 @@ func GetState(delay int64) *model.HostState {
|
||||
|
||||
func TrackNetworkSpeed() {
|
||||
var innerNetInTransfer, innerNetOutTransfer uint64
|
||||
nc, err := net.IOCounters(false)
|
||||
nc, err := net.IOCounters(true)
|
||||
if err == nil {
|
||||
innerNetInTransfer += nc[0].BytesRecv
|
||||
innerNetOutTransfer += nc[0].BytesSent
|
||||
for _, v := range nc {
|
||||
if isListContainsStr(excludeNetInterfaces, v.Name) {
|
||||
continue
|
||||
}
|
||||
innerNetInTransfer += v.BytesRecv
|
||||
innerNetOutTransfer += v.BytesSent
|
||||
}
|
||||
now := uint64(time.Now().Unix())
|
||||
diff := now - atomic.LoadUint64(&lastUpdate)
|
||||
if diff > 0 {
|
||||
@ -99,3 +108,40 @@ func TrackNetworkSpeed() {
|
||||
atomic.StoreUint64(&lastUpdate, now)
|
||||
}
|
||||
}
|
||||
|
||||
func getDiskTotalAndUsed() (total uint64, used uint64) {
|
||||
diskList, _ := disk.Partitions(false)
|
||||
devices := make(map[string]string)
|
||||
countedDiskForMac := make(map[string]struct{})
|
||||
for _, d := range diskList {
|
||||
fsType := strings.ToLower(d.Fstype)
|
||||
// 不统计 K8s 的虚拟挂载点:https://github.com/shirou/gopsutil/issues/1007
|
||||
if devices[d.Device] == "" && isListContainsStr(expectDiskFsTypes, fsType) && !strings.Contains(d.Mountpoint, "/var/lib/kubelet") {
|
||||
devices[d.Device] = d.Mountpoint
|
||||
}
|
||||
}
|
||||
for device, mountPath := range devices {
|
||||
diskUsageOf, _ := disk.Usage(mountPath)
|
||||
// 这里是针对 Mac 机器的处理,https://github.com/giampaolo/psutil/issues/1509
|
||||
matches := getMacDiskNo.FindStringSubmatch(device)
|
||||
if len(matches) == 2 {
|
||||
if _, has := countedDiskForMac[matches[1]]; !has {
|
||||
countedDiskForMac[matches[1]] = struct{}{}
|
||||
total += diskUsageOf.Total
|
||||
}
|
||||
} else {
|
||||
total += diskUsageOf.Total
|
||||
}
|
||||
used += diskUsageOf.Used
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func isListContainsStr(list []string, str string) bool {
|
||||
for i := 0; i < len(list); i++ {
|
||||
if strings.Contains(list[i], str) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package controller
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@ -80,79 +79,8 @@ func (p *commonPage) checkViewPassword(c *gin.Context) {
|
||||
c.Next()
|
||||
}
|
||||
|
||||
type ServiceItem struct {
|
||||
Monitor model.Monitor
|
||||
TotalUp uint64
|
||||
TotalDown uint64
|
||||
CurrentUp uint64
|
||||
CurrentDown uint64
|
||||
Delay *[30]float32
|
||||
Up *[30]int
|
||||
Down *[30]int
|
||||
}
|
||||
|
||||
func (p *commonPage) service(c *gin.Context) {
|
||||
var msm map[uint64]*ServiceItem
|
||||
|
||||
var cached bool
|
||||
if _, has := c.Get(model.CtxKeyAuthorizedUser); !has {
|
||||
data, has := dao.Cache.Get(model.CacheKeyServicePage)
|
||||
if has {
|
||||
log.Println("use cache")
|
||||
msm = data.(map[uint64]*ServiceItem)
|
||||
cached = true
|
||||
}
|
||||
}
|
||||
|
||||
if !cached {
|
||||
msm = make(map[uint64]*ServiceItem)
|
||||
var ms []model.Monitor
|
||||
dao.DB.Find(&ms)
|
||||
year, month, day := time.Now().Date()
|
||||
today := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
|
||||
var mhs []model.MonitorHistory
|
||||
dao.DB.Where("created_at >= ?", today.AddDate(0, 0, -29)).Find(&mhs)
|
||||
|
||||
for i := 0; i < len(ms); i++ {
|
||||
msm[ms[i].ID] = &ServiceItem{
|
||||
Monitor: ms[i],
|
||||
Delay: &[30]float32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
Up: &[30]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
Down: &[30]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
}
|
||||
}
|
||||
// 整合数据
|
||||
todayStatus := make(map[uint64][]bool)
|
||||
for i := 0; i < len(mhs); i++ {
|
||||
dayIndex := 29
|
||||
if mhs[i].CreatedAt.Before(today) {
|
||||
dayIndex = 28 - (int(today.Sub(mhs[i].CreatedAt).Hours()) / 24)
|
||||
} else {
|
||||
todayStatus[mhs[i].MonitorID] = append(todayStatus[mhs[i].MonitorID], mhs[i].Successful)
|
||||
}
|
||||
if mhs[i].Successful {
|
||||
msm[mhs[i].MonitorID].TotalUp++
|
||||
msm[mhs[i].MonitorID].Delay[dayIndex] = (msm[mhs[i].MonitorID].Delay[dayIndex]*float32(msm[mhs[i].MonitorID].Up[dayIndex]) + mhs[i].Delay) / float32(msm[mhs[i].MonitorID].Up[dayIndex]+1)
|
||||
msm[mhs[i].MonitorID].Up[dayIndex]++
|
||||
} else {
|
||||
msm[mhs[i].MonitorID].TotalDown++
|
||||
msm[mhs[i].MonitorID].Down[dayIndex]++
|
||||
}
|
||||
}
|
||||
// 当日最后 20 个采样作为当前状态
|
||||
for _, m := range msm {
|
||||
for i := len(todayStatus[m.Monitor.ID]) - 1; i >= 0 && i >= (len(todayStatus[m.Monitor.ID])-1-20); i-- {
|
||||
if todayStatus[m.Monitor.ID][i] {
|
||||
m.CurrentUp++
|
||||
} else {
|
||||
m.CurrentDown++
|
||||
}
|
||||
}
|
||||
}
|
||||
// 未登录人员缓存十分钟
|
||||
dao.Cache.Set(model.CacheKeyServicePage, msm, time.Minute*10)
|
||||
}
|
||||
|
||||
msm := dao.ServiceSentinelShared.LoadStats()
|
||||
c.HTML(http.StatusOK, "theme-"+dao.Conf.Site.Theme+"/service", mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": "服务状态",
|
||||
"Services": msm,
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"code.cloudfoundry.org/bytefmt"
|
||||
"github.com/gin-contrib/pprof"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/naiba/nezha/pkg/mygin"
|
||||
@ -15,10 +16,11 @@ import (
|
||||
|
||||
func ServeWeb(port uint) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.Default()
|
||||
if dao.Conf.Debug {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
pprof.Register(r)
|
||||
}
|
||||
r := gin.Default()
|
||||
r.Use(mygin.RecordPath)
|
||||
r.SetFuncMap(template.FuncMap{
|
||||
"tf": func(t time.Time) string {
|
||||
@ -54,7 +56,7 @@ func ServeWeb(port uint) {
|
||||
}
|
||||
if a == 0 {
|
||||
// 这是从未在线的情况
|
||||
return 1 / float32(b) * 100
|
||||
return 0.00001 / float32(b) * 100
|
||||
}
|
||||
return float32(a) / float32(b) * 100
|
||||
},
|
||||
@ -67,7 +69,7 @@ func ServeWeb(port uint) {
|
||||
}
|
||||
if a == 0 {
|
||||
// 这是从未在线的情况
|
||||
return 1 / float32(b) * 100
|
||||
return 0.00001 / float32(b) * 100
|
||||
}
|
||||
return float32(a) / float32(b) * 100
|
||||
},
|
||||
|
@ -7,8 +7,6 @@ import (
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/pkg/mygin"
|
||||
"github.com/naiba/nezha/service/dao"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/github"
|
||||
)
|
||||
|
||||
type guestPage struct {
|
||||
@ -27,31 +25,22 @@ func (gp *guestPage) serve() {
|
||||
|
||||
gr.GET("/login", gp.login)
|
||||
|
||||
var endPoint oauth2.Endpoint
|
||||
|
||||
if dao.Conf.Oauth2.Type == model.ConfigTypeGitee {
|
||||
endPoint = oauth2.Endpoint{
|
||||
AuthURL: "https://gitee.com/oauth/authorize",
|
||||
TokenURL: "https://gitee.com/oauth/token",
|
||||
}
|
||||
} else {
|
||||
endPoint = github.Endpoint
|
||||
}
|
||||
|
||||
oauth := &oauth2controller{
|
||||
oauth2Config: &oauth2.Config{
|
||||
ClientID: dao.Conf.Oauth2.ClientID,
|
||||
ClientSecret: dao.Conf.Oauth2.ClientSecret,
|
||||
Scopes: []string{},
|
||||
Endpoint: endPoint,
|
||||
},
|
||||
r: gr,
|
||||
}
|
||||
oauth.serve()
|
||||
}
|
||||
|
||||
func (gp *guestPage) login(c *gin.Context) {
|
||||
LoginType := "GitHub"
|
||||
RegistrationLink := "https://github.com/join"
|
||||
if dao.Conf.Oauth2.Type == model.ConfigTypeGitee {
|
||||
LoginType = "Gitee"
|
||||
RegistrationLink = "https://gitee.com/signup"
|
||||
}
|
||||
c.HTML(http.StatusOK, "dashboard/login", mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": "登录",
|
||||
"Title": "登录",
|
||||
"LoginType": LoginType,
|
||||
"RegistrationLink": RegistrationLink,
|
||||
}))
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -73,6 +74,7 @@ func (ma *memberAPI) delete(c *gin.Context) {
|
||||
case "monitor":
|
||||
err = dao.DB.Delete(&model.Monitor{}, "id = ?", id).Error
|
||||
if err == nil {
|
||||
dao.ServiceSentinelShared.OnMonitorDelete(id)
|
||||
err = dao.DB.Delete(&model.MonitorHistory{}, "monitor_id = ?", id).Error
|
||||
}
|
||||
case "cron":
|
||||
@ -190,10 +192,12 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
||||
}
|
||||
|
||||
type monitorForm struct {
|
||||
ID uint64
|
||||
Name string
|
||||
Target string
|
||||
Type uint8
|
||||
ID uint64
|
||||
Name string
|
||||
Target string
|
||||
Type uint8
|
||||
Notify string
|
||||
SkipServersRaw string
|
||||
}
|
||||
|
||||
func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
|
||||
@ -202,9 +206,11 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
|
||||
err := c.ShouldBindJSON(&mf)
|
||||
if err == nil {
|
||||
m.Name = mf.Name
|
||||
m.Target = mf.Target
|
||||
m.Target = strings.TrimSpace(mf.Target)
|
||||
m.Type = mf.Type
|
||||
m.ID = mf.ID
|
||||
m.SkipServersRaw = mf.SkipServersRaw
|
||||
m.Notify = mf.Notify == "on"
|
||||
}
|
||||
if err == nil {
|
||||
if m.ID == 0 {
|
||||
@ -219,6 +225,8 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
|
||||
Message: fmt.Sprintf("请求错误:%s", err),
|
||||
})
|
||||
return
|
||||
} else {
|
||||
dao.ServiceSentinelShared.OnMonitorUpdate()
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: http.StatusOK,
|
||||
|
@ -39,11 +39,9 @@ func (mp *memberPage) server(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (mp *memberPage) monitor(c *gin.Context) {
|
||||
var monitors []model.Monitor
|
||||
dao.DB.Find(&monitors)
|
||||
c.HTML(http.StatusOK, "dashboard/monitor", mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": "服务监控",
|
||||
"Monitors": monitors,
|
||||
"Monitors": dao.ServiceSentinelShared.Monitors(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@ -12,6 +11,7 @@ import (
|
||||
"github.com/google/go-github/github"
|
||||
GitHubAPI "github.com/google/go-github/github"
|
||||
"golang.org/x/oauth2"
|
||||
GitHubOauth2 "golang.org/x/oauth2/github"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/pkg/mygin"
|
||||
@ -20,8 +20,7 @@ import (
|
||||
)
|
||||
|
||||
type oauth2controller struct {
|
||||
oauth2Config *oauth2.Config
|
||||
r gin.IRoutes
|
||||
r gin.IRoutes
|
||||
}
|
||||
|
||||
func (oa *oauth2controller) serve() {
|
||||
@ -29,39 +28,58 @@ func (oa *oauth2controller) serve() {
|
||||
oa.r.GET("/oauth2/callback", oa.callback)
|
||||
}
|
||||
|
||||
func (oa *oauth2controller) fillRedirectURL(c *gin.Context) {
|
||||
func (oa *oauth2controller) getCommonOauth2Config(c *gin.Context) *oauth2.Config {
|
||||
var endPoint oauth2.Endpoint
|
||||
if dao.Conf.Oauth2.Type == model.ConfigTypeGitee {
|
||||
endPoint = oauth2.Endpoint{
|
||||
AuthURL: "https://gitee.com/oauth/authorize",
|
||||
TokenURL: "https://gitee.com/oauth/token",
|
||||
}
|
||||
} else {
|
||||
endPoint = GitHubOauth2.Endpoint
|
||||
}
|
||||
|
||||
return &oauth2.Config{
|
||||
ClientID: dao.Conf.Oauth2.ClientID,
|
||||
ClientSecret: dao.Conf.Oauth2.ClientSecret,
|
||||
Scopes: []string{},
|
||||
Endpoint: endPoint,
|
||||
RedirectURL: oa.getRedirectURL(c),
|
||||
}
|
||||
}
|
||||
|
||||
func (oa *oauth2controller) getRedirectURL(c *gin.Context) string {
|
||||
schame := "http://"
|
||||
if strings.HasPrefix(c.Request.Referer(), "https://") {
|
||||
schame = "https://"
|
||||
}
|
||||
oa.oauth2Config.RedirectURL = schame + c.Request.Host + "/oauth2/callback"
|
||||
return schame + c.Request.Host + "/oauth2/callback"
|
||||
}
|
||||
|
||||
func (oa *oauth2controller) login(c *gin.Context) {
|
||||
oa.fillRedirectURL(c)
|
||||
state := utils.RandStringBytesMaskImprSrcUnsafe(6)
|
||||
dao.Cache.Set(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, c.ClientIP()), state, 0)
|
||||
url := oa.oauth2Config.AuthCodeURL(state, oauth2.AccessTypeOnline)
|
||||
url := oa.getCommonOauth2Config(c).AuthCodeURL(state, oauth2.AccessTypeOnline)
|
||||
c.Redirect(http.StatusFound, url)
|
||||
}
|
||||
|
||||
func (oa *oauth2controller) callback(c *gin.Context) {
|
||||
oa.fillRedirectURL(c)
|
||||
var err error
|
||||
// 验证登录跳转时的 State
|
||||
state, ok := dao.Cache.Get(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, c.ClientIP()))
|
||||
if !ok || state.(string) != c.Query("state") {
|
||||
err = errors.New("非法的登录方式")
|
||||
}
|
||||
oauth2Config := oa.getCommonOauth2Config(c)
|
||||
ctx := context.Background()
|
||||
var otk *oauth2.Token
|
||||
if err == nil {
|
||||
otk, err = oa.oauth2Config.Exchange(ctx, c.Query("code"))
|
||||
otk, err = oauth2Config.Exchange(ctx, c.Query("code"))
|
||||
}
|
||||
var client *GitHubAPI.Client
|
||||
if err == nil {
|
||||
oc := oa.oauth2Config.Client(ctx, otk)
|
||||
if dao.Conf.Oauth2.Type == "gitee" {
|
||||
oc := oauth2Config.Client(ctx, otk)
|
||||
if dao.Conf.Oauth2.Type == model.ConfigTypeGitee {
|
||||
client, err = GitHubAPI.NewEnterpriseClient("https://gitee.com/api/v5/", "https://gitee.com/api/v5/", oc)
|
||||
} else {
|
||||
client = GitHubAPI.NewClient(oc)
|
||||
@ -71,7 +89,6 @@ func (oa *oauth2controller) callback(c *gin.Context) {
|
||||
if err == nil {
|
||||
gu, _, err = client.Users.Get(ctx, "")
|
||||
}
|
||||
log.Printf("%+v", gu)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusBadRequest,
|
||||
|
@ -52,6 +52,7 @@ func initSystem() {
|
||||
dao.DB.AutoMigrate(model.Server{}, model.User{},
|
||||
model.Notification{}, model.AlertRule{}, model.Monitor{},
|
||||
model.MonitorHistory{}, model.Cron{})
|
||||
dao.NewServiceSentinel()
|
||||
|
||||
loadServers() //加载服务器列表
|
||||
loadCrons() //加载计划任务
|
||||
@ -109,6 +110,6 @@ func loadCrons() {
|
||||
func main() {
|
||||
go controller.ServeWeb(dao.Conf.HTTPPort)
|
||||
go rpc.ServeRPC(dao.Conf.GRPCPort)
|
||||
go rpc.DispatchTask(time.Minute * 3)
|
||||
go rpc.DispatchTask(time.Second * 30)
|
||||
dao.AlertSentinelStart()
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
pb "github.com/naiba/nezha/proto"
|
||||
"github.com/naiba/nezha/service/dao"
|
||||
rpcService "github.com/naiba/nezha/service/rpc"
|
||||
@ -28,9 +27,8 @@ func ServeRPC(port uint) {
|
||||
func DispatchTask(duration time.Duration) {
|
||||
var index uint64 = 0
|
||||
for {
|
||||
var tasks []model.Monitor
|
||||
var hasAliveAgent bool
|
||||
dao.DB.Find(&tasks)
|
||||
tasks := dao.ServiceSentinelShared.Monitors()
|
||||
dao.SortedServerLock.RLock()
|
||||
startedAt := time.Now()
|
||||
for i := 0; i < len(tasks); i++ {
|
||||
@ -41,7 +39,9 @@ func DispatchTask(duration time.Duration) {
|
||||
}
|
||||
hasAliveAgent = false
|
||||
}
|
||||
if dao.SortedServerList[index].TaskStream == nil {
|
||||
// 1. 如果此任务不可使用此服务器请求,跳过这个服务器(有些 IPv6 only 开了 NAT64 的机器请求 IPv4 总会出问题)
|
||||
// 2. 如果服务器不在线,跳过这个服务器
|
||||
if tasks[i].SkipServers[dao.SortedServerList[index].ID] || dao.SortedServerList[index].TaskStream == nil {
|
||||
i--
|
||||
index++
|
||||
continue
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/genkiroid/cert"
|
||||
"github.com/go-ping/ping"
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
@ -20,8 +21,8 @@ import (
|
||||
func main() {
|
||||
// icmp()
|
||||
// tcpping()
|
||||
// httpWithSSLInfo()
|
||||
sysinfo()
|
||||
httpWithSSLInfo()
|
||||
// sysinfo()
|
||||
// cmdExec()
|
||||
}
|
||||
|
||||
@ -33,7 +34,7 @@ func tcpping() {
|
||||
}
|
||||
conn.Write([]byte("ping\n"))
|
||||
conn.Close()
|
||||
fmt.Println(time.Now().Sub(start).Microseconds(), float32(time.Now().Sub(start).Microseconds())/1000.0)
|
||||
fmt.Println(time.Since(start).Microseconds(), float32(time.Since(start).Microseconds())/1000.0)
|
||||
}
|
||||
|
||||
func sysinfo() {
|
||||
@ -53,7 +54,6 @@ func sysinfo() {
|
||||
for model, count := range cpuModelCount {
|
||||
cpus = append(cpus, fmt.Sprintf("%s %d %s Core", model, count, cpuType))
|
||||
}
|
||||
log.Println(cpus)
|
||||
os.Exit(0)
|
||||
// 硬盘信息,不使用的原因是会重复统计 Linux、Mac
|
||||
dparts, _ := disk.Partitions(false)
|
||||
@ -73,11 +73,12 @@ func httpWithSSLInfo() {
|
||||
httpClient := &http.Client{Transport: transCfg, CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}}
|
||||
resp, err := httpClient.Get("http://mail.nai.ba")
|
||||
fmt.Println(err, resp.StatusCode)
|
||||
url := "https://ops.naibahq.com"
|
||||
resp, err := httpClient.Get(url)
|
||||
fmt.Println(err, resp)
|
||||
// SSL 证书信息获取
|
||||
// c := cert.NewCert("expired-ecc-dv.ssl.com")
|
||||
// fmt.Println(c.Error)
|
||||
c := cert.NewCert(url[8:])
|
||||
fmt.Println(c.Error)
|
||||
}
|
||||
|
||||
func icmp() {
|
||||
|
9
go.mod
9
go.mod
@ -7,8 +7,9 @@ require (
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/genkiroid/cert v0.0.0-20191007122723-897560fbbe50
|
||||
github.com/gin-contrib/pprof v1.3.0
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663
|
||||
github.com/go-ping/ping v0.0.0-20210407214646-e4e642a95741
|
||||
github.com/golang/protobuf v1.4.2
|
||||
github.com/google/go-github v17.0.0+incompatible
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
@ -17,14 +18,16 @@ require (
|
||||
github.com/p14yground/go-github-selfupdate v1.2.3-0.20210119020835-db3523c6834b
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v3 v3.20.11
|
||||
github.com/shirou/gopsutil/v3 v3.21.3
|
||||
github.com/spf13/viper v1.7.1
|
||||
github.com/stretchr/testify v1.6.1
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 // indirect
|
||||
google.golang.org/grpc v1.33.1
|
||||
google.golang.org/protobuf v1.25.0
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
gorm.io/driver/sqlite v1.1.4
|
||||
gorm.io/gorm v1.20.8
|
||||
gorm.io/gorm v1.21.8
|
||||
)
|
||||
|
63
go.sum
63
go.sum
@ -63,7 +63,6 @@ github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@ -74,15 +73,17 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/genkiroid/cert v0.0.0-20191007122723-897560fbbe50 h1:vLwmYBduhnWWqShoUGbVgDulhcLdanoYtCQxYMzwaqQ=
|
||||
github.com/genkiroid/cert v0.0.0-20191007122723-897560fbbe50/go.mod h1:Pb7nyGYAfDyE/IkU6AJeRshIFko0wJC9cOqeYzYQffk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
|
||||
github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
@ -93,8 +94,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663 h1:jI2GiiRh+pPbey52EVmbU6kuLiXqwy4CXZ4gwUBj8Y0=
|
||||
github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663/go.mod h1:35JbSyV/BYqHwwRA6Zr1uVDm1637YlNOU61wI797NPI=
|
||||
github.com/go-ping/ping v0.0.0-20210407214646-e4e642a95741 h1:b0sLP++Tsle+s57tqg5sUk1/OQsC6yMCciVeqNzOcwU=
|
||||
github.com/go-ping/ping v0.0.0-20210407214646-e4e642a95741/go.mod h1:35JbSyV/BYqHwwRA6Zr1uVDm1637YlNOU61wI797NPI=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
@ -106,7 +107,6 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@ -121,7 +121,6 @@ github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
@ -131,19 +130,16 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
@ -200,8 +196,9 @@ github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7V
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
||||
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
@ -282,8 +279,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shirou/gopsutil/v3 v3.20.11 h1:NeVf1K0cgxsWz+N3671ojRptdgzvp7BXL3KV21R0JnA=
|
||||
github.com/shirou/gopsutil/v3 v3.20.11/go.mod h1:igHnfak0qnw1biGeI2qKQvu0ZkwvEkUcCLlYhZzdr/4=
|
||||
github.com/shirou/gopsutil/v3 v3.21.3 h1:wgcdAHZS2H6qy4JFewVTtqfiYxFzCeEJod/mLztdPG8=
|
||||
github.com/shirou/gopsutil/v3 v3.21.3/go.mod h1:ghfMypLDrFSWN2c9cDYFLHyynQ+QUht0cv/18ZqVczw=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
@ -305,7 +302,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@ -313,6 +309,10 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
||||
github.com/tklauser/go-sysconf v0.3.4 h1:HT8SVixZd3IzLdfs/xlpq0jeSfTX57g1v6wB1EuzV7M=
|
||||
github.com/tklauser/go-sysconf v0.3.4/go.mod h1:Cl2c8ZRWfHD5IrfHo9VN+FX9kCFjIOyVklgXycLB6ek=
|
||||
github.com/tklauser/numcpus v0.2.1 h1:ct88eFm+Q7m2ZfXJdan1xYoXKlmwsfP+k88q05KvlZc=
|
||||
github.com/tklauser/numcpus v0.2.1/go.mod h1:9aU+wOc6WjUIZEwWMP62PL/41d65P+iks1gBkr4QyP8=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
@ -380,7 +380,6 @@ golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@ -395,7 +394,6 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
@ -403,14 +401,13 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 h1:0PC75Fz/kyMGhL0e1QnypqK2kQMqKt9csD1GnMJR+Zk=
|
||||
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -445,34 +442,33 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5 h1:iCaAy5bMeEvwANu3YnJfWwI0kWAGkEa2RXPdweI/ysk=
|
||||
golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@ -521,7 +517,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -543,10 +538,8 @@ google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSr
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||
@ -557,7 +550,6 @@ google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRn
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
@ -576,7 +568,6 @@ google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfG
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
@ -589,7 +580,6 @@ google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
@ -610,7 +600,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@ -624,9 +613,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@ -635,8 +622,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
|
||||
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
|
||||
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.20.8 h1:iToaOdZgjNvlc44NFkxfLa3U9q63qwaxt0FdNCiwOMs=
|
||||
gorm.io/gorm v1.20.8/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.21.8 h1:2CEwZSzogdhsKPlJ9OvBKTdlWIpELXb6HbfLfMNhSYI=
|
||||
gorm.io/gorm v1.21.8/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
12
model/api.go
Normal file
12
model/api.go
Normal file
@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
type ServiceItemResponse struct {
|
||||
Monitor Monitor
|
||||
TotalUp uint64
|
||||
TotalDown uint64
|
||||
CurrentUp uint64
|
||||
CurrentDown uint64
|
||||
Delay *[30]float32
|
||||
Up *[30]int
|
||||
Down *[30]int
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
pb "github.com/naiba/nezha/proto"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -14,9 +17,13 @@ const (
|
||||
|
||||
type Monitor struct {
|
||||
Common
|
||||
Name string
|
||||
Type uint8
|
||||
Target string
|
||||
Name string
|
||||
Type uint8
|
||||
Target string
|
||||
SkipServersRaw string
|
||||
Notify bool
|
||||
|
||||
SkipServers map[uint64]bool `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
func (m *Monitor) PB() *pb.Task {
|
||||
@ -26,3 +33,15 @@ func (m *Monitor) PB() *pb.Task {
|
||||
Data: m.Target,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Monitor) AfterFind(tx *gorm.DB) error {
|
||||
var skipServers []uint64
|
||||
if err := 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
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
.nb-container {
|
||||
padding-top: 75px;
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 55px;
|
||||
margin-bottom: -47px;
|
||||
}
|
||||
|
@ -1,273 +1,371 @@
|
||||
function readableBytes(bytes) {
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
||||
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
return (bytes / Math.pow(1024, i)).toFixed(0) + ' ' + sizes[i];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
||||
sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
return (bytes / Math.pow(1024, i)).toFixed(0) + " " + sizes[i];
|
||||
}
|
||||
|
||||
const confirmBtn = $('.mini.confirm.modal .positive.button')
|
||||
const confirmBtn = $(".mini.confirm.modal .positive.button");
|
||||
|
||||
function showConfirm(title, content, callFn, extData) {
|
||||
const modal = $('.mini.confirm.modal')
|
||||
modal.children('.header').text(title)
|
||||
modal.children('.content').text(content)
|
||||
if (confirmBtn.hasClass('loading')) {
|
||||
return false
|
||||
}
|
||||
modal.modal({
|
||||
closable: true,
|
||||
onApprove: function () {
|
||||
confirmBtn.toggleClass('loading')
|
||||
callFn(extData)
|
||||
return false
|
||||
}
|
||||
}).modal('show')
|
||||
const modal = $(".mini.confirm.modal");
|
||||
modal.children(".header").text(title);
|
||||
modal.children(".content").text(content);
|
||||
if (confirmBtn.hasClass("loading")) {
|
||||
return false;
|
||||
}
|
||||
modal
|
||||
.modal({
|
||||
closable: true,
|
||||
onApprove: function () {
|
||||
confirmBtn.toggleClass("loading");
|
||||
callFn(extData);
|
||||
return false;
|
||||
},
|
||||
})
|
||||
.modal("show");
|
||||
}
|
||||
|
||||
function showFormModal(modelSelector, formID, URL, getData) {
|
||||
$(modelSelector).modal({
|
||||
closable: true,
|
||||
onApprove: function () {
|
||||
let success = false
|
||||
const btn = $(modelSelector + ' .positive.button')
|
||||
const form = $(modelSelector + ' form')
|
||||
if (btn.hasClass('loading')) {
|
||||
return success
|
||||
}
|
||||
form.children('.message').remove()
|
||||
btn.toggleClass('loading')
|
||||
const data = getData ? getData() : $(formID).serializeArray().reduce(function (obj, item) {
|
||||
$(modelSelector)
|
||||
.modal({
|
||||
closable: true,
|
||||
onApprove: function () {
|
||||
let success = false;
|
||||
const btn = $(modelSelector + " .positive.button");
|
||||
const form = $(modelSelector + " form");
|
||||
if (btn.hasClass("loading")) {
|
||||
return success;
|
||||
}
|
||||
form.children(".message").remove();
|
||||
btn.toggleClass("loading");
|
||||
const data = getData
|
||||
? getData()
|
||||
: $(formID)
|
||||
.serializeArray()
|
||||
.reduce(function (obj, item) {
|
||||
// ID 类的数据
|
||||
if ((item.name.endsWith('_id') ||
|
||||
item.name === 'id' || item.name === 'ID' ||
|
||||
item.name === 'RequestType' || item.name === 'RequestMethod' ||
|
||||
item.name === 'DisplayIndex' || item.name === 'Type')) {
|
||||
obj[item.name] = parseInt(item.value);
|
||||
if (
|
||||
item.name.endsWith("_id") ||
|
||||
item.name === "id" ||
|
||||
item.name === "ID" ||
|
||||
item.name === "RequestType" ||
|
||||
item.name === "RequestMethod" ||
|
||||
item.name === "DisplayIndex" ||
|
||||
item.name === "Type"
|
||||
) {
|
||||
obj[item.name] = parseInt(item.value);
|
||||
} else {
|
||||
obj[item.name] = item.value;
|
||||
obj[item.name] = item.value;
|
||||
}
|
||||
|
||||
if (item.name == 'ServersRaw') {
|
||||
if (item.value.length > 2) {
|
||||
obj[item.name] = '[' + item.value.substr(3, item.value.length - 1) + ']'
|
||||
}
|
||||
if (item.name.endsWith("ServersRaw")) {
|
||||
if (item.value.length > 2) {
|
||||
obj[item.name] = JSON.stringify(
|
||||
[...item.value.matchAll(/\d+/gm)].map((k) =>
|
||||
parseInt(k[0])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
$.post(URL, JSON.stringify(data)).done(function (resp) {
|
||||
if (resp.code == 200) {
|
||||
if (resp.message) {
|
||||
$.suiAlert({
|
||||
title: '操作成功',
|
||||
type: 'success',
|
||||
description: resp.message,
|
||||
time: '3',
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
window.location.reload()
|
||||
} else {
|
||||
form.append(`<div class="ui negative message"><div class="header">操作失败</div><p>` + resp.message + `</p></div>`)
|
||||
}
|
||||
}).fail(function (err) {
|
||||
form.append(`<div class="ui negative message"><div class="header">网络错误</div><p>` + err.responseText + `</p></div>`)
|
||||
}).always(function () {
|
||||
btn.toggleClass('loading')
|
||||
});
|
||||
return success
|
||||
}
|
||||
}).modal('show')
|
||||
}, {});
|
||||
$.post(URL, JSON.stringify(data))
|
||||
.done(function (resp) {
|
||||
if (resp.code == 200) {
|
||||
if (resp.message) {
|
||||
$.suiAlert({
|
||||
title: "操作成功",
|
||||
type: "success",
|
||||
description: resp.message,
|
||||
time: "3",
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
window.location.reload();
|
||||
} else {
|
||||
form.append(
|
||||
`<div class="ui negative message"><div class="header">操作失败</div><p>` +
|
||||
resp.message +
|
||||
`</p></div>`
|
||||
);
|
||||
}
|
||||
})
|
||||
.fail(function (err) {
|
||||
form.append(
|
||||
`<div class="ui negative message"><div class="header">网络错误</div><p>` +
|
||||
err.responseText +
|
||||
`</p></div>`
|
||||
);
|
||||
})
|
||||
.always(function () {
|
||||
btn.toggleClass("loading");
|
||||
});
|
||||
return success;
|
||||
},
|
||||
})
|
||||
.modal("show");
|
||||
}
|
||||
|
||||
function addOrEditAlertRule(rule) {
|
||||
const modal = $('.rule.modal')
|
||||
modal.children('.header').text((rule ? '修改' : '添加') + '报警规则')
|
||||
modal.find('.positive.button').html(rule ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>')
|
||||
modal.find('input[name=ID]').val(rule ? rule.ID : null)
|
||||
modal.find('input[name=Name]').val(rule ? rule.Name : null)
|
||||
modal.find('textarea[name=RulesRaw]').val(rule ? rule.RulesRaw : null)
|
||||
if (rule && rule.Enable) {
|
||||
modal.find('.ui.rule-enable.checkbox').checkbox('set checked')
|
||||
} else {
|
||||
modal.find('.ui.rule-enable.checkbox').checkbox('set unchecked')
|
||||
}
|
||||
showFormModal('.rule.modal', '#ruleForm', '/api/alert-rule')
|
||||
const modal = $(".rule.modal");
|
||||
modal.children(".header").text((rule ? "修改" : "添加") + "报警规则");
|
||||
modal
|
||||
.find(".positive.button")
|
||||
.html(
|
||||
rule ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>'
|
||||
);
|
||||
modal.find("input[name=ID]").val(rule ? rule.ID : null);
|
||||
modal.find("input[name=Name]").val(rule ? rule.Name : null);
|
||||
modal.find("textarea[name=RulesRaw]").val(rule ? rule.RulesRaw : null);
|
||||
if (rule && rule.Enable) {
|
||||
modal.find(".ui.rule-enable.checkbox").checkbox("set checked");
|
||||
} else {
|
||||
modal.find(".ui.rule-enable.checkbox").checkbox("set unchecked");
|
||||
}
|
||||
showFormModal(".rule.modal", "#ruleForm", "/api/alert-rule");
|
||||
}
|
||||
|
||||
function addOrEditNotification(notification) {
|
||||
const modal = $('.notification.modal')
|
||||
modal.children('.header').text((notification ? '修改' : '添加') + '通知方式')
|
||||
modal.find('.positive.button').html(notification ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>')
|
||||
modal.find('input[name=ID]').val(notification ? notification.ID : null)
|
||||
modal.find('input[name=Name]').val(notification ? notification.Name : null)
|
||||
modal.find('input[name=URL]').val(notification ? notification.URL : null)
|
||||
modal.find('textarea[name=RequestBody]').val(notification ? notification.RequestBody : null)
|
||||
modal.find('select[name=RequestMethod]').val(notification ? notification.RequestMethod : 1)
|
||||
modal.find('select[name=RequestType]').val(notification ? notification.RequestType : 1)
|
||||
if (notification && notification.VerifySSL) {
|
||||
modal.find('.ui.nf-ssl.checkbox').checkbox('set checked')
|
||||
} else {
|
||||
modal.find('.ui.nf-ssl.checkbox').checkbox('set unchecked')
|
||||
}
|
||||
showFormModal('.notification.modal', '#notificationForm', '/api/notification')
|
||||
const modal = $(".notification.modal");
|
||||
modal.children(".header").text((notification ? "修改" : "添加") + "通知方式");
|
||||
modal
|
||||
.find(".positive.button")
|
||||
.html(
|
||||
notification
|
||||
? '修改<i class="edit icon"></i>'
|
||||
: '添加<i class="add icon"></i>'
|
||||
);
|
||||
modal.find("input[name=ID]").val(notification ? notification.ID : null);
|
||||
modal.find("input[name=Name]").val(notification ? notification.Name : null);
|
||||
modal.find("input[name=URL]").val(notification ? notification.URL : null);
|
||||
modal
|
||||
.find("textarea[name=RequestBody]")
|
||||
.val(notification ? notification.RequestBody : null);
|
||||
modal
|
||||
.find("select[name=RequestMethod]")
|
||||
.val(notification ? notification.RequestMethod : 1);
|
||||
modal
|
||||
.find("select[name=RequestType]")
|
||||
.val(notification ? notification.RequestType : 1);
|
||||
if (notification && notification.VerifySSL) {
|
||||
modal.find(".ui.nf-ssl.checkbox").checkbox("set checked");
|
||||
} else {
|
||||
modal.find(".ui.nf-ssl.checkbox").checkbox("set unchecked");
|
||||
}
|
||||
showFormModal(
|
||||
".notification.modal",
|
||||
"#notificationForm",
|
||||
"/api/notification"
|
||||
);
|
||||
}
|
||||
|
||||
function addOrEditServer(server) {
|
||||
const modal = $('.server.modal')
|
||||
modal.children('.header').text((server ? '修改' : '添加') + '服务器')
|
||||
modal.find('.positive.button').html(server ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>')
|
||||
modal.find('input[name=id]').val(server ? server.ID : null)
|
||||
modal.find('input[name=name]').val(server ? server.Name : null)
|
||||
modal.find('input[name=Tag]').val(server ? server.Tag : null)
|
||||
modal.find('input[name=DisplayIndex]').val(server ? server.DisplayIndex : null)
|
||||
modal.find('textarea[name=Note]').val(server ? server.Note : null)
|
||||
if (server) {
|
||||
modal.find('.secret.field').attr('style', '')
|
||||
modal.find('input[name=secret]').val(server.Secret)
|
||||
} else {
|
||||
modal.find('.secret.field').attr('style', 'display:none')
|
||||
modal.find('input[name=secret]').val('')
|
||||
}
|
||||
showFormModal('.server.modal', '#serverForm', '/api/server')
|
||||
const modal = $(".server.modal");
|
||||
modal.children(".header").text((server ? "修改" : "添加") + "服务器");
|
||||
modal
|
||||
.find(".positive.button")
|
||||
.html(
|
||||
server ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>'
|
||||
);
|
||||
modal.find("input[name=id]").val(server ? server.ID : null);
|
||||
modal.find("input[name=name]").val(server ? server.Name : null);
|
||||
modal.find("input[name=Tag]").val(server ? server.Tag : null);
|
||||
modal
|
||||
.find("input[name=DisplayIndex]")
|
||||
.val(server ? server.DisplayIndex : null);
|
||||
modal.find("textarea[name=Note]").val(server ? server.Note : null);
|
||||
if (server) {
|
||||
modal.find(".secret.field").attr("style", "");
|
||||
modal.find("input[name=secret]").val(server.Secret);
|
||||
} else {
|
||||
modal.find(".secret.field").attr("style", "display:none");
|
||||
modal.find("input[name=secret]").val("");
|
||||
}
|
||||
showFormModal(".server.modal", "#serverForm", "/api/server");
|
||||
}
|
||||
|
||||
function addOrEditMonitor(monitor) {
|
||||
const modal = $('.monitor.modal')
|
||||
modal.children('.header').text((monitor ? '修改' : '添加') + '监控')
|
||||
modal.find('.positive.button').html(monitor ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>')
|
||||
modal.find('input[name=ID]').val(monitor ? monitor.ID : null)
|
||||
modal.find('input[name=Name]').val(monitor ? monitor.Name : null)
|
||||
modal.find('input[name=Target]').val(monitor ? monitor.Target : null)
|
||||
modal.find('select[name=Type]').val(monitor ? monitor.Type : 1)
|
||||
showFormModal('.monitor.modal', '#monitorForm', '/api/monitor')
|
||||
const modal = $(".monitor.modal");
|
||||
modal.children(".header").text((monitor ? "修改" : "添加") + "监控");
|
||||
modal
|
||||
.find(".positive.button")
|
||||
.html(
|
||||
monitor ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>'
|
||||
);
|
||||
modal.find("input[name=ID]").val(monitor ? monitor.ID : null);
|
||||
modal.find("input[name=Name]").val(monitor ? monitor.Name : null);
|
||||
modal.find("input[name=Target]").val(monitor ? monitor.Target : null);
|
||||
modal.find("select[name=Type]").val(monitor ? monitor.Type : 1);
|
||||
if (monitor && monitor.Notify) {
|
||||
modal.find(".ui.nb-notify.checkbox").checkbox("set checked");
|
||||
} else {
|
||||
modal.find(".ui.nb-notify.checkbox").checkbox("set unchecked");
|
||||
}
|
||||
var servers;
|
||||
if (monitor) {
|
||||
servers = monitor.SkipServersRaw;
|
||||
const serverList = JSON.parse(servers || "[]");
|
||||
const node = modal.find("i.dropdown.icon");
|
||||
for (let i = 0; i < serverList.length; i++) {
|
||||
node.after(
|
||||
'<a class="ui label transition visible" data-value="' +
|
||||
serverList[i] +
|
||||
'" style="display: inline-block !important;">ID:' +
|
||||
serverList[i] +
|
||||
'<i class="delete icon"></i></a>'
|
||||
);
|
||||
}
|
||||
}
|
||||
modal
|
||||
.find("input[name=SkipServersRaw]")
|
||||
.val(monitor ? "[]," + servers.substr(1, servers.length - 2) : "[]");
|
||||
showFormModal(".monitor.modal", "#monitorForm", "/api/monitor");
|
||||
}
|
||||
|
||||
function addOrEditCron(cron) {
|
||||
const modal = $('.cron.modal')
|
||||
modal.children('.header').text((cron ? '修改' : '添加') + '计划任务')
|
||||
modal.find('.positive.button').html(cron ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>')
|
||||
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) => {
|
||||
el.remove()
|
||||
})
|
||||
var servers
|
||||
if (cron) {
|
||||
servers = cron.ServersRaw
|
||||
serverList = JSON.parse(servers)
|
||||
const node = modal.find('i.dropdown.icon')
|
||||
for (let i = 0; i < serverList.length; i++) {
|
||||
node.after('<a class="ui label transition visible" data-value="' + serverList[i] + '" style="display: inline-block !important;">ID:' + serverList[i] + '<i class="delete icon"></i></a>')
|
||||
}
|
||||
const modal = $(".cron.modal");
|
||||
modal.children(".header").text((cron ? "修改" : "添加") + "计划任务");
|
||||
modal
|
||||
.find(".positive.button")
|
||||
.html(
|
||||
cron ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>'
|
||||
);
|
||||
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) => {
|
||||
el.remove();
|
||||
});
|
||||
var servers;
|
||||
if (cron) {
|
||||
servers = cron.ServersRaw;
|
||||
const serverList = JSON.parse(servers || "[]");
|
||||
const node = modal.find("i.dropdown.icon");
|
||||
for (let i = 0; i < serverList.length; i++) {
|
||||
node.after(
|
||||
'<a class="ui label transition visible" data-value="' +
|
||||
serverList[i] +
|
||||
'" style="display: inline-block !important;">ID:' +
|
||||
serverList[i] +
|
||||
'<i class="delete icon"></i></a>'
|
||||
);
|
||||
}
|
||||
modal.find('input[name=ServersRaw]').val(cron ? '[],' + servers.substr(1, servers.length - 2) : '[]')
|
||||
modal.find('textarea[name=Command]').val(cron ? cron.Command : null)
|
||||
if (cron && cron.PushSuccessful) {
|
||||
modal.find('.ui.push-successful.checkbox').checkbox('set checked')
|
||||
} else {
|
||||
modal.find('.ui.push-successful.checkbox').checkbox('set unchecked')
|
||||
}
|
||||
showFormModal('.cron.modal', '#cronForm', '/api/cron')
|
||||
}
|
||||
modal
|
||||
.find("input[name=ServersRaw]")
|
||||
.val(cron ? "[]," + servers.substr(1, servers.length - 2) : "[]");
|
||||
modal.find("textarea[name=Command]").val(cron ? cron.Command : null);
|
||||
if (cron && cron.PushSuccessful) {
|
||||
modal.find(".ui.push-successful.checkbox").checkbox("set checked");
|
||||
} else {
|
||||
modal.find(".ui.push-successful.checkbox").checkbox("set unchecked");
|
||||
}
|
||||
showFormModal(".cron.modal", "#cronForm", "/api/cron");
|
||||
}
|
||||
|
||||
function deleteRequest(api) {
|
||||
$.ajax({
|
||||
url: api,
|
||||
type: 'DELETE',
|
||||
}).done(resp => {
|
||||
if (resp.code == 200) {
|
||||
if (resp.message) {
|
||||
alert(resp.message)
|
||||
} else {
|
||||
alert('删除成功')
|
||||
}
|
||||
window.location.reload()
|
||||
$.ajax({
|
||||
url: api,
|
||||
type: "DELETE",
|
||||
})
|
||||
.done((resp) => {
|
||||
if (resp.code == 200) {
|
||||
if (resp.message) {
|
||||
alert(resp.message);
|
||||
} else {
|
||||
alert('删除失败 ' + resp.code + ':' + resp.message)
|
||||
confirmBtn.toggleClass('loading')
|
||||
alert("删除成功");
|
||||
}
|
||||
}).fail(err => {
|
||||
alert('网络错误:' + err.responseText)
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert("删除失败 " + resp.code + ":" + resp.message);
|
||||
confirmBtn.toggleClass("loading");
|
||||
}
|
||||
})
|
||||
.fail((err) => {
|
||||
alert("网络错误:" + err.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
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')
|
||||
$(btn).toggleClass("loading");
|
||||
$.ajax({
|
||||
url: "/api/cron/" + cronId + "/manual",
|
||||
type: "GET",
|
||||
})
|
||||
.done((resp) => {
|
||||
$(btn).toggleClass("loading");
|
||||
if (resp.code == 200) {
|
||||
$.suiAlert({
|
||||
title: '触发失败 ',
|
||||
type: 'error',
|
||||
description: '网络错误:' + err.responseText,
|
||||
time: '3',
|
||||
position: 'top-center',
|
||||
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) {
|
||||
$.suiAlert({
|
||||
title: '注销成功',
|
||||
type: 'success',
|
||||
description: '如需继续访问请使用 GitHub 再次登录',
|
||||
time: '3',
|
||||
position: 'top-center',
|
||||
});
|
||||
window.location.reload()
|
||||
} else {
|
||||
$.suiAlert({
|
||||
title: '注销失败',
|
||||
description: resp.code + ':' + resp.message,
|
||||
type: 'error',
|
||||
time: '3',
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
}).fail(function (err) {
|
||||
$.post("/api/logout", JSON.stringify({ id: id }))
|
||||
.done(function (resp) {
|
||||
if (resp.code == 200) {
|
||||
$.suiAlert({
|
||||
title: '网络错误',
|
||||
description: err.responseText,
|
||||
type: 'error',
|
||||
time: '3',
|
||||
position: 'top-center',
|
||||
title: "注销成功",
|
||||
type: "success",
|
||||
description: "如需继续访问请使用 GitHub 再次登录",
|
||||
time: "3",
|
||||
position: "top-center",
|
||||
});
|
||||
window.location.reload();
|
||||
} else {
|
||||
$.suiAlert({
|
||||
title: "注销失败",
|
||||
description: resp.code + ":" + resp.message,
|
||||
type: "error",
|
||||
time: "3",
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
})
|
||||
.fail(function (err) {
|
||||
$.suiAlert({
|
||||
title: "网络错误",
|
||||
description: err.responseText,
|
||||
type: "error",
|
||||
time: "3",
|
||||
position: "top-center",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(() => {
|
||||
try {
|
||||
$('.ui.servers.search.dropdown').dropdown({
|
||||
clearable: true,
|
||||
apiSettings: {
|
||||
url: '/api/search-server?word={query}',
|
||||
cache: false,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
}
|
||||
})
|
||||
try {
|
||||
$(".ui.servers.search.dropdown").dropdown({
|
||||
clearable: true,
|
||||
apiSettings: {
|
||||
url: "/api/search-server?word={query}",
|
||||
cache: false,
|
||||
},
|
||||
});
|
||||
} catch (error) {}
|
||||
});
|
||||
|
2
resource/template/common/footer.html
vendored
2
resource/template/common/footer.html
vendored
@ -9,7 +9,7 @@
|
||||
<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="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.min.js"></script>
|
||||
<script src="/static/main.js?v202101240939"></script>
|
||||
<script src="/static/main.js?v202104232106"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
2
resource/template/common/header.html
vendored
2
resource/template/common/header.html
vendored
@ -9,7 +9,7 @@
|
||||
<title>{{.Title}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.1/dist/semantic.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/main.css?v202102051040">
|
||||
<link rel="stylesheet" type="text/css" href="/static/main.css?v202104192145">
|
||||
<link rel="shortcut icon" type="image/png" href="/static/logo.png?v20210320" />
|
||||
</head>
|
||||
|
||||
|
2
resource/template/common/menu.html
vendored
2
resource/template/common/menu.html
vendored
@ -14,7 +14,7 @@
|
||||
</a>
|
||||
{{else}}
|
||||
<a class='item{{if eq .MatchedPath "/"}} active{{end}}' href="/"><i class="home icon"></i>首页</a>
|
||||
<a class='item{{if eq .MatchedPath "/service"}} active{{end}}' href="/service"><i class="rss icon"></i>服务状态</a>
|
||||
<a class='item{{if eq .MatchedPath "/service"}} active{{end}}' href="/service"><i class="rss icon"></i>服务</a>
|
||||
{{end}}
|
||||
<div class="right menu">
|
||||
<div class="item">
|
||||
|
87
resource/template/component/monitor.html
vendored
87
resource/template/component/monitor.html
vendored
@ -1,38 +1,59 @@
|
||||
{{define "component/monitor"}}
|
||||
<div class="ui tiny monitor modal transition hidden">
|
||||
<div class="header">添加监控</div>
|
||||
<div class="content">
|
||||
<form id="monitorForm" class="ui form">
|
||||
<input type="hidden" name="ID">
|
||||
<div class="field">
|
||||
<label>名称</label>
|
||||
<input type="text" name="Name" placeholder="博客">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>目标</label>
|
||||
<input type="text" name="Target" placeholder="HTTP(https://t.tt)|Ping(t.tt)|TCP(t.tt:80)">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>类型</label>
|
||||
<select name="Type" class="ui fluid dropdown">
|
||||
<option value="1">HTTP-GET(SSL到期、变更)</option>
|
||||
<option value="2">ICMP-Ping</option>
|
||||
<option value="3">TCP-Ping</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
<div class="ui warning message">
|
||||
<p>
|
||||
类型为 <b>HTTP-GET</b> 时输入URL(带 http/https, HTTPS 协议的会顺带监控SSL证书);<br>
|
||||
类型为 <b>ICMP-Ping</b> 时输入主机名/IP,不带端口;<br>
|
||||
类型为 <b>TCP-Ping</b> 时输入主机名/IP + 端口号:example.com:22
|
||||
</p>
|
||||
<div class="header">添加监控</div>
|
||||
<div class="content">
|
||||
<form id="monitorForm" class="ui form">
|
||||
<input type="hidden" name="ID" />
|
||||
<div class="field">
|
||||
<label>名称</label>
|
||||
<input type="text" name="Name" placeholder="博客" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>目标</label>
|
||||
<input
|
||||
type="text"
|
||||
name="Target"
|
||||
placeholder="HTTP(https://t.tt)|Ping(t.tt)|TCP(t.tt:80)"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>类型</label>
|
||||
<select name="Type" class="ui fluid dropdown">
|
||||
<option value="1">HTTP-GET(SSL到期、变更)</option>
|
||||
<option value="2">ICMP-Ping</option>
|
||||
<option value="3">TCP-Ping</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>不通过下列服务器请求</label>
|
||||
<div class="ui fluid multiple servers search selection dropdown">
|
||||
<input type="hidden" name="SkipServersRaw" />
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="default text">输入ID/名称以搜索</div>
|
||||
<div class="menu"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui nb-notify checkbox">
|
||||
<input name="Notify" type="checkbox" tabindex="0" class="hidden" />
|
||||
<label>启用故障通知</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="ui warning message">
|
||||
<p>
|
||||
类型为 <b>HTTP-GET</b> 时输入URL(带 http/https, HTTPS
|
||||
协议的会顺带监控SSL证书);<br />
|
||||
类型为 <b>ICMP-Ping</b> 时输入主机名/IP,不带端口;<br />
|
||||
类型为 <b>TCP-Ping</b> 时输入主机名/IP + 端口号:example.com:22
|
||||
</p>
|
||||
</div>
|
||||
<div class=" actions">
|
||||
<div class="ui negative button">取消</div>
|
||||
<button class="ui positive right labeled icon button">确认<i class="checkmark icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="ui negative button">取消</div>
|
||||
<button class="ui positive right labeled icon button">
|
||||
确认<i class="checkmark icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
@ -6,12 +6,12 @@
|
||||
<h2 class="ui teal image header">
|
||||
<img src="static/logo.png?v20210320" class="image">
|
||||
<div class="content">
|
||||
使用 GitHub 账号登录
|
||||
使用 {{.LoginType}} 账号登录
|
||||
</div>
|
||||
</h2>
|
||||
<a href="/oauth2/login" class="ui fluid large teal submit button">登录</a>
|
||||
<div class="ui message">
|
||||
没有账号? <a href="https://github.com/join" target="_blank">注册</a>
|
||||
没有账号? <a href="{{.RegistrationLink}}" target="_blank">注册</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,56 +1,65 @@
|
||||
{{define "dashboard/monitor"}}
|
||||
{{template "common/header" .}}
|
||||
{{template "common/menu" .}}
|
||||
{{define "dashboard/monitor"}} {{template "common/header" .}} {{template
|
||||
"common/menu" .}}
|
||||
<div class="nb-container">
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
<div class="right floated right aligned twelve wide column">
|
||||
<button class="ui right labeled positive icon button" onclick="addOrEditMonitor()"><i
|
||||
class="add icon"></i> 添加监控
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名称</th>
|
||||
<th>目标</th>
|
||||
<th>类型</th>
|
||||
<th>管理</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $monitor := .Monitors}}
|
||||
<tr>
|
||||
<td>{{$monitor.ID}}</td>
|
||||
<td>{{$monitor.Name}}</td>
|
||||
<td>{{$monitor.Target}}</td>
|
||||
<td>
|
||||
{{if eq $monitor.Type 1}}HTTP(S)/SSL证书
|
||||
{{else if eq $monitor.Type 2}}
|
||||
ICMP Ping
|
||||
{{else}}
|
||||
TCP 端口
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<div class="ui mini icon buttons">
|
||||
<button class="ui button" onclick="addOrEditMonitor({{$monitor}})">
|
||||
<i class="edit icon"></i>
|
||||
</button>
|
||||
<button class="ui button"
|
||||
onclick="showConfirm('删除监控','确认删除此监控?',deleteRequest,'/api/monitor/'+{{$monitor.ID}})">
|
||||
<i class="trash alternate outline icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
<div class="right floated right aligned twelve wide column">
|
||||
<button
|
||||
class="ui right labeled positive icon button"
|
||||
onclick="addOrEditMonitor()"
|
||||
>
|
||||
<i class="add icon"></i> 添加监控
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名称</th>
|
||||
<th>目标</th>
|
||||
<th>跳过的服务器</th>
|
||||
<th>类型</th>
|
||||
<th>通知</th>
|
||||
<th>管理</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $monitor := .Monitors}}
|
||||
<tr>
|
||||
<td>{{$monitor.ID}}</td>
|
||||
<td>{{$monitor.Name}}</td>
|
||||
<td>{{$monitor.Target}}</td>
|
||||
<td>{{$monitor.SkipServersRaw}}</td>
|
||||
<td>
|
||||
{{if eq $monitor.Type 1}}HTTP(S)/SSL证书 {{else if eq $monitor.Type
|
||||
2}} ICMP Ping {{else}} TCP 端口 {{end}}
|
||||
</td>
|
||||
<td>{{$monitor.Notify}}</td>
|
||||
<td>
|
||||
<div class="ui mini icon buttons">
|
||||
<button
|
||||
class="ui button"
|
||||
onclick="addOrEditMonitor({{$monitor}})"
|
||||
>
|
||||
<i class="edit icon"></i>
|
||||
</button>
|
||||
<button
|
||||
class="ui button"
|
||||
onclick="showConfirm('删除监控','确认删除此监控?',deleteRequest,'/api/monitor/'+{{$monitor.ID}})"
|
||||
>
|
||||
<i class="trash alternate outline icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{template "component/monitor"}}
|
||||
{{template "common/footer" .}}
|
||||
{{end}}
|
||||
{{template "component/monitor"}} {{template "common/footer" .}}
|
||||
<script>
|
||||
$(".checkbox").checkbox();
|
||||
</script>
|
||||
{{end}}
|
||||
|
@ -8,11 +8,19 @@
|
||||
<label>站点标题</label>
|
||||
<input type="text" name="Title" placeholder="哪吒监控" value="{{.Conf.Site.Brand}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>登录类型</label>
|
||||
<select name="Oauth2Type">
|
||||
<option value="github"{{if eq .Conf.Oauth2.Type "github"}} selected="selected"{{end}}>GitHub</option>
|
||||
<option value="gitee"{{if eq .Conf.Oauth2.Type "gitee"}} selected="selected"{{end}}>Gitee</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>管理员列表</label>
|
||||
<input type="text" name="Admin" placeholder="1010,2020" value="{{.Conf.Oauth2.Admin}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>前台主题</label>
|
||||
<select name="Theme">
|
||||
<option value="default"{{if eq .Conf.Site.Theme "default"}} selected="selected"{{end}}>默认主题</option>
|
||||
<option value="daynight"{{if eq .Conf.Site.Theme "daynight"}} selected="selected"{{end}}>JackieSung DayNight</option>
|
||||
|
@ -1,4 +1,4 @@
|
||||
debug: true
|
||||
debug: false
|
||||
httpport: 80
|
||||
oauth2:
|
||||
type: "nz_oauth2_type" #Oauth2 登录接入类型,gitee/github
|
||||
|
@ -2,7 +2,7 @@ version: "3.3"
|
||||
|
||||
services:
|
||||
dashboard:
|
||||
image: image_url
|
||||
image: nz_image_url
|
||||
restart: always
|
||||
volumes:
|
||||
- ./data:/dashboard/data
|
||||
|
@ -11,7 +11,7 @@ NZ_BASE_PATH="/opt/nezha"
|
||||
NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
|
||||
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
|
||||
NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
|
||||
NZ_VERSION="v0.4.10"
|
||||
NZ_VERSION="v0.5.0"
|
||||
|
||||
red='\033[0;31m'
|
||||
green='\033[0;32m'
|
||||
@ -276,7 +276,7 @@ modify_dashboard_config() {
|
||||
sed -i "s/nz_site_title/${nz_site_title}/" ${NZ_DASHBOARD_PATH}/data/config.yaml
|
||||
sed -i "s/nz_site_port/${nz_site_port}/" ${NZ_DASHBOARD_PATH}/docker-compose.yaml
|
||||
sed -i "s/nz_grpc_port/${nz_grpc_port}/" ${NZ_DASHBOARD_PATH}/docker-compose.yaml
|
||||
sed -i "s/image_url/${Docker_IMG}/" ${NZ_DASHBOARD_PATH}/docker-compose.yaml
|
||||
sed -i "s/nz_image_url/${Docker_IMG}/" ${NZ_DASHBOARD_PATH}/docker-compose.yaml
|
||||
|
||||
echo -e "面板配置 ${green}修改成功,请稍等重启生效${plain}"
|
||||
|
||||
|
@ -15,7 +15,7 @@ Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/opt/nezha/agent/
|
||||
ExecStart=/opt/nezha/agent/nezha-agent -d -s nz_grpc_host:nz_grpc_port -p nz_client_secret
|
||||
ExecStart=/opt/nezha/agent/nezha-agent -s nz_grpc_host:nz_grpc_port -p nz_client_secret
|
||||
Restart=always
|
||||
#Environment=DEBUG=true
|
||||
|
||||
|
@ -43,7 +43,9 @@ func AlertSentinelStart() {
|
||||
checkStatus()
|
||||
checkCount++
|
||||
if lastPrint.Before(startedAt.Add(-1 * time.Hour)) {
|
||||
log.Println("报警规则检测每小时", checkCount, "次", startedAt, time.Now())
|
||||
if Conf.Debug {
|
||||
log.Println("报警规则检测每小时", checkCount, "次", startedAt, time.Now())
|
||||
}
|
||||
checkCount = 0
|
||||
lastPrint = startedAt
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
pb "github.com/naiba/nezha/proto"
|
||||
)
|
||||
|
||||
var Version = "v0.4.15" // !!记得修改 README 重的 badge 版本!!
|
||||
var Version = "v0.7.1" // !!记得修改 README 中的 badge 版本!!
|
||||
|
||||
const (
|
||||
SnapshotDelay = 3
|
||||
@ -46,7 +46,7 @@ func ReSortServer() {
|
||||
|
||||
sort.SliceStable(SortedServerList, func(i, j int) bool {
|
||||
if SortedServerList[i].DisplayIndex == SortedServerList[j].DisplayIndex {
|
||||
return SortedServerList[i].ID < SortedServerList[i].ID
|
||||
return SortedServerList[i].ID < SortedServerList[j].ID
|
||||
}
|
||||
return SortedServerList[i].DisplayIndex > SortedServerList[j].DisplayIndex
|
||||
})
|
||||
|
333
service/dao/servicesentinel.go
Normal file
333
service/dao/servicesentinel.go
Normal file
@ -0,0 +1,333 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
pb "github.com/naiba/nezha/proto"
|
||||
)
|
||||
|
||||
const _CurrentStatusSize = 30 // 统计 5 分钟内的数据为当前状态
|
||||
|
||||
var ServiceSentinelShared *ServiceSentinel
|
||||
|
||||
type ReportData struct {
|
||||
Data *pb.TaskResult
|
||||
Reporter uint64
|
||||
}
|
||||
|
||||
type _TodayStatsOfMonitor struct {
|
||||
Up int
|
||||
Down int
|
||||
Delay float32
|
||||
}
|
||||
|
||||
func NewServiceSentinel() {
|
||||
ServiceSentinelShared = &ServiceSentinel{
|
||||
serviceReportChannel: make(chan ReportData, 200),
|
||||
serviceStatusToday: make(map[uint64]*_TodayStatsOfMonitor),
|
||||
serviceCurrentStatusIndex: make(map[uint64]int),
|
||||
serviceCurrentStatusData: make(map[uint64][]model.MonitorHistory),
|
||||
latestDate: make(map[uint64]string),
|
||||
lastStatus: make(map[uint64]string),
|
||||
serviceResponseDataStoreCurrentUp: make(map[uint64]uint64),
|
||||
serviceResponseDataStoreCurrentDown: make(map[uint64]uint64),
|
||||
monitors: make(map[uint64]model.Monitor),
|
||||
sslCertCache: make(map[uint64]string),
|
||||
}
|
||||
ServiceSentinelShared.OnMonitorUpdate()
|
||||
|
||||
year, month, day := time.Now().Date()
|
||||
today := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
|
||||
var mhs []model.MonitorHistory
|
||||
DB.Where("created_at >= ?", today).Find(&mhs)
|
||||
|
||||
// 加载当日记录
|
||||
totalDelay := make(map[uint64]float32)
|
||||
for i := 0; i < len(mhs); i++ {
|
||||
if mhs[i].Successful {
|
||||
ServiceSentinelShared.serviceStatusToday[mhs[i].MonitorID].Up++
|
||||
totalDelay[mhs[i].MonitorID] += mhs[i].Delay
|
||||
} else {
|
||||
ServiceSentinelShared.serviceStatusToday[mhs[i].MonitorID].Down++
|
||||
}
|
||||
}
|
||||
for id, delay := range totalDelay {
|
||||
ServiceSentinelShared.serviceStatusToday[id].Delay = delay / float32(ServiceSentinelShared.serviceStatusToday[id].Up)
|
||||
}
|
||||
|
||||
// 更新入库时间及当日数据入库游标
|
||||
for k := range ServiceSentinelShared.monitors {
|
||||
ServiceSentinelShared.latestDate[k] = time.Now().Format("02-Jan-06")
|
||||
}
|
||||
|
||||
go ServiceSentinelShared.worker()
|
||||
}
|
||||
|
||||
/*
|
||||
使用缓存 channel,处理上报的 Service 请求结果,然后判断是否需要报警
|
||||
需要记录上一次的状态信息
|
||||
*/
|
||||
type ServiceSentinel struct {
|
||||
serviceResponseDataStoreLock sync.RWMutex
|
||||
monitorsLock sync.RWMutex
|
||||
serviceReportChannel chan ReportData
|
||||
serviceStatusToday map[uint64]*_TodayStatsOfMonitor
|
||||
serviceCurrentStatusIndex map[uint64]int
|
||||
serviceCurrentStatusData map[uint64][]model.MonitorHistory
|
||||
latestDate map[uint64]string
|
||||
lastStatus map[uint64]string
|
||||
serviceResponseDataStoreCurrentUp map[uint64]uint64
|
||||
serviceResponseDataStoreCurrentDown map[uint64]uint64
|
||||
monitors map[uint64]model.Monitor
|
||||
sslCertCache map[uint64]string
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) Dispatch(r ReportData) {
|
||||
ss.serviceReportChannel <- r
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) Monitors() []model.Monitor {
|
||||
ss.monitorsLock.RLock()
|
||||
defer ss.monitorsLock.RUnlock()
|
||||
var monitors []model.Monitor
|
||||
for _, v := range ss.monitors {
|
||||
monitors = append(monitors, v)
|
||||
}
|
||||
sort.SliceStable(monitors, func(i, j int) bool {
|
||||
return monitors[i].ID < monitors[j].ID
|
||||
})
|
||||
return monitors
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) OnMonitorUpdate() {
|
||||
var monitors []model.Monitor
|
||||
DB.Find(&monitors)
|
||||
ss.monitorsLock.Lock()
|
||||
defer ss.monitorsLock.Unlock()
|
||||
ss.monitors = make(map[uint64]model.Monitor)
|
||||
for i := 0; i < len(monitors); i++ {
|
||||
ss.monitors[monitors[i].ID] = monitors[i]
|
||||
if len(ss.serviceCurrentStatusData[monitors[i].ID]) == 0 {
|
||||
ss.serviceCurrentStatusData[monitors[i].ID] = make([]model.MonitorHistory, _CurrentStatusSize)
|
||||
}
|
||||
if ss.serviceStatusToday[monitors[i].ID] == nil {
|
||||
ss.serviceStatusToday[monitors[i].ID] = &_TodayStatsOfMonitor{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) OnMonitorDelete(id uint64) {
|
||||
ss.serviceResponseDataStoreLock.Lock()
|
||||
defer ss.serviceResponseDataStoreLock.Unlock()
|
||||
delete(ss.serviceCurrentStatusIndex, id)
|
||||
delete(ss.serviceCurrentStatusData, id)
|
||||
delete(ss.latestDate, id)
|
||||
delete(ss.lastStatus, id)
|
||||
delete(ss.serviceResponseDataStoreCurrentUp, id)
|
||||
delete(ss.serviceResponseDataStoreCurrentDown, id)
|
||||
delete(ss.sslCertCache, id)
|
||||
ss.monitorsLock.Lock()
|
||||
defer ss.monitorsLock.Unlock()
|
||||
delete(ss.monitors, id)
|
||||
Cache.Delete(model.CacheKeyServicePage)
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) LoadStats() map[uint64]*model.ServiceItemResponse {
|
||||
var cached bool
|
||||
var msm map[uint64]*model.ServiceItemResponse
|
||||
data, has := Cache.Get(model.CacheKeyServicePage)
|
||||
if has {
|
||||
msm = data.(map[uint64]*model.ServiceItemResponse)
|
||||
cached = true
|
||||
}
|
||||
if !cached {
|
||||
msm = make(map[uint64]*model.ServiceItemResponse)
|
||||
var ms []model.Monitor
|
||||
DB.Find(&ms)
|
||||
year, month, day := time.Now().Date()
|
||||
today := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
|
||||
var mhs []model.MonitorHistory
|
||||
DB.Where("created_at >= ? AND created_at < ?", today.AddDate(0, 0, -29), today).Find(&mhs)
|
||||
for i := 0; i < len(ms); i++ {
|
||||
msm[ms[i].ID] = &model.ServiceItemResponse{
|
||||
Monitor: ms[i],
|
||||
Delay: &[30]float32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
Up: &[30]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
Down: &[30]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
}
|
||||
}
|
||||
// 整合数据
|
||||
for i := 0; i < len(mhs); i++ {
|
||||
dayIndex := 28 - (int(today.Sub(mhs[i].CreatedAt).Hours()) / 24)
|
||||
if mhs[i].Successful {
|
||||
msm[mhs[i].MonitorID].TotalUp++
|
||||
msm[mhs[i].MonitorID].Delay[dayIndex] = (msm[mhs[i].MonitorID].Delay[dayIndex]*float32(msm[mhs[i].MonitorID].Up[dayIndex]) + mhs[i].Delay) / float32(msm[mhs[i].MonitorID].Up[dayIndex]+1)
|
||||
msm[mhs[i].MonitorID].Up[dayIndex]++
|
||||
} else {
|
||||
msm[mhs[i].MonitorID].TotalDown++
|
||||
msm[mhs[i].MonitorID].Down[dayIndex]++
|
||||
}
|
||||
}
|
||||
// 缓存一天
|
||||
Cache.Set(model.CacheKeyServicePage, msm, time.Until(time.Date(year, month, day, 23, 59, 59, 999, today.Location())))
|
||||
}
|
||||
// 最新一天的数据
|
||||
ss.serviceResponseDataStoreLock.RLock()
|
||||
defer ss.serviceResponseDataStoreLock.RUnlock()
|
||||
for k := range ss.monitors {
|
||||
if msm[k] == nil {
|
||||
msm[k] = &model.ServiceItemResponse{
|
||||
Up: new([30]int),
|
||||
Down: new([30]int),
|
||||
Delay: new([30]float32),
|
||||
}
|
||||
}
|
||||
msm[k].Monitor = ss.monitors[k]
|
||||
v := ss.serviceStatusToday[k]
|
||||
msm[k].Up[29] = v.Up
|
||||
msm[k].Down[29] = v.Down
|
||||
msm[k].TotalUp += uint64(v.Up)
|
||||
msm[k].TotalDown += uint64(v.Down)
|
||||
msm[k].Delay[29] = v.Delay
|
||||
}
|
||||
// 最后 5 分钟的状态 与 monitor 对象填充
|
||||
for k, v := range ss.serviceResponseDataStoreCurrentDown {
|
||||
msm[k].CurrentDown = v
|
||||
}
|
||||
for k, v := range ss.serviceResponseDataStoreCurrentUp {
|
||||
msm[k].CurrentUp = v
|
||||
}
|
||||
return msm
|
||||
}
|
||||
|
||||
func getStateStr(percent uint64) string {
|
||||
if percent == 0 {
|
||||
return "无数据"
|
||||
}
|
||||
if percent > 95 {
|
||||
return "良好"
|
||||
}
|
||||
if percent > 80 {
|
||||
return "低可用"
|
||||
}
|
||||
return "故障"
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) worker() {
|
||||
for r := range ss.serviceReportChannel {
|
||||
if ss.monitors[r.Data.GetId()].ID == 0 {
|
||||
continue
|
||||
}
|
||||
mh := model.PB2MonitorHistory(r.Data)
|
||||
ss.serviceResponseDataStoreLock.Lock()
|
||||
// 先查看是否到下一天
|
||||
nowDate := time.Now().Format("02-Jan-06")
|
||||
if nowDate != ss.latestDate[mh.MonitorID] {
|
||||
// 清理前一天数据
|
||||
ss.latestDate[mh.MonitorID] = nowDate
|
||||
ss.serviceResponseDataStoreCurrentUp[mh.MonitorID] = 0
|
||||
ss.serviceResponseDataStoreCurrentDown[mh.MonitorID] = 0
|
||||
ss.serviceStatusToday[mh.MonitorID].Delay = 0
|
||||
ss.serviceStatusToday[mh.MonitorID].Up = 0
|
||||
ss.serviceStatusToday[mh.MonitorID].Down = 0
|
||||
}
|
||||
// 写入当天状态
|
||||
if mh.Successful {
|
||||
ss.serviceStatusToday[mh.MonitorID].Delay = (ss.serviceStatusToday[mh.
|
||||
MonitorID].Delay*float32(ss.serviceStatusToday[mh.MonitorID].Up) +
|
||||
mh.Delay) / float32(ss.serviceStatusToday[mh.MonitorID].Up+1)
|
||||
ss.serviceStatusToday[mh.MonitorID].Up++
|
||||
} else {
|
||||
ss.serviceStatusToday[mh.MonitorID].Down++
|
||||
}
|
||||
// 写入当前数据
|
||||
ss.serviceCurrentStatusData[mh.MonitorID][ss.serviceCurrentStatusIndex[mh.MonitorID]] = mh
|
||||
ss.serviceCurrentStatusIndex[mh.MonitorID]++
|
||||
// 数据持久化
|
||||
if ss.serviceCurrentStatusIndex[mh.MonitorID] == _CurrentStatusSize {
|
||||
ss.serviceCurrentStatusIndex[mh.MonitorID] = 0
|
||||
dataToSave := ss.serviceCurrentStatusData[mh.MonitorID]
|
||||
if err := DB.Create(&dataToSave).Error; err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
// 更新当前状态
|
||||
ss.serviceResponseDataStoreCurrentUp[mh.MonitorID] = 0
|
||||
ss.serviceResponseDataStoreCurrentDown[mh.MonitorID] = 0
|
||||
for i := 0; i < len(ss.serviceCurrentStatusData[mh.MonitorID]); i++ {
|
||||
if ss.serviceCurrentStatusData[mh.MonitorID][i].MonitorID > 0 {
|
||||
if ss.serviceCurrentStatusData[mh.MonitorID][i].Successful {
|
||||
ss.serviceResponseDataStoreCurrentUp[mh.MonitorID]++
|
||||
} else {
|
||||
ss.serviceResponseDataStoreCurrentDown[mh.MonitorID]++
|
||||
}
|
||||
}
|
||||
}
|
||||
var upPercent uint64 = 0
|
||||
if ss.serviceResponseDataStoreCurrentDown[mh.MonitorID]+ss.serviceResponseDataStoreCurrentUp[mh.MonitorID] > 0 {
|
||||
upPercent = ss.serviceResponseDataStoreCurrentUp[mh.MonitorID] * 100 / (ss.serviceResponseDataStoreCurrentDown[mh.MonitorID] + ss.serviceResponseDataStoreCurrentUp[mh.MonitorID])
|
||||
}
|
||||
stateStr := getStateStr(upPercent)
|
||||
if Conf.Debug {
|
||||
log.Println(ss.monitors[mh.MonitorID].Target, stateStr, "Agent:", r.Reporter, "Successful:", mh.Successful, "Response:", mh.Data)
|
||||
}
|
||||
if stateStr == "故障" || stateStr != ss.lastStatus[mh.MonitorID] {
|
||||
ss.monitorsLock.RLock()
|
||||
isNeedSendNotification := (ss.lastStatus[mh.MonitorID] != "" || stateStr == "故障") && ss.monitors[mh.MonitorID].Notify
|
||||
ss.lastStatus[mh.MonitorID] = stateStr
|
||||
if isNeedSendNotification {
|
||||
go SendNotification(fmt.Sprintf("服务监控:%s 服务状态:%s", ss.monitors[mh.MonitorID].Name, stateStr), true)
|
||||
}
|
||||
ss.monitorsLock.RUnlock()
|
||||
}
|
||||
ss.serviceResponseDataStoreLock.Unlock()
|
||||
// SSL 证书报警
|
||||
var errMsg string
|
||||
if strings.HasPrefix(mh.Data, "SSL证书错误:") {
|
||||
// 排除 i/o timeont、connection timeout、EOF 错误
|
||||
if !strings.HasSuffix(mh.Data, "timeout") &&
|
||||
!strings.HasSuffix(mh.Data, "EOF") &&
|
||||
!strings.HasSuffix(mh.Data, "timed out") {
|
||||
errMsg = mh.Data
|
||||
}
|
||||
} else {
|
||||
var newCert = strings.Split(mh.Data, "|")
|
||||
if len(newCert) > 1 {
|
||||
if ss.sslCertCache[mh.MonitorID] == "" {
|
||||
ss.sslCertCache[mh.MonitorID] = mh.Data
|
||||
}
|
||||
expiresNew, _ := time.Parse("2006-01-02 15:04:05 -0700 MST", newCert[1])
|
||||
// 证书过期提醒
|
||||
if expiresNew.Before(time.Now().AddDate(0, 0, 7)) {
|
||||
errMsg = fmt.Sprintf(
|
||||
"SSL证书将在七天内过期,过期时间:%s。",
|
||||
expiresNew.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
// 证书变更提醒
|
||||
var oldCert = strings.Split(ss.sslCertCache[mh.MonitorID], "|")
|
||||
var expiresOld time.Time
|
||||
if len(oldCert) > 1 {
|
||||
expiresOld, _ = time.Parse("2006-01-02 15:04:05 -0700 MST", oldCert[1])
|
||||
}
|
||||
if oldCert[0] != newCert[0] && !expiresNew.Equal(expiresOld) {
|
||||
errMsg = fmt.Sprintf(
|
||||
"SSL证书变更,旧:%s, %s 过期;新:%s, %s 过期。",
|
||||
oldCert[0], expiresOld.Format("2006-01-02 15:04:05"), newCert[0], expiresNew.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
}
|
||||
}
|
||||
if errMsg != "" {
|
||||
ss.monitorsLock.RLock()
|
||||
if ss.monitors[mh.MonitorID].Notify {
|
||||
go SendNotification(fmt.Sprintf("服务监控:%s %s", ss.monitors[mh.MonitorID].Name, errMsg), true)
|
||||
}
|
||||
ss.monitorsLock.RUnlock()
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ func (a *AuthHandler) GetRequestMetadata(ctx context.Context, uri ...string) (ma
|
||||
}
|
||||
|
||||
func (a *AuthHandler) RequireTransportSecurity() bool {
|
||||
return !dao.Conf.Debug
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *AuthHandler) Check(ctx context.Context) (uint64, error) {
|
||||
|
@ -3,8 +3,6 @@ package rpc
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
@ -22,49 +20,12 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece
|
||||
if clientID, err = s.Auth.Check(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.GetType() == model.TaskTypeHTTPGET {
|
||||
// SSL 证书报警
|
||||
var errMsg string
|
||||
if strings.HasPrefix(r.GetData(), "SSL证书错误:") {
|
||||
// 排除 i/o timeont、connection timeout、EOF 错误
|
||||
if !strings.HasSuffix(r.GetData(), "timeout") &&
|
||||
!strings.HasSuffix(r.GetData(), "EOF") &&
|
||||
!strings.HasSuffix(r.GetData(), "timed out") {
|
||||
errMsg = r.GetData()
|
||||
}
|
||||
} else {
|
||||
var last model.MonitorHistory
|
||||
var newCert = strings.Split(r.GetData(), "|")
|
||||
if len(newCert) > 1 {
|
||||
expiresNew, _ := time.Parse("2006-01-02 15:04:05 -0700 MST", newCert[1])
|
||||
// 证书过期提醒
|
||||
if expiresNew.Before(time.Now().AddDate(0, 0, 7)) {
|
||||
errMsg = fmt.Sprintf(
|
||||
"SSL证书将在七天内过期,过期时间:%s。",
|
||||
expiresNew.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
// 证书变更提醒
|
||||
if err := dao.DB.Where("monitor_id = ? AND data LIKE ?", r.GetId(), "%|%").Order("id DESC").First(&last).Error; err == nil {
|
||||
var oldCert = strings.Split(last.Data, "|")
|
||||
var expiresOld time.Time
|
||||
if len(oldCert) > 1 {
|
||||
expiresOld, _ = time.Parse("2006-01-02 15:04:05 -0700 MST", oldCert[1])
|
||||
}
|
||||
if last.Data != "" && oldCert[0] != newCert[0] && !expiresNew.Equal(expiresOld) {
|
||||
errMsg = fmt.Sprintf(
|
||||
"SSL证书变更,旧:%s, %s 过期;新:%s, %s 过期。",
|
||||
oldCert[0], expiresOld.Format("2006-01-02 15:04:05"), newCert[0], expiresNew.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if errMsg != "" {
|
||||
var monitor model.Monitor
|
||||
dao.DB.First(&monitor, "id = ?", r.GetId())
|
||||
dao.SendNotification(fmt.Sprintf("服务监控:%s %s", monitor.Name, errMsg), true)
|
||||
}
|
||||
}
|
||||
if r.GetType() == model.TaskTypeCommand {
|
||||
if r.GetType() != model.TaskTypeCommand {
|
||||
dao.ServiceSentinelShared.Dispatch(dao.ReportData{
|
||||
Data: r,
|
||||
Reporter: clientID,
|
||||
})
|
||||
} else {
|
||||
// 处理上报的计划任务
|
||||
dao.CronLock.RLock()
|
||||
defer dao.CronLock.RUnlock()
|
||||
@ -81,12 +42,6 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece
|
||||
LastResult: r.GetSuccessful(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 存入历史记录
|
||||
mh := model.PB2MonitorHistory(r)
|
||||
if err := dao.DB.Create(&mh).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &pb.Receipt{Proced: true}, nil
|
||||
}
|
||||
@ -102,10 +57,7 @@ func (s *NezhaHandler) RequestTask(h *pb.Host, stream pb.NezhaService_RequestTas
|
||||
dao.ServerList[clientID].TaskStream = stream
|
||||
dao.ServerList[clientID].TaskClose = closeCh
|
||||
dao.ServerLock.RUnlock()
|
||||
select {
|
||||
case err = <-closeCh:
|
||||
return err
|
||||
}
|
||||
return <-closeCh
|
||||
}
|
||||
|
||||
func (s *NezhaHandler) ReportSystemState(c context.Context, r *pb.State) (*pb.Receipt, error) {
|
||||
@ -131,7 +83,6 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
|
||||
host := model.PB2Host(r)
|
||||
dao.ServerLock.RLock()
|
||||
defer dao.ServerLock.RUnlock()
|
||||
log.Println(dao.Conf.IgnoredIPNotificationServerIDs)
|
||||
if dao.Conf.EnableIPChangeNotification &&
|
||||
dao.Conf.IgnoredIPNotificationServerIDs[clientID] != struct{}{} &&
|
||||
dao.ServerList[clientID].Host != nil &&
|
||||
|
Loading…
x
Reference in New Issue
Block a user