This commit is contained in:
Erope 2021-05-13 17:55:15 +08:00
commit e6385773ba
37 changed files with 1093 additions and 606 deletions

View File

@ -10,6 +10,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - 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 - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2 uses: goreleaser/goreleaser-action@v2
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')

View File

@ -10,6 +10,7 @@ on:
- "script/**" - "script/**"
- "*.md" - "*.md"
- ".*" - ".*"
- ".github/workflows/agent.yml"
jobs: jobs:
deploy: deploy:

View File

@ -15,11 +15,19 @@ builds:
- 386 - 386
- amd64 - amd64
- mips - mips
- mips64
gomips: gomips:
- softfloat - softfloat
ignore:
- goos: windows
goarch: arm
- goos: windows
goarch: arm64
main: ./cmd/agent main: ./cmd/agent
binary: nezha-agent binary: nezha-agent
# 为路由器、开发板缩小二进制体积
hooks:
post:
- upx --best "{{.Path}}"
checksum: checksum:
name_template: "checksums.txt" name_template: "checksums.txt"
snapshot: snapshot:

View File

@ -1,13 +1,14 @@
<div align="center" style="background-color: white"> <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="哪吒监控"> <img width="500" style="max-width:100%" src="https://raw.githubusercontent.com/naiba/nezha/master/resource/static/brand.png" title="哪吒监控">
<br><br> <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">&nbsp;<img src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/workflow/status/naiba/nezha/Agent%20release?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<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">&nbsp;<img src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/workflow/status/naiba/nezha/Agent%20release?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.5.0-brightgreen?style=for-the-badge&logo=linux">
<br> <br>
<p>:trollface: 哪吒监控 一站式轻监控轻运维系统。支持系统状态、HTTP(SSL 证书变更、即将到期、到期)、TCP、Ping 监控报警,命令批量执行和计划任务。</p> <p>:trollface: 哪吒监控 一站式轻监控轻运维系统。支持系统状态、HTTP(SSL 证书变更、即将到期、到期)、TCP、Ping 监控报警,命令批量执行和计划任务。</p>
</div> </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) \>> [我们的用户](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 ```shell
curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install.sh -o nezha.sh && chmod +x nezha.sh 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. 添加通知方式 1. 添加通知方式
- server 酱示例 - server 酱示例
- 名称server 酱 - 名称server 酱
- URLhttps://sc.ftqq.com/SCUrandomkeys.send?text=#NEZHA# - URLhttps://sc.ftqq.com/SCUrandomkeys.send?text=#NEZHA#
- 请求方式: GET - 请求方式: GET
- 请求类型: 默认 - 请求类型: 默认
- Body: 空 - Body: 空
- wxpusher 示例,需要关注你的应用 - wxpusher 示例,需要关注你的应用
- 名称: wxpusher - 名称: wxpusher
@ -72,6 +75,7 @@ URL 里面也可放置占位符,请求时会进行简单的字符串替换。
- Body: `{"appToken":"你的appToken","topicIds":[],"content":"#NEZHA#","contentType":"1","uids":["你的uid"]}` - Body: `{"appToken":"你的appToken","topicIds":[],"content":"#NEZHA#","contentType":"1","uids":["你的uid"]}`
- telegram 示例 [@haitau](https://github.com/haitau) 贡献 - telegram 示例 [@haitau](https://github.com/haitau) 贡献
- 名称telegram 机器人消息通知 - 名称telegram 机器人消息通知
- URLhttps://api.telegram.org/botXXXXXX/sendMessage?chat_id=YYYYYY&text=#NEZHA# - URLhttps://api.telegram.org/botXXXXXX/sendMessage?chat_id=YYYYYY&text=#NEZHA#
- 请求方式: GET - 请求方式: GET
@ -121,7 +125,17 @@ URL 里面也可放置占位符,请求时会进行简单的字符串替换。
</style> </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> <style>
@ -147,15 +161,27 @@ URL 里面也可放置占位符,请求时会进行简单的字符串替换。
</script> </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 主题更改背景图片示例 - hotaru 主题更改背景图片示例
``` ```
<style> <style>
.hotaru-cover { .hotaru-cover {
background: url(https://s3.ax1x.com/2020/12/08/DzHv6A.jpg) center; background: url(https://s3.ax1x.com/2020/12/08/DzHv6A.jpg) center;
} }
</style> </style>
``` ```
</details> </details>

View File

@ -9,9 +9,9 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"net/url"
"os" "os"
"os/exec" "os/exec"
"strings"
"time" "time"
"github.com/blang/semver" "github.com/blang/semver"
@ -35,11 +35,10 @@ var (
) )
var ( var (
reporting bool
client pb.NezhaServiceClient client pb.NezhaServiceClient
ctx = context.Background() ctx = context.Background()
delayWhenError = time.Second * 10 // Agent 重连间隔 delayWhenError = time.Second * 10 // Agent 重连间隔
updateCh = make(chan struct{}, 0) // Agent 自动更新间隔 updateCh = make(chan struct{}) // Agent 自动更新间隔
httpClient = &http.Client{ httpClient = &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
@ -56,17 +55,16 @@ func doSelfUpdate() {
updateCh <- struct{}{} updateCh <- struct{}{}
}() }()
v := semver.MustParse(version) v := semver.MustParse(version)
log.Println("Check update", v) println("Check update", v)
latest, err := selfupdate.UpdateSelf(v, "naiba/nezha") latest, err := selfupdate.UpdateSelf(v, "naiba/nezha")
if err != nil { if err != nil {
log.Println("Binary update failed:", err) println("Binary update failed:", err)
return return
} }
if latest.Version.Equals(v) { if latest.Version.Equals(v) {
// latest version is the same as current version. It means current binary is up to date. println("Current binary is up to date", version)
log.Println("Current binary is the latest version", version)
} else { } else {
log.Println("Successfully updated to version", latest.Version) println("Upgrade successfully", latest.Version)
os.Exit(1) os.Exit(1)
} }
} }
@ -81,7 +79,7 @@ func main() {
var debug bool var debug bool
flag.String("i", "", "unused 旧Agent配置兼容") flag.String("i", "", "unused 旧Agent配置兼容")
flag.BoolVar(&debug, "d", false, "允许不安全连接") flag.BoolVar(&debug, "d", false, "开启调试信息")
flag.StringVar(&server, "s", "localhost:5555", "管理面板RPC端口") flag.StringVar(&server, "s", "localhost:5555", "管理面板RPC端口")
flag.StringVar(&clientSecret, "p", "", "Agent连接Secret") flag.StringVar(&clientSecret, "p", "", "Agent连接Secret")
flag.Parse() flag.Parse()
@ -121,18 +119,18 @@ func run() {
var conn *grpc.ClientConn var conn *grpc.ClientConn
retry := func() { retry := func() {
log.Println("Error to close connection ...") println("Error to close connection ...")
if conn != nil { if conn != nil {
conn.Close() conn.Close()
} }
time.Sleep(delayWhenError) time.Sleep(delayWhenError)
log.Println("Try to reconnect ...") println("Try to reconnect ...")
} }
for { for {
conn, err = grpc.Dial(server, grpc.WithInsecure(), grpc.WithPerRPCCredentials(&auth)) conn, err = grpc.Dial(server, grpc.WithInsecure(), grpc.WithPerRPCCredentials(&auth))
if err != nil { if err != nil {
log.Printf("grpc.Dial err: %v", err) println("grpc.Dial err: ", err)
retry() retry()
continue continue
} }
@ -140,26 +138,26 @@ func run() {
// 第一步注册 // 第一步注册
_, err = client.ReportSystemInfo(ctx, monitor.GetHost().PB()) _, err = client.ReportSystemInfo(ctx, monitor.GetHost().PB())
if err != nil { if err != nil {
log.Printf("client.ReportSystemInfo err: %v", err) println("client.ReportSystemInfo err: ", err)
retry() retry()
continue continue
} }
// 执行 Task // 执行 Task
tasks, err := client.RequestTask(ctx, monitor.GetHost().PB()) tasks, err := client.RequestTask(ctx, monitor.GetHost().PB())
if err != nil { if err != nil {
log.Printf("client.RequestTask err: %v", err) println("client.RequestTask err: ", err)
retry() retry()
continue continue
} }
err = receiveTasks(tasks) err = receiveTasks(tasks)
log.Printf("receiveTasks exit to main: %v", err) println("receiveTasks exit to main: ", err)
retry() retry()
} }
} }
func receiveTasks(tasks pb.NezhaService_RequestTaskClient) error { func receiveTasks(tasks pb.NezhaService_RequestTaskClient) error {
var err error var err error
defer log.Printf("receiveTasks exit %v => %v", time.Now(), err) defer println("receiveTasks exit", time.Now(), "=>", err)
for { for {
var task *pb.Task var task *pb.Task
task, err = tasks.Recv() task, err = tasks.Recv()
@ -179,24 +177,32 @@ func doTask(task *pb.Task) {
start := time.Now() start := time.Now()
resp, err := httpClient.Get(task.GetData()) resp, err := httpClient.Get(task.GetData())
if err == nil { 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 { if resp.StatusCode > 399 || resp.StatusCode < 200 {
err = errors.New("\n应用错误" + resp.Status) err = errors.New("\n应用错误" + resp.Status)
} }
} }
if err == nil { if err == nil {
if strings.HasPrefix(task.GetData(), "https://") { // 检查 SSL 证书信息
c := cert.NewCert(task.GetData()[8:]) serviceUrl, err := url.Parse(task.GetData())
if c.Error != "" { if err == nil {
result.Data = "SSL证书错误" + c.Error 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 { } else {
result.Data = c.Issuer + "|" + c.NotAfter
result.Successful = true result.Successful = true
} }
} else { } else {
result.Successful = true result.Data = "URL解析错误" + err.Error()
} }
} else { } else {
// HTTP 请求失败
result.Data = err.Error() result.Data = err.Error()
} }
case model.TaskTypeICMPPing: case model.TaskTypeICMPPing:
@ -219,7 +225,7 @@ func doTask(task *pb.Task) {
if err == nil { if err == nil {
conn.Write([]byte("ping\n")) conn.Write([]byte("ping\n"))
conn.Close() conn.Close()
result.Delay = float32(time.Now().Sub(start).Microseconds()) / 1000.0 result.Delay = float32(time.Since(start).Microseconds()) / 1000.0
result.Successful = true result.Successful = true
} else { } else {
result.Data = err.Error() result.Data = err.Error()
@ -260,9 +266,9 @@ func doTask(task *pb.Task) {
result.Data = string(output) result.Data = string(output)
result.Successful = true result.Successful = true
} }
result.Delay = float32(time.Now().Sub(startedAt).Seconds()) result.Delay = float32(time.Since(startedAt).Seconds())
default: default:
log.Printf("Unknown action: %v", task) println("Unknown action: ", task)
} }
client.ReportTask(ctx, &result) client.ReportTask(ctx, &result)
} }
@ -270,13 +276,13 @@ func doTask(task *pb.Task) {
func reportState() { func reportState() {
var lastReportHostInfo time.Time var lastReportHostInfo time.Time
var err error var err error
defer log.Printf("reportState exit %v => %v", time.Now(), err) defer println("reportState exit", time.Now(), "=>", err)
for { for {
if client != nil { if client != nil {
monitor.TrackNetworkSpeed() monitor.TrackNetworkSpeed()
_, err = client.ReportSystemState(ctx, monitor.GetState(dao.ReportDelay).PB()) _, err = client.ReportSystemState(ctx, monitor.GetState(dao.ReportDelay).PB())
if err != nil { if err != nil {
log.Printf("reportState error %v", err) println("reportState error", err)
time.Sleep(delayWhenError) time.Sleep(delayWhenError)
} }
if lastReportHostInfo.Before(time.Now().Add(-10 * time.Minute)) { 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...)
}
}

View File

@ -2,6 +2,7 @@ package monitor
import ( import (
"fmt" "fmt"
"regexp"
"strings" "strings"
"sync/atomic" "sync/atomic"
"time" "time"
@ -17,12 +18,19 @@ import (
) )
var netInSpeed, netOutSpeed, netInTransfer, netOutTransfer, lastUpdate uint64 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 { func GetHost() *model.Host {
hi, _ := host.Info() hi, _ := host.Info()
var cpuType string var cpuType string
if hi.VirtualizationSystem != "" { if hi.VirtualizationSystem != "" {
cpuType = "Virtual" cpuType = "Vrtual"
} else { } else {
cpuType = "Physical" cpuType = "Physical"
} }
@ -37,15 +45,15 @@ func GetHost() *model.Host {
} }
mv, _ := mem.VirtualMemory() mv, _ := mem.VirtualMemory()
ms, _ := mem.SwapMemory() ms, _ := mem.SwapMemory()
u, _ := disk.Usage("/") diskTotal, _ := getDiskTotalAndUsed()
return &model.Host{ return &model.Host{
Platform: hi.OS, Platform: hi.OS,
PlatformVersion: hi.PlatformVersion, PlatformVersion: hi.PlatformVersion,
CPU: cpus, CPU: cpus,
MemTotal: mv.Total, MemTotal: mv.Total,
DiskTotal: u.Total,
SwapTotal: ms.Total, SwapTotal: ms.Total,
DiskTotal: diskTotal,
Arch: hi.KernelArch, Arch: hi.KernelArch,
Virtualization: hi.VirtualizationSystem, Virtualization: hi.VirtualizationSystem,
BootTime: hi.BootTime, BootTime: hi.BootTime,
@ -57,23 +65,19 @@ func GetHost() *model.Host {
func GetState(delay int64) *model.HostState { func GetState(delay int64) *model.HostState {
hi, _ := host.Info() hi, _ := host.Info()
// Memory
mv, _ := mem.VirtualMemory() mv, _ := mem.VirtualMemory()
ms, _ := mem.SwapMemory() ms, _ := mem.SwapMemory()
// CPU
var cpuPercent float64 var cpuPercent float64
cp, err := cpu.Percent(time.Second*time.Duration(delay), false) cp, err := cpu.Percent(time.Second*time.Duration(delay), false)
if err == nil { if err == nil {
cpuPercent = cp[0] cpuPercent = cp[0]
} }
// Disk _, diskUsed := getDiskTotalAndUsed()
u, _ := disk.Usage("/")
return &model.HostState{ return &model.HostState{
CPU: cpuPercent, CPU: cpuPercent,
MemUsed: mv.Used, MemUsed: mv.Used,
SwapUsed: ms.Used, SwapUsed: ms.Used,
DiskUsed: u.Used, DiskUsed: diskUsed,
NetInTransfer: atomic.LoadUint64(&netInTransfer), NetInTransfer: atomic.LoadUint64(&netInTransfer),
NetOutTransfer: atomic.LoadUint64(&netOutTransfer), NetOutTransfer: atomic.LoadUint64(&netOutTransfer),
NetInSpeed: atomic.LoadUint64(&netInSpeed), NetInSpeed: atomic.LoadUint64(&netInSpeed),
@ -84,10 +88,15 @@ func GetState(delay int64) *model.HostState {
func TrackNetworkSpeed() { func TrackNetworkSpeed() {
var innerNetInTransfer, innerNetOutTransfer uint64 var innerNetInTransfer, innerNetOutTransfer uint64
nc, err := net.IOCounters(false) nc, err := net.IOCounters(true)
if err == nil { if err == nil {
innerNetInTransfer += nc[0].BytesRecv for _, v := range nc {
innerNetOutTransfer += nc[0].BytesSent if isListContainsStr(excludeNetInterfaces, v.Name) {
continue
}
innerNetInTransfer += v.BytesRecv
innerNetOutTransfer += v.BytesSent
}
now := uint64(time.Now().Unix()) now := uint64(time.Now().Unix())
diff := now - atomic.LoadUint64(&lastUpdate) diff := now - atomic.LoadUint64(&lastUpdate)
if diff > 0 { if diff > 0 {
@ -99,3 +108,40 @@ func TrackNetworkSpeed() {
atomic.StoreUint64(&lastUpdate, now) 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
}

View File

@ -3,7 +3,6 @@ package controller
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"net/http" "net/http"
"time" "time"
@ -80,79 +79,8 @@ func (p *commonPage) checkViewPassword(c *gin.Context) {
c.Next() 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) { func (p *commonPage) service(c *gin.Context) {
var msm map[uint64]*ServiceItem msm := dao.ServiceSentinelShared.LoadStats()
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)
}
c.HTML(http.StatusOK, "theme-"+dao.Conf.Site.Theme+"/service", mygin.CommonEnvironment(c, gin.H{ c.HTML(http.StatusOK, "theme-"+dao.Conf.Site.Theme+"/service", mygin.CommonEnvironment(c, gin.H{
"Title": "服务状态", "Title": "服务状态",
"Services": msm, "Services": msm,

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"code.cloudfoundry.org/bytefmt" "code.cloudfoundry.org/bytefmt"
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/naiba/nezha/pkg/mygin" "github.com/naiba/nezha/pkg/mygin"
@ -15,10 +16,11 @@ import (
func ServeWeb(port uint) { func ServeWeb(port uint) {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
r := gin.Default()
if dao.Conf.Debug { if dao.Conf.Debug {
gin.SetMode(gin.DebugMode) gin.SetMode(gin.DebugMode)
pprof.Register(r)
} }
r := gin.Default()
r.Use(mygin.RecordPath) r.Use(mygin.RecordPath)
r.SetFuncMap(template.FuncMap{ r.SetFuncMap(template.FuncMap{
"tf": func(t time.Time) string { "tf": func(t time.Time) string {
@ -54,7 +56,7 @@ func ServeWeb(port uint) {
} }
if a == 0 { if a == 0 {
// 这是从未在线的情况 // 这是从未在线的情况
return 1 / float32(b) * 100 return 0.00001 / float32(b) * 100
} }
return float32(a) / float32(b) * 100 return float32(a) / float32(b) * 100
}, },
@ -67,7 +69,7 @@ func ServeWeb(port uint) {
} }
if a == 0 { if a == 0 {
// 这是从未在线的情况 // 这是从未在线的情况
return 1 / float32(b) * 100 return 0.00001 / float32(b) * 100
} }
return float32(a) / float32(b) * 100 return float32(a) / float32(b) * 100
}, },

View File

@ -7,8 +7,6 @@ import (
"github.com/naiba/nezha/model" "github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin" "github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/service/dao" "github.com/naiba/nezha/service/dao"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
) )
type guestPage struct { type guestPage struct {
@ -27,31 +25,22 @@ func (gp *guestPage) serve() {
gr.GET("/login", gp.login) 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{ oauth := &oauth2controller{
oauth2Config: &oauth2.Config{
ClientID: dao.Conf.Oauth2.ClientID,
ClientSecret: dao.Conf.Oauth2.ClientSecret,
Scopes: []string{},
Endpoint: endPoint,
},
r: gr, r: gr,
} }
oauth.serve() oauth.serve()
} }
func (gp *guestPage) login(c *gin.Context) { 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{ c.HTML(http.StatusOK, "dashboard/login", mygin.CommonEnvironment(c, gin.H{
"Title": "登录", "Title": "登录",
"LoginType": LoginType,
"RegistrationLink": RegistrationLink,
})) }))
} }

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -73,6 +74,7 @@ func (ma *memberAPI) delete(c *gin.Context) {
case "monitor": case "monitor":
err = dao.DB.Delete(&model.Monitor{}, "id = ?", id).Error err = dao.DB.Delete(&model.Monitor{}, "id = ?", id).Error
if err == nil { if err == nil {
dao.ServiceSentinelShared.OnMonitorDelete(id)
err = dao.DB.Delete(&model.MonitorHistory{}, "monitor_id = ?", id).Error err = dao.DB.Delete(&model.MonitorHistory{}, "monitor_id = ?", id).Error
} }
case "cron": case "cron":
@ -190,10 +192,12 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
} }
type monitorForm struct { type monitorForm struct {
ID uint64 ID uint64
Name string Name string
Target string Target string
Type uint8 Type uint8
Notify string
SkipServersRaw string
} }
func (ma *memberAPI) addOrEditMonitor(c *gin.Context) { func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
@ -202,9 +206,11 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
err := c.ShouldBindJSON(&mf) err := c.ShouldBindJSON(&mf)
if err == nil { if err == nil {
m.Name = mf.Name m.Name = mf.Name
m.Target = mf.Target m.Target = strings.TrimSpace(mf.Target)
m.Type = mf.Type m.Type = mf.Type
m.ID = mf.ID m.ID = mf.ID
m.SkipServersRaw = mf.SkipServersRaw
m.Notify = mf.Notify == "on"
} }
if err == nil { if err == nil {
if m.ID == 0 { if m.ID == 0 {
@ -219,6 +225,8 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
Message: fmt.Sprintf("请求错误:%s", err), Message: fmt.Sprintf("请求错误:%s", err),
}) })
return return
} else {
dao.ServiceSentinelShared.OnMonitorUpdate()
} }
c.JSON(http.StatusOK, model.Response{ c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK, Code: http.StatusOK,

View File

@ -39,11 +39,9 @@ func (mp *memberPage) server(c *gin.Context) {
} }
func (mp *memberPage) monitor(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{ c.HTML(http.StatusOK, "dashboard/monitor", mygin.CommonEnvironment(c, gin.H{
"Title": "服务监控", "Title": "服务监控",
"Monitors": monitors, "Monitors": dao.ServiceSentinelShared.Monitors(),
})) }))
} }

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log"
"net/http" "net/http"
"strings" "strings"
@ -12,6 +11,7 @@ import (
"github.com/google/go-github/github" "github.com/google/go-github/github"
GitHubAPI "github.com/google/go-github/github" GitHubAPI "github.com/google/go-github/github"
"golang.org/x/oauth2" "golang.org/x/oauth2"
GitHubOauth2 "golang.org/x/oauth2/github"
"github.com/naiba/nezha/model" "github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin" "github.com/naiba/nezha/pkg/mygin"
@ -20,8 +20,7 @@ import (
) )
type oauth2controller struct { type oauth2controller struct {
oauth2Config *oauth2.Config r gin.IRoutes
r gin.IRoutes
} }
func (oa *oauth2controller) serve() { func (oa *oauth2controller) serve() {
@ -29,39 +28,58 @@ func (oa *oauth2controller) serve() {
oa.r.GET("/oauth2/callback", oa.callback) 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://" schame := "http://"
if strings.HasPrefix(c.Request.Referer(), "https://") { if strings.HasPrefix(c.Request.Referer(), "https://") {
schame = "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) { func (oa *oauth2controller) login(c *gin.Context) {
oa.fillRedirectURL(c)
state := utils.RandStringBytesMaskImprSrcUnsafe(6) state := utils.RandStringBytesMaskImprSrcUnsafe(6)
dao.Cache.Set(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, c.ClientIP()), state, 0) 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) c.Redirect(http.StatusFound, url)
} }
func (oa *oauth2controller) callback(c *gin.Context) { func (oa *oauth2controller) callback(c *gin.Context) {
oa.fillRedirectURL(c)
var err error var err error
// 验证登录跳转时的 State // 验证登录跳转时的 State
state, ok := dao.Cache.Get(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, c.ClientIP())) state, ok := dao.Cache.Get(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, c.ClientIP()))
if !ok || state.(string) != c.Query("state") { if !ok || state.(string) != c.Query("state") {
err = errors.New("非法的登录方式") err = errors.New("非法的登录方式")
} }
oauth2Config := oa.getCommonOauth2Config(c)
ctx := context.Background() ctx := context.Background()
var otk *oauth2.Token var otk *oauth2.Token
if err == nil { if err == nil {
otk, err = oa.oauth2Config.Exchange(ctx, c.Query("code")) otk, err = oauth2Config.Exchange(ctx, c.Query("code"))
} }
var client *GitHubAPI.Client var client *GitHubAPI.Client
if err == nil { if err == nil {
oc := oa.oauth2Config.Client(ctx, otk) oc := oauth2Config.Client(ctx, otk)
if dao.Conf.Oauth2.Type == "gitee" { if dao.Conf.Oauth2.Type == model.ConfigTypeGitee {
client, err = GitHubAPI.NewEnterpriseClient("https://gitee.com/api/v5/", "https://gitee.com/api/v5/", oc) client, err = GitHubAPI.NewEnterpriseClient("https://gitee.com/api/v5/", "https://gitee.com/api/v5/", oc)
} else { } else {
client = GitHubAPI.NewClient(oc) client = GitHubAPI.NewClient(oc)
@ -71,7 +89,6 @@ func (oa *oauth2controller) callback(c *gin.Context) {
if err == nil { if err == nil {
gu, _, err = client.Users.Get(ctx, "") gu, _, err = client.Users.Get(ctx, "")
} }
log.Printf("%+v", gu)
if err != nil { if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{ mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,

View File

@ -52,6 +52,7 @@ func initSystem() {
dao.DB.AutoMigrate(model.Server{}, model.User{}, dao.DB.AutoMigrate(model.Server{}, model.User{},
model.Notification{}, model.AlertRule{}, model.Monitor{}, model.Notification{}, model.AlertRule{}, model.Monitor{},
model.MonitorHistory{}, model.Cron{}) model.MonitorHistory{}, model.Cron{})
dao.NewServiceSentinel()
loadServers() //加载服务器列表 loadServers() //加载服务器列表
loadCrons() //加载计划任务 loadCrons() //加载计划任务
@ -109,6 +110,6 @@ func loadCrons() {
func main() { func main() {
go controller.ServeWeb(dao.Conf.HTTPPort) go controller.ServeWeb(dao.Conf.HTTPPort)
go rpc.ServeRPC(dao.Conf.GRPCPort) go rpc.ServeRPC(dao.Conf.GRPCPort)
go rpc.DispatchTask(time.Minute * 3) go rpc.DispatchTask(time.Second * 30)
dao.AlertSentinelStart() dao.AlertSentinelStart()
} }

View File

@ -7,7 +7,6 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"github.com/naiba/nezha/model"
pb "github.com/naiba/nezha/proto" pb "github.com/naiba/nezha/proto"
"github.com/naiba/nezha/service/dao" "github.com/naiba/nezha/service/dao"
rpcService "github.com/naiba/nezha/service/rpc" rpcService "github.com/naiba/nezha/service/rpc"
@ -28,9 +27,8 @@ func ServeRPC(port uint) {
func DispatchTask(duration time.Duration) { func DispatchTask(duration time.Duration) {
var index uint64 = 0 var index uint64 = 0
for { for {
var tasks []model.Monitor
var hasAliveAgent bool var hasAliveAgent bool
dao.DB.Find(&tasks) tasks := dao.ServiceSentinelShared.Monitors()
dao.SortedServerLock.RLock() dao.SortedServerLock.RLock()
startedAt := time.Now() startedAt := time.Now()
for i := 0; i < len(tasks); i++ { for i := 0; i < len(tasks); i++ {
@ -41,7 +39,9 @@ func DispatchTask(duration time.Duration) {
} }
hasAliveAgent = false 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-- i--
index++ index++
continue continue

View File

@ -10,6 +10,7 @@ import (
"os/exec" "os/exec"
"time" "time"
"github.com/genkiroid/cert"
"github.com/go-ping/ping" "github.com/go-ping/ping"
"github.com/naiba/nezha/pkg/utils" "github.com/naiba/nezha/pkg/utils"
"github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/cpu"
@ -20,8 +21,8 @@ import (
func main() { func main() {
// icmp() // icmp()
// tcpping() // tcpping()
// httpWithSSLInfo() httpWithSSLInfo()
sysinfo() // sysinfo()
// cmdExec() // cmdExec()
} }
@ -33,7 +34,7 @@ func tcpping() {
} }
conn.Write([]byte("ping\n")) conn.Write([]byte("ping\n"))
conn.Close() 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() { func sysinfo() {
@ -53,7 +54,6 @@ func sysinfo() {
for model, count := range cpuModelCount { for model, count := range cpuModelCount {
cpus = append(cpus, fmt.Sprintf("%s %d %s Core", model, count, cpuType)) cpus = append(cpus, fmt.Sprintf("%s %d %s Core", model, count, cpuType))
} }
log.Println(cpus)
os.Exit(0) os.Exit(0)
// 硬盘信息,不使用的原因是会重复统计 Linux、Mac // 硬盘信息,不使用的原因是会重复统计 Linux、Mac
dparts, _ := disk.Partitions(false) dparts, _ := disk.Partitions(false)
@ -73,11 +73,12 @@ func httpWithSSLInfo() {
httpClient := &http.Client{Transport: transCfg, CheckRedirect: func(req *http.Request, via []*http.Request) error { httpClient := &http.Client{Transport: transCfg, CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
}} }}
resp, err := httpClient.Get("http://mail.nai.ba") url := "https://ops.naibahq.com"
fmt.Println(err, resp.StatusCode) resp, err := httpClient.Get(url)
fmt.Println(err, resp)
// SSL 证书信息获取 // SSL 证书信息获取
// c := cert.NewCert("expired-ecc-dv.ssl.com") c := cert.NewCert(url[8:])
// fmt.Println(c.Error) fmt.Println(c.Error)
} }
func icmp() { func icmp() {

9
go.mod
View File

@ -7,8 +7,9 @@ require (
github.com/blang/semver v3.5.1+incompatible github.com/blang/semver v3.5.1+incompatible
github.com/fsnotify/fsnotify v1.4.9 github.com/fsnotify/fsnotify v1.4.9
github.com/genkiroid/cert v0.0.0-20191007122723-897560fbbe50 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/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/golang/protobuf v1.4.2
github.com/google/go-github v17.0.0+incompatible github.com/google/go-github v17.0.0+incompatible
github.com/gorilla/websocket v1.4.2 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/p14yground/go-github-selfupdate v1.2.3-0.20210119020835-db3523c6834b
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/robfig/cron/v3 v3.0.1 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/spf13/viper v1.7.1
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 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/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/grpc v1.33.1
google.golang.org/protobuf v1.25.0 google.golang.org/protobuf v1.25.0
gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 v2.2.8
gorm.io/driver/sqlite v1.1.4 gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.20.8 gorm.io/gorm v1.21.8
) )

63
go.sum
View File

@ -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-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/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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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/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/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.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 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 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 h1:vLwmYBduhnWWqShoUGbVgDulhcLdanoYtCQxYMzwaqQ=
github.com/genkiroid/cert v0.0.0-20191007122723-897560fbbe50/go.mod h1:Pb7nyGYAfDyE/IkU6AJeRshIFko0wJC9cOqeYzYQffk= 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/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 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 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 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 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= 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-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 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= 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-20210407214646-e4e642a95741 h1:b0sLP++Tsle+s57tqg5sUk1/OQsC6yMCciVeqNzOcwU=
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/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 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 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= 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/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.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 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/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-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/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/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.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.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.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.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/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.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-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.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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 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 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/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.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.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.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.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.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.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 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 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.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/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.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 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/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/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/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.21.3 h1:wgcdAHZS2H6qy4JFewVTtqfiYxFzCeEJod/mLztdPG8=
github.com/shirou/gopsutil/v3 v3.20.11/go.mod h1:igHnfak0qnw1biGeI2qKQvu0ZkwvEkUcCLlYhZzdr/4= 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/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 h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 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/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/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 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE= 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/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 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 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-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-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-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-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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/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-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-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-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-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-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/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-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-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-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-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-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-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-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-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-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-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/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.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.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.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.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.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.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-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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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/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-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-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-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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/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.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.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.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.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.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= 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-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-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-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-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-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 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-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-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-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-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-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 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.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 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.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.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/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= 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 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 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/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 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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/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.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.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.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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= 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.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.8 h1:iToaOdZgjNvlc44NFkxfLa3U9q63qwaxt0FdNCiwOMs= gorm.io/gorm v1.21.8 h1:2CEwZSzogdhsKPlJ9OvBKTdlWIpELXb6HbfLfMNhSYI=
gorm.io/gorm v1.20.8/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 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-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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/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
View 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
}

View File

@ -1,7 +1,10 @@
package model package model
import ( import (
"encoding/json"
pb "github.com/naiba/nezha/proto" pb "github.com/naiba/nezha/proto"
"gorm.io/gorm"
) )
const ( const (
@ -14,9 +17,13 @@ const (
type Monitor struct { type Monitor struct {
Common Common
Name string Name string
Type uint8 Type uint8
Target string Target string
SkipServersRaw string
Notify bool
SkipServers map[uint64]bool `gorm:"-" json:"-"`
} }
func (m *Monitor) PB() *pb.Task { func (m *Monitor) PB() *pb.Task {
@ -26,3 +33,15 @@ func (m *Monitor) PB() *pb.Task {
Data: m.Target, 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
}

View File

@ -6,7 +6,7 @@
.nb-container { .nb-container {
padding-top: 75px; padding-top: 75px;
min-height: 100%; min-height: 100vh;
padding-bottom: 55px; padding-bottom: 55px;
margin-bottom: -47px; margin-bottom: -47px;
} }

View File

@ -1,273 +1,371 @@
function readableBytes(bytes) { function readableBytes(bytes) {
var i = Math.floor(Math.log(bytes) / Math.log(1024)), var i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
return (bytes / Math.pow(1024, i)).toFixed(0) + ' ' + sizes[i]; 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) { function showConfirm(title, content, callFn, extData) {
const modal = $('.mini.confirm.modal') const modal = $(".mini.confirm.modal");
modal.children('.header').text(title) modal.children(".header").text(title);
modal.children('.content').text(content) modal.children(".content").text(content);
if (confirmBtn.hasClass('loading')) { if (confirmBtn.hasClass("loading")) {
return false return false;
} }
modal.modal({ modal
closable: true, .modal({
onApprove: function () { closable: true,
confirmBtn.toggleClass('loading') onApprove: function () {
callFn(extData) confirmBtn.toggleClass("loading");
return false callFn(extData);
} return false;
}).modal('show') },
})
.modal("show");
} }
function showFormModal(modelSelector, formID, URL, getData) { function showFormModal(modelSelector, formID, URL, getData) {
$(modelSelector).modal({ $(modelSelector)
closable: true, .modal({
onApprove: function () { closable: true,
let success = false onApprove: function () {
const btn = $(modelSelector + ' .positive.button') let success = false;
const form = $(modelSelector + ' form') const btn = $(modelSelector + " .positive.button");
if (btn.hasClass('loading')) { const form = $(modelSelector + " form");
return success if (btn.hasClass("loading")) {
} return success;
form.children('.message').remove() }
btn.toggleClass('loading') form.children(".message").remove();
const data = getData ? getData() : $(formID).serializeArray().reduce(function (obj, item) { btn.toggleClass("loading");
const data = getData
? getData()
: $(formID)
.serializeArray()
.reduce(function (obj, item) {
// ID 类的数据 // ID 类的数据
if ((item.name.endsWith('_id') || if (
item.name === 'id' || item.name === 'ID' || item.name.endsWith("_id") ||
item.name === 'RequestType' || item.name === 'RequestMethod' || item.name === "id" ||
item.name === 'DisplayIndex' || item.name === 'Type')) { item.name === "ID" ||
obj[item.name] = parseInt(item.value); item.name === "RequestType" ||
item.name === "RequestMethod" ||
item.name === "DisplayIndex" ||
item.name === "Type"
) {
obj[item.name] = parseInt(item.value);
} else { } else {
obj[item.name] = item.value; obj[item.name] = item.value;
} }
if (item.name == 'ServersRaw') { if (item.name.endsWith("ServersRaw")) {
if (item.value.length > 2) { if (item.value.length > 2) {
obj[item.name] = '[' + item.value.substr(3, item.value.length - 1) + ']' obj[item.name] = JSON.stringify(
} [...item.value.matchAll(/\d+/gm)].map((k) =>
parseInt(k[0])
)
);
}
} }
return obj; return obj;
}, {}); }, {});
$.post(URL, JSON.stringify(data)).done(function (resp) { $.post(URL, JSON.stringify(data))
if (resp.code == 200) { .done(function (resp) {
if (resp.message) { if (resp.code == 200) {
$.suiAlert({ if (resp.message) {
title: '操作成功', $.suiAlert({
type: 'success', title: "操作成功",
description: resp.message, type: "success",
time: '3', description: resp.message,
position: 'top-center', time: "3",
}); position: "top-center",
} });
window.location.reload() }
} else { window.location.reload();
form.append(`<div class="ui negative message"><div class="header">操作失败</div><p>` + resp.message + `</p></div>`) } else {
} form.append(
}).fail(function (err) { `<div class="ui negative message"><div class="header">操作失败</div><p>` +
form.append(`<div class="ui negative message"><div class="header">网络错误</div><p>` + err.responseText + `</p></div>`) resp.message +
}).always(function () { `</p></div>`
btn.toggleClass('loading') );
}); }
return success })
} .fail(function (err) {
}).modal('show') 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) { function addOrEditAlertRule(rule) {
const modal = $('.rule.modal') const modal = $(".rule.modal");
modal.children('.header').text((rule ? '修改' : '添加') + '报警规则') modal.children(".header").text((rule ? "修改" : "添加") + "报警规则");
modal.find('.positive.button').html(rule ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>') modal
modal.find('input[name=ID]').val(rule ? rule.ID : null) .find(".positive.button")
modal.find('input[name=Name]').val(rule ? rule.Name : null) .html(
modal.find('textarea[name=RulesRaw]').val(rule ? rule.RulesRaw : null) rule ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>'
if (rule && rule.Enable) { );
modal.find('.ui.rule-enable.checkbox').checkbox('set checked') modal.find("input[name=ID]").val(rule ? rule.ID : null);
} else { modal.find("input[name=Name]").val(rule ? rule.Name : null);
modal.find('.ui.rule-enable.checkbox').checkbox('set unchecked') modal.find("textarea[name=RulesRaw]").val(rule ? rule.RulesRaw : null);
} if (rule && rule.Enable) {
showFormModal('.rule.modal', '#ruleForm', '/api/alert-rule') 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) { function addOrEditNotification(notification) {
const modal = $('.notification.modal') const modal = $(".notification.modal");
modal.children('.header').text((notification ? '修改' : '添加') + '通知方式') modal.children(".header").text((notification ? "修改" : "添加") + "通知方式");
modal.find('.positive.button').html(notification ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>') modal
modal.find('input[name=ID]').val(notification ? notification.ID : null) .find(".positive.button")
modal.find('input[name=Name]').val(notification ? notification.Name : null) .html(
modal.find('input[name=URL]').val(notification ? notification.URL : null) notification
modal.find('textarea[name=RequestBody]').val(notification ? notification.RequestBody : null) ? '修改<i class="edit icon"></i>'
modal.find('select[name=RequestMethod]').val(notification ? notification.RequestMethod : 1) : '添加<i class="add icon"></i>'
modal.find('select[name=RequestType]').val(notification ? notification.RequestType : 1) );
if (notification && notification.VerifySSL) { modal.find("input[name=ID]").val(notification ? notification.ID : null);
modal.find('.ui.nf-ssl.checkbox').checkbox('set checked') modal.find("input[name=Name]").val(notification ? notification.Name : null);
} else { modal.find("input[name=URL]").val(notification ? notification.URL : null);
modal.find('.ui.nf-ssl.checkbox').checkbox('set unchecked') modal
} .find("textarea[name=RequestBody]")
showFormModal('.notification.modal', '#notificationForm', '/api/notification') .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) { function addOrEditServer(server) {
const modal = $('.server.modal') const modal = $(".server.modal");
modal.children('.header').text((server ? '修改' : '添加') + '服务器') modal.children(".header").text((server ? "修改" : "添加") + "服务器");
modal.find('.positive.button').html(server ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>') modal
modal.find('input[name=id]').val(server ? server.ID : null) .find(".positive.button")
modal.find('input[name=name]').val(server ? server.Name : null) .html(
modal.find('input[name=Tag]').val(server ? server.Tag : null) server ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>'
modal.find('input[name=DisplayIndex]').val(server ? server.DisplayIndex : null) );
modal.find('textarea[name=Note]').val(server ? server.Note : null) modal.find("input[name=id]").val(server ? server.ID : null);
if (server) { modal.find("input[name=name]").val(server ? server.Name : null);
modal.find('.secret.field').attr('style', '') modal.find("input[name=Tag]").val(server ? server.Tag : null);
modal.find('input[name=secret]').val(server.Secret) modal
} else { .find("input[name=DisplayIndex]")
modal.find('.secret.field').attr('style', 'display:none') .val(server ? server.DisplayIndex : null);
modal.find('input[name=secret]').val('') modal.find("textarea[name=Note]").val(server ? server.Note : null);
} if (server) {
showFormModal('.server.modal', '#serverForm', '/api/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) { function addOrEditMonitor(monitor) {
const modal = $('.monitor.modal') const modal = $(".monitor.modal");
modal.children('.header').text((monitor ? '修改' : '添加') + '监控') modal.children(".header").text((monitor ? "修改" : "添加") + "监控");
modal.find('.positive.button').html(monitor ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>') modal
modal.find('input[name=ID]').val(monitor ? monitor.ID : null) .find(".positive.button")
modal.find('input[name=Name]').val(monitor ? monitor.Name : null) .html(
modal.find('input[name=Target]').val(monitor ? monitor.Target : null) monitor ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>'
modal.find('select[name=Type]').val(monitor ? monitor.Type : 1) );
showFormModal('.monitor.modal', '#monitorForm', '/api/monitor') 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) { function addOrEditCron(cron) {
const modal = $('.cron.modal') const modal = $(".cron.modal");
modal.children('.header').text((cron ? '修改' : '添加') + '计划任务') modal.children(".header").text((cron ? "修改" : "添加") + "计划任务");
modal.find('.positive.button').html(cron ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>') modal
modal.find('input[name=ID]').val(cron ? cron.ID : null) .find(".positive.button")
modal.find('input[name=Name]').val(cron ? cron.Name : null) .html(
modal.find('input[name=Scheduler]').val(cron ? cron.Scheduler : null) cron ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>'
modal.find('a.ui.label.visible').each((i, el) => { );
el.remove() modal.find("input[name=ID]").val(cron ? cron.ID : null);
}) modal.find("input[name=Name]").val(cron ? cron.Name : null);
var servers modal.find("input[name=Scheduler]").val(cron ? cron.Scheduler : null);
if (cron) { modal.find("a.ui.label.visible").each((i, el) => {
servers = cron.ServersRaw el.remove();
serverList = JSON.parse(servers) });
const node = modal.find('i.dropdown.icon') var servers;
for (let i = 0; i < serverList.length; i++) { if (cron) {
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>') 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) modal
if (cron && cron.PushSuccessful) { .find("input[name=ServersRaw]")
modal.find('.ui.push-successful.checkbox').checkbox('set checked') .val(cron ? "[]," + servers.substr(1, servers.length - 2) : "[]");
} else { modal.find("textarea[name=Command]").val(cron ? cron.Command : null);
modal.find('.ui.push-successful.checkbox').checkbox('set unchecked') if (cron && cron.PushSuccessful) {
} modal.find(".ui.push-successful.checkbox").checkbox("set checked");
showFormModal('.cron.modal', '#cronForm', '/api/cron') } else {
modal.find(".ui.push-successful.checkbox").checkbox("set unchecked");
}
showFormModal(".cron.modal", "#cronForm", "/api/cron");
} }
function deleteRequest(api) { function deleteRequest(api) {
$.ajax({ $.ajax({
url: api, url: api,
type: 'DELETE', type: "DELETE",
}).done(resp => { })
if (resp.code == 200) { .done((resp) => {
if (resp.message) { if (resp.code == 200) {
alert(resp.message) if (resp.message) {
} else { alert(resp.message);
alert('删除成功')
}
window.location.reload()
} else { } else {
alert('删除失败 ' + resp.code + '' + resp.message) alert("删除成功");
confirmBtn.toggleClass('loading')
} }
}).fail(err => { window.location.reload();
alert('网络错误:' + err.responseText) } else {
alert("删除失败 " + resp.code + "" + resp.message);
confirmBtn.toggleClass("loading");
}
})
.fail((err) => {
alert("网络错误:" + err.responseText);
}); });
} }
function manualTrigger(btn, cronId) { function manualTrigger(btn, cronId) {
$(btn).toggleClass('loading') $(btn).toggleClass("loading");
$.ajax({ $.ajax({
url: '/api/cron/' + cronId + '/manual', url: "/api/cron/" + cronId + "/manual",
type: 'GET', type: "GET",
}).done(resp => { })
$(btn).toggleClass('loading') .done((resp) => {
if (resp.code == 200) { $(btn).toggleClass("loading");
$.suiAlert({ if (resp.code == 200) {
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({ $.suiAlert({
title: '触发失败 ', title: "触发成功,等待执行结果",
type: 'error', type: "success",
description: '网络错误:' + err.responseText, description: resp.message,
time: '3', time: "3",
position: 'top-center', position: "top-center",
}); });
} else {
$.suiAlert({
title: "触发失败 ",
type: "error",
description: resp.code + "" + resp.message,
time: "3",
position: "top-center",
});
}
})
.fail((err) => {
$(btn).toggleClass("loading");
$.suiAlert({
title: "触发失败 ",
type: "error",
description: "网络错误:" + err.responseText,
time: "3",
position: "top-center",
});
}); });
} }
function logout(id) { function logout(id) {
$.post('/api/logout', JSON.stringify({ id: id })).done(function (resp) { $.post("/api/logout", JSON.stringify({ id: id }))
if (resp.code == 200) { .done(function (resp) {
$.suiAlert({ if (resp.code == 200) {
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({ $.suiAlert({
title: '网络错误', title: "注销成功",
description: err.responseText, type: "success",
type: 'error', description: "如需继续访问请使用 GitHub 再次登录",
time: '3', time: "3",
position: 'top-center', 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(() => { $(document).ready(() => {
try { try {
$('.ui.servers.search.dropdown').dropdown({ $(".ui.servers.search.dropdown").dropdown({
clearable: true, clearable: true,
apiSettings: { apiSettings: {
url: '/api/search-server?word={query}', url: "/api/search-server?word={query}",
cache: false, cache: false,
}, },
}) });
} catch (error) { } catch (error) {}
} });
})

View File

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

View File

@ -9,7 +9,7 @@
<title>{{.Title}}</title> <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="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/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" /> <link rel="shortcut icon" type="image/png" href="/static/logo.png?v20210320" />
</head> </head>

View File

@ -14,7 +14,7 @@
</a> </a>
{{else}} {{else}}
<a class='item{{if eq .MatchedPath "/"}} active{{end}}' href="/"><i class="home icon"></i>首页</a> <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}} {{end}}
<div class="right menu"> <div class="right menu">
<div class="item"> <div class="item">

View File

@ -1,38 +1,59 @@
{{define "component/monitor"}} {{define "component/monitor"}}
<div class="ui tiny monitor modal transition hidden"> <div class="ui tiny monitor modal transition hidden">
<div class="header">添加监控</div> <div class="header">添加监控</div>
<div class="content"> <div class="content">
<form id="monitorForm" class="ui form"> <form id="monitorForm" class="ui form">
<input type="hidden" name="ID"> <input type="hidden" name="ID" />
<div class="field"> <div class="field">
<label>名称</label> <label>名称</label>
<input type="text" name="Name" placeholder="博客"> <input type="text" name="Name" placeholder="博客" />
</div> </div>
<div class="field"> <div class="field">
<label>目标</label> <label>目标</label>
<input type="text" name="Target" placeholder="HTTP(https://t.tt)Ping(t.tt)TCP(t.tt:80)"> <input
</div> type="text"
<div class="field"> name="Target"
<label>类型</label> placeholder="HTTP(https://t.tt)Ping(t.tt)TCP(t.tt:80)"
<select name="Type" class="ui fluid dropdown"> />
<option value="1">HTTP-GET(SSL到期、变更)</option> </div>
<option value="2">ICMP-Ping</option> <div class="field">
<option value="3">TCP-Ping</option> <label>类型</label>
</select> <select name="Type" class="ui fluid dropdown">
</div> <option value="1">HTTP-GET(SSL到期、变更)</option>
</form> <option value="2">ICMP-Ping</option>
<div class="ui warning message"> <option value="3">TCP-Ping</option>
<p> </select>
类型为 <b>HTTP-GET</b> 时输入URL(带 http/https, HTTPS 协议的会顺带监控SSL证书)<br> </div>
类型为 <b>ICMP-Ping</b> 时输入主机名/IP不带端口<br> <div class="field">
类型为 <b>TCP-Ping</b> 时输入主机名/IP + 端口号example.com:22 <label>不通过下列服务器请求</label>
</p> <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>
<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>
<div class=" actions"> </div>
<div class="ui negative button">取消</div> <div class="actions">
<button class="ui positive right labeled icon button">确认<i class="checkmark icon"></i> <div class="ui negative button">取消</div>
</button> <button class="ui positive right labeled icon button">
</div> 确认<i class="checkmark icon"></i>
</button>
</div>
</div> </div>
{{end}} {{end}}

View File

@ -6,12 +6,12 @@
<h2 class="ui teal image header"> <h2 class="ui teal image header">
<img src="static/logo.png?v20210320" class="image"> <img src="static/logo.png?v20210320" class="image">
<div class="content"> <div class="content">
使用 GitHub 账号登录 使用 {{.LoginType}} 账号登录
</div> </div>
</h2> </h2>
<a href="/oauth2/login" class="ui fluid large teal submit button">登录</a> <a href="/oauth2/login" class="ui fluid large teal submit button">登录</a>
<div class="ui message"> <div class="ui message">
没有账号? <a href="https://github.com/join" target="_blank">注册</a> 没有账号? <a href="{{.RegistrationLink}}" target="_blank">注册</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,56 +1,65 @@
{{define "dashboard/monitor"}} {{define "dashboard/monitor"}} {{template "common/header" .}} {{template
{{template "common/header" .}} "common/menu" .}}
{{template "common/menu" .}}
<div class="nb-container"> <div class="nb-container">
<div class="ui container"> <div class="ui container">
<div class="ui grid"> <div class="ui grid">
<div class="right floated right aligned twelve wide column"> <div class="right floated right aligned twelve wide column">
<button class="ui right labeled positive icon button" onclick="addOrEditMonitor()"><i <button
class="add icon"></i> 添加监控 class="ui right labeled positive icon button"
</button> onclick="addOrEditMonitor()"
</div> >
</div> <i class="add icon"></i> 添加监控
<table class="ui very basic table"> </button>
<thead> </div>
<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> </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> </div>
{{template "component/monitor"}} {{template "component/monitor"}} {{template "common/footer" .}}
{{template "common/footer" .}} <script>
$(".checkbox").checkbox();
</script>
{{end}} {{end}}

View File

@ -8,11 +8,19 @@
<label>站点标题</label> <label>站点标题</label>
<input type="text" name="Title" placeholder="哪吒监控" value="{{.Conf.Site.Brand}}"> <input type="text" name="Title" placeholder="哪吒监控" value="{{.Conf.Site.Brand}}">
</div> </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"> <div class="field">
<label>管理员列表</label> <label>管理员列表</label>
<input type="text" name="Admin" placeholder="1010,2020" value="{{.Conf.Oauth2.Admin}}"> <input type="text" name="Admin" placeholder="1010,2020" value="{{.Conf.Oauth2.Admin}}">
</div> </div>
<div class="field"> <div class="field">
<label>前台主题</label>
<select name="Theme"> <select name="Theme">
<option value="default"{{if eq .Conf.Site.Theme "default"}} selected="selected"{{end}}>默认主题</option> <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> <option value="daynight"{{if eq .Conf.Site.Theme "daynight"}} selected="selected"{{end}}>JackieSung DayNight</option>

View File

@ -1,4 +1,4 @@
debug: true debug: false
httpport: 80 httpport: 80
oauth2: oauth2:
type: "nz_oauth2_type" #Oauth2 登录接入类型gitee/github type: "nz_oauth2_type" #Oauth2 登录接入类型gitee/github

View File

@ -2,7 +2,7 @@ version: "3.3"
services: services:
dashboard: dashboard:
image: image_url image: nz_image_url
restart: always restart: always
volumes: volumes:
- ./data:/dashboard/data - ./data:/dashboard/data

View File

@ -11,7 +11,7 @@ NZ_BASE_PATH="/opt/nezha"
NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard" NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent" NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service" NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
NZ_VERSION="v0.4.10" NZ_VERSION="v0.5.0"
red='\033[0;31m' red='\033[0;31m'
green='\033[0;32m' 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_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_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/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}" echo -e "面板配置 ${green}修改成功,请稍等重启生效${plain}"

View File

@ -15,7 +15,7 @@ Type=simple
User=root User=root
Group=root Group=root
WorkingDirectory=/opt/nezha/agent/ 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 Restart=always
#Environment=DEBUG=true #Environment=DEBUG=true

View File

@ -43,7 +43,9 @@ func AlertSentinelStart() {
checkStatus() checkStatus()
checkCount++ checkCount++
if lastPrint.Before(startedAt.Add(-1 * time.Hour)) { 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 checkCount = 0
lastPrint = startedAt lastPrint = startedAt
} }

View File

@ -13,7 +13,7 @@ import (
pb "github.com/naiba/nezha/proto" pb "github.com/naiba/nezha/proto"
) )
var Version = "v0.4.15" // !!记得修改 README 重的 badge 版本!! var Version = "v0.7.1" // !!记得修改 README 中的 badge 版本!!
const ( const (
SnapshotDelay = 3 SnapshotDelay = 3
@ -46,7 +46,7 @@ func ReSortServer() {
sort.SliceStable(SortedServerList, func(i, j int) bool { sort.SliceStable(SortedServerList, func(i, j int) bool {
if SortedServerList[i].DisplayIndex == SortedServerList[j].DisplayIndex { 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 return SortedServerList[i].DisplayIndex > SortedServerList[j].DisplayIndex
}) })

View 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()
}
}
}

View File

@ -18,7 +18,7 @@ func (a *AuthHandler) GetRequestMetadata(ctx context.Context, uri ...string) (ma
} }
func (a *AuthHandler) RequireTransportSecurity() bool { func (a *AuthHandler) RequireTransportSecurity() bool {
return !dao.Conf.Debug return false
} }
func (a *AuthHandler) Check(ctx context.Context) (uint64, error) { func (a *AuthHandler) Check(ctx context.Context) (uint64, error) {

View File

@ -3,8 +3,6 @@ package rpc
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"strings"
"time" "time"
"github.com/naiba/nezha/model" "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 { if clientID, err = s.Auth.Check(c); err != nil {
return nil, err return nil, err
} }
if r.GetType() == model.TaskTypeHTTPGET { if r.GetType() != model.TaskTypeCommand {
// SSL 证书报警 dao.ServiceSentinelShared.Dispatch(dao.ReportData{
var errMsg string Data: r,
if strings.HasPrefix(r.GetData(), "SSL证书错误") { Reporter: clientID,
// 排除 i/o timeont、connection timeout、EOF 错误 })
if !strings.HasSuffix(r.GetData(), "timeout") && } else {
!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 {
// 处理上报的计划任务 // 处理上报的计划任务
dao.CronLock.RLock() dao.CronLock.RLock()
defer dao.CronLock.RUnlock() defer dao.CronLock.RUnlock()
@ -81,12 +42,6 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece
LastResult: r.GetSuccessful(), 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 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].TaskStream = stream
dao.ServerList[clientID].TaskClose = closeCh dao.ServerList[clientID].TaskClose = closeCh
dao.ServerLock.RUnlock() dao.ServerLock.RUnlock()
select { return <-closeCh
case err = <-closeCh:
return err
}
} }
func (s *NezhaHandler) ReportSystemState(c context.Context, r *pb.State) (*pb.Receipt, error) { 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) host := model.PB2Host(r)
dao.ServerLock.RLock() dao.ServerLock.RLock()
defer dao.ServerLock.RUnlock() defer dao.ServerLock.RUnlock()
log.Println(dao.Conf.IgnoredIPNotificationServerIDs)
if dao.Conf.EnableIPChangeNotification && if dao.Conf.EnableIPChangeNotification &&
dao.Conf.IgnoredIPNotificationServerIDs[clientID] != struct{}{} && dao.Conf.IgnoredIPNotificationServerIDs[clientID] != struct{}{} &&
dao.ServerList[clientID].Host != nil && dao.ServerList[clientID].Host != nil &&