mirror of
https://github.com/nezhahq/nezha.git
synced 2025-01-22 20:58:14 -05:00
🚸 release: v0.2.1
This commit is contained in:
parent
303dc73e16
commit
d792fc8499
18
README.md
18
README.md
@ -1,6 +1,6 @@
|
|||||||
# 哪吒面板
|
# 哪吒面板
|
||||||
|
|
||||||
服务期状态监控,报警通知,被动接收,极省资源 64M 小鸡也能装 Agent。
|
系统状态、API(SSL证书变更、即将到期、到期)/TCP端口存活/PING 监控,报警通知,被动接收,极省资源 64M 小鸡也能装 Agent。
|
||||||
|
|
||||||
| 哪吒面板 | 首页截图1 | 首页截图2 |
|
| 哪吒面板 | 首页截图1 | 首页截图2 |
|
||||||
| ---- | ---- | ---- |
|
| ---- | ---- | ---- |
|
||||||
@ -131,7 +131,7 @@ URL 里面也可放置占位符,请求时会进行简单的字符串替换。
|
|||||||
- net_in_speed(入站网速)、net_out_speed(出站网速)、net_all_speed(双向网速)、transfer_in(入站流量)、transfer_out(出站流量)、transfer_all(双向流量):Min/Max 数值为字节(1kb=1024,1mb = 1024*1024)
|
- net_in_speed(入站网速)、net_out_speed(出站网速)、net_all_speed(双向网速)、transfer_in(入站流量)、transfer_out(出站流量)、transfer_all(双向流量):Min/Max 数值为字节(1kb=1024,1mb = 1024*1024)
|
||||||
- offline:不支持 Min/Max 参数
|
- offline:不支持 Min/Max 参数
|
||||||
- Duration:持续秒数,监控比较简陋,取持续时间内的 70 采样结果
|
- Duration:持续秒数,监控比较简陋,取持续时间内的 70 采样结果
|
||||||
|
- Ignore: `{"1": true, "2":false}` 忽略此规则的服务器ID列表
|
||||||
## 常见问题
|
## 常见问题
|
||||||
|
|
||||||
### 数据备份恢复
|
### 数据备份恢复
|
||||||
@ -182,7 +182,19 @@ URL 里面也可放置占位符,请求时会进行简单的字符串替换。
|
|||||||
|
|
||||||
## 变更日志
|
## 变更日志
|
||||||
|
|
||||||
- `0.2.0` **重大更新**
|
- `dashboard 0.2.1` `agent 0.2.1`
|
||||||
|
|
||||||
|
- dashboard
|
||||||
|
- 修复了默认开启IP变更通知
|
||||||
|
- hotaru 主题的服务状态页面
|
||||||
|
- **新增可以指定服务器忽略监控规则**
|
||||||
|
- 修复info透明 @ilay1678
|
||||||
|
|
||||||
|
- agent
|
||||||
|
- 优化了 IPv6/IPv4 双栈问题
|
||||||
|
- 增加 SSL 证书过期、即将过期提醒
|
||||||
|
|
||||||
|
- `dashboard 0.2.0` `agent 0.2.0` **重大更新**
|
||||||
|
|
||||||
增加了服务监控(TCP端口延迟、Ping、HTTP-SSL 证书)功能,此版本 Agent 与旧面板不兼容,而 Agent 是通过 GitHub Release 自动更新的 所以务必更新面板开启最新功能。
|
增加了服务监控(TCP端口延迟、Ping、HTTP-SSL 证书)功能,此版本 Agent 与旧面板不兼容,而 Agent 是通过 GitHub Release 自动更新的 所以务必更新面板开启最新功能。
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@ -50,6 +51,9 @@ var (
|
|||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
delayWhenError = time.Second * 10
|
delayWhenError = time.Second * 10
|
||||||
updateCh = make(chan struct{}, 0)
|
updateCh = make(chan struct{}, 0)
|
||||||
|
httpClient = &http.Client{Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
}}
|
||||||
)
|
)
|
||||||
|
|
||||||
func doSelfUpdate() {
|
func doSelfUpdate() {
|
||||||
@ -151,6 +155,7 @@ func run(cmd *cobra.Command, args []string) {
|
|||||||
func receiveTasks(tasks pb.NezhaService_RequestTaskClient) error {
|
func receiveTasks(tasks pb.NezhaService_RequestTaskClient) error {
|
||||||
var err error
|
var err error
|
||||||
var task *pb.Task
|
var task *pb.Task
|
||||||
|
|
||||||
defer log.Printf("receiveTasks exit %v %v => %v", time.Now(), task, err)
|
defer log.Printf("receiveTasks exit %v %v => %v", time.Now(), task, err)
|
||||||
for {
|
for {
|
||||||
task, err = tasks.Recv()
|
task, err = tasks.Recv()
|
||||||
@ -159,30 +164,31 @@ func receiveTasks(tasks pb.NezhaService_RequestTaskClient) error {
|
|||||||
}
|
}
|
||||||
var result pb.TaskResult
|
var result pb.TaskResult
|
||||||
result.Id = task.GetId()
|
result.Id = task.GetId()
|
||||||
|
result.Type = task.GetType()
|
||||||
switch task.GetType() {
|
switch task.GetType() {
|
||||||
case model.MonitorTypeHTTPGET:
|
case model.MonitorTypeHTTPGET:
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
resp, err := http.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
|
result.Delay = float32(time.Now().Sub(start).Microseconds()) / 1000.0
|
||||||
if resp.StatusCode > 299 || resp.StatusCode < 200 {
|
if resp.StatusCode > 299 || resp.StatusCode < 200 {
|
||||||
err = errors.New("\n应用错误:" + resp.Status)
|
err = errors.New("\n应用错误:" + resp.Status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var certs cert.Certs
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if strings.HasPrefix(task.GetData(), "https://") {
|
if strings.HasPrefix(task.GetData(), "https://") {
|
||||||
certs, err = cert.NewCerts([]string{task.GetData()})
|
c := cert.NewCert(task.GetData()[8:])
|
||||||
|
if c.Error != "" {
|
||||||
|
if strings.Contains(c.Error, "expired") {
|
||||||
|
result.Data = "SSL证书错误:证书已过期"
|
||||||
|
} else {
|
||||||
|
result.Data = "SSL证书错误:" + c.Error
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
if err == nil {
|
result.Data = c.Issuer + "|" + c.NotAfter
|
||||||
if len(certs) == 0 {
|
|
||||||
err = errors.New("\n获取SSL证书错误:未获取到证书")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
result.Data = certs[0].Issuer
|
|
||||||
result.Successful = true
|
result.Successful = true
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
result.Data = err.Error()
|
result.Data = err.Error()
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,9 @@ func ServeWeb(port uint) {
|
|||||||
"ts": func(s string) string {
|
"ts": func(s string) string {
|
||||||
return strings.TrimSpace(s)
|
return strings.TrimSpace(s)
|
||||||
},
|
},
|
||||||
|
"float32f": func(f float32) string {
|
||||||
|
return fmt.Sprintf("%.2f", f)
|
||||||
|
},
|
||||||
"divU64": func(a, b uint64) float32 {
|
"divU64": func(a, b uint64) float32 {
|
||||||
if b == 0 {
|
if b == 0 {
|
||||||
if a > 0 {
|
if a > 0 {
|
||||||
@ -49,6 +52,10 @@ func ServeWeb(port uint) {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
if a == 0 {
|
||||||
|
// 这是从未在线的情况
|
||||||
|
return 1 / float32(b) * 100
|
||||||
|
}
|
||||||
return float32(a) / float32(b) * 100
|
return float32(a) / float32(b) * 100
|
||||||
},
|
},
|
||||||
"div": func(a, b int) float32 {
|
"div": func(a, b int) float32 {
|
||||||
@ -58,6 +65,10 @@ func ServeWeb(port uint) {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
if a == 0 {
|
||||||
|
// 这是从未在线的情况
|
||||||
|
return 1 / float32(b) * 100
|
||||||
|
}
|
||||||
return float32(a) / float32(b) * 100
|
return float32(a) / float32(b) * 100
|
||||||
},
|
},
|
||||||
"addU64": func(a, b uint64) uint64 {
|
"addU64": func(a, b uint64) uint64 {
|
||||||
|
@ -2,7 +2,6 @@ package rpc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -47,9 +46,7 @@ func DispatchTask(duration time.Duration) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
hasAliveAgent = true
|
hasAliveAgent = true
|
||||||
log.Println("DispatchTask 确认派发 >>>>>", i, index)
|
|
||||||
dao.SortedServerList[index].TaskStream.Send(tasks[i].PB())
|
dao.SortedServerList[index].TaskStream.Send(tasks[i].PB())
|
||||||
log.Println("DispatchTask 确认派发 <<<<<", i, index)
|
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
dao.ServerLock.RUnlock()
|
dao.ServerLock.RUnlock()
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -13,11 +15,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// 跳过 SSL 检查
|
||||||
|
transCfg := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{Transport: transCfg}
|
||||||
|
_, err := httpClient.Get("https://expired-ecc-dv.ssl.com")
|
||||||
|
fmt.Println(err)
|
||||||
|
// SSL 证书信息获取
|
||||||
|
c := cert.NewCert("expired-ecc-dv.ssl.com")
|
||||||
|
fmt.Println(c.Error)
|
||||||
|
// TCP
|
||||||
conn, err := net.DialTimeout("tcp", "example.com:80", time.Second*10)
|
conn, err := net.DialTimeout("tcp", "example.com:80", time.Second*10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
println(conn)
|
println(conn)
|
||||||
|
// ICMP Ping
|
||||||
pinger, err := ping.NewPinger("example.com")
|
pinger, err := ping.NewPinger("example.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -28,11 +42,7 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%+v", pinger.Statistics())
|
fmt.Printf("%+v", pinger.Statistics())
|
||||||
certs, err := cert.NewCerts([]string{"example.com"})
|
// 硬盘信息
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Println(certs)
|
|
||||||
dparts, _ := disk.Partitions(false)
|
dparts, _ := disk.Partitions(false)
|
||||||
for _, part := range dparts {
|
for _, part := range dparts {
|
||||||
u, _ := disk.Usage(part.Mountpoint)
|
u, _ := disk.Usage(part.Mountpoint)
|
||||||
|
@ -21,6 +21,7 @@ type Rule struct {
|
|||||||
Min uint64 // 最小阈值 (百分比、字节 kb ÷ 1024)
|
Min uint64 // 最小阈值 (百分比、字节 kb ÷ 1024)
|
||||||
Max uint64 // 最大阈值 (百分比、字节 kb ÷ 1024)
|
Max uint64 // 最大阈值 (百分比、字节 kb ÷ 1024)
|
||||||
Duration uint64 // 持续时间 (秒)
|
Duration uint64 // 持续时间 (秒)
|
||||||
|
Ignore map[uint64]bool //忽略此规则的ID列表
|
||||||
}
|
}
|
||||||
|
|
||||||
func percentage(used, total uint64) uint64 {
|
func percentage(used, total uint64) uint64 {
|
||||||
@ -30,7 +31,11 @@ func percentage(used, total uint64) uint64 {
|
|||||||
return used * 100 / total
|
return used * 100 / total
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snapshot 未通过规则返回 struct{}{}, 通过返回 nil
|
||||||
func (u *Rule) Snapshot(server *Server) interface{} {
|
func (u *Rule) Snapshot(server *Server) interface{} {
|
||||||
|
if u.Ignore[server.ID] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
var src uint64
|
var src uint64
|
||||||
switch u.Type {
|
switch u.Type {
|
||||||
case "cpu":
|
case "cpu":
|
||||||
@ -72,9 +77,9 @@ func (u *Rule) Snapshot(server *Server) interface{} {
|
|||||||
type AlertRule struct {
|
type AlertRule struct {
|
||||||
Common
|
Common
|
||||||
Name string
|
Name string
|
||||||
Rules []Rule `gorm:"-" json:"-"`
|
|
||||||
RulesRaw string
|
RulesRaw string
|
||||||
Enable *bool
|
Enable *bool
|
||||||
|
Rules []Rule `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AlertRule) BeforeSave(tx *gorm.DB) error {
|
func (r *AlertRule) BeforeSave(tx *gorm.DB) error {
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label>类型</label>
|
<label>类型</label>
|
||||||
<select name="Type" class="ui fluid dropdown">
|
<select name="Type" class="ui fluid dropdown">
|
||||||
<option value="1">HTTP-GET</option>
|
<option value="1">HTTP-GET(SSL到期、变更)</option>
|
||||||
<option value="2">ICMP-Ping</option>
|
<option value="2">ICMP-Ping</option>
|
||||||
<option value="3">TCP-Ping</option>
|
<option value="3">TCP-Ping</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<div class="ui grid">
|
<div class="ui grid">
|
||||||
<div class="three wide column">
|
<div class="three wide column">
|
||||||
<p>{{$service.Monitor.Name}}</p>
|
<p>{{$service.Monitor.Name}}</p>
|
||||||
<p>30天在线率{{divU64 $service.TotalUp (addU64 $service.TotalUp $service.TotalDown)}}%</p>
|
<p>30天在线率{{float32f (divU64 $service.TotalUp (addU64 $service.TotalUp $service.TotalDown))}}%</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="eleven wide column">
|
<div class="eleven wide column">
|
||||||
{{range $i,$d := $service.Delay}}
|
{{range $i,$d := $service.Delay}}
|
||||||
|
@ -133,7 +133,20 @@ func checkStatus() {
|
|||||||
// 发送通知
|
// 发送通知
|
||||||
max, desc := alert.Check(alertsStore[alert.ID][server.ID])
|
max, desc := alert.Check(alertsStore[alert.ID][server.ID])
|
||||||
if desc != "" {
|
if desc != "" {
|
||||||
nID := getNotificationHash(server, desc)
|
message := fmt.Sprintf("报警规则:%s,服务器:%s(%s),%s,逮到咯,快去看看!", alert.Name, server.Name, server.Host.IP, desc)
|
||||||
|
go SendNotification(message)
|
||||||
|
}
|
||||||
|
// 清理旧数据
|
||||||
|
if max > 0 && max < len(alertsStore[alert.ID][server.ID]) {
|
||||||
|
alertsStore[alert.ID][server.ID] = alertsStore[alert.ID][server.ID][len(alertsStore[alert.ID][server.ID])-max:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendNotification(desc string) {
|
||||||
|
// 通知防骚扰策略
|
||||||
|
nID := hex.EncodeToString(md5.New().Sum([]byte(desc)))
|
||||||
var flag bool
|
var flag bool
|
||||||
if cacheN, has := dao.Cache.Get(nID); has {
|
if cacheN, has := dao.Cache.Get(nID); has {
|
||||||
nHistory := cacheN.(NotificationHistory)
|
nHistory := cacheN.(NotificationHistory)
|
||||||
@ -156,27 +169,15 @@ func checkStatus() {
|
|||||||
Until: time.Now().Add(firstNotificationDelay),
|
Until: time.Now().Add(firstNotificationDelay),
|
||||||
}, firstNotificationDelay+time.Minute*10)
|
}, firstNotificationDelay+time.Minute*10)
|
||||||
}
|
}
|
||||||
if flag {
|
|
||||||
message := fmt.Sprintf("报警规则:%s,服务器:%s(%s),%s,逮到咯,快去看看!", alert.Name, server.Name, server.Host.IP, desc)
|
|
||||||
go SendNotification(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 清理旧数据
|
|
||||||
if max > 0 && max < len(alertsStore[alert.ID][server.ID]) {
|
|
||||||
alertsStore[alert.ID][server.ID] = alertsStore[alert.ID][server.ID][len(alertsStore[alert.ID][server.ID])-max:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SendNotification(desc string) {
|
if !flag {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发出通知
|
||||||
notificationsLock.RLock()
|
notificationsLock.RLock()
|
||||||
defer notificationsLock.RUnlock()
|
defer notificationsLock.RUnlock()
|
||||||
for i := 0; i < len(notifications); i++ {
|
for i := 0; i < len(notifications); i++ {
|
||||||
notifications[i].Send(desc)
|
notifications[i].Send(desc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNotificationHash(server *model.Server, desc string) string {
|
|
||||||
return hex.EncodeToString(md5.New().Sum([]byte(fmt.Sprintf("%d::%s", server.ID, desc))))
|
|
||||||
}
|
|
||||||
|
@ -3,6 +3,7 @@ package rpc
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/naiba/nezha/model"
|
"github.com/naiba/nezha/model"
|
||||||
@ -21,15 +22,33 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if r.GetType() == model.MonitorTypeHTTPGET {
|
if r.GetType() == model.MonitorTypeHTTPGET {
|
||||||
// SSL 证书变更报警
|
// SSL 证书报警
|
||||||
var last model.MonitorHistory
|
var last model.MonitorHistory
|
||||||
if err := dao.DB.Where("monitor_id = ?", r.GetId()).Order("id DESC").First(&last).Error; err == nil {
|
if err := dao.DB.Where("monitor_id = ?", r.GetId()).Order("id DESC").First(&last).Error; err == nil {
|
||||||
if last.Data != "" && last.Data != r.GetData() {
|
var errMsg string
|
||||||
|
if strings.HasPrefix(r.GetData(), "SSL证书错误:") {
|
||||||
|
// 证书错误提醒
|
||||||
|
errMsg = r.GetData()
|
||||||
|
} else {
|
||||||
|
var splits = strings.Split(r.GetData(), "|")
|
||||||
|
// 证书变更提醒
|
||||||
|
if last.Data != "" && last.Data != splits[0] {
|
||||||
|
errMsg = fmt.Sprintf(
|
||||||
|
"SSL证书变更,旧:%s,新:%s。",
|
||||||
|
last.Data, splits[0])
|
||||||
|
}
|
||||||
|
expires, err := time.Parse("2006-01-02 15:04:05 -0700 MST", splits[1])
|
||||||
|
// 证书过期提醒
|
||||||
|
if err == nil && expires.Before(time.Now().AddDate(0, 0, 7)) {
|
||||||
|
errMsg = fmt.Sprintf(
|
||||||
|
"SSL证书将在七天内过期,过期时间:%s。",
|
||||||
|
expires.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errMsg != "" {
|
||||||
var monitor model.Monitor
|
var monitor model.Monitor
|
||||||
dao.DB.First(&monitor, "id = ?", last.MonitorID)
|
dao.DB.First(&monitor, "id = ?", last.MonitorID)
|
||||||
alertmanager.SendNotification(fmt.Sprintf(
|
alertmanager.SendNotification(fmt.Sprintf("服务监控:%s %s", monitor.Name, errMsg))
|
||||||
"监控:%s SSL证书变更,旧:%s,新:%s。",
|
|
||||||
monitor.Name, last.Data, r.GetData()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,12 +57,6 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece
|
|||||||
if err := dao.DB.Create(&mh).Error; err != nil {
|
if err := dao.DB.Create(&mh).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// 更新最后检测时间
|
|
||||||
var m model.Monitor
|
|
||||||
m.ID = r.GetId()
|
|
||||||
if err := dao.DB.Model(&m).Update("last_check", time.Now()).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &pb.Receipt{Proced: true}, nil
|
return &pb.Receipt{Proced: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +106,7 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
|
|||||||
host.IP != "" &&
|
host.IP != "" &&
|
||||||
dao.ServerList[clientID].Host.IP != host.IP {
|
dao.ServerList[clientID].Host.IP != host.IP {
|
||||||
alertmanager.SendNotification(fmt.Sprintf(
|
alertmanager.SendNotification(fmt.Sprintf(
|
||||||
"服务器:%s IP变更提醒,旧IP:%s,新IP:%s。",
|
"IP变更提醒 服务器:%s ,旧IP:%s,新IP:%s。",
|
||||||
dao.ServerList[clientID].Name, dao.ServerList[clientID].Host.IP, host.IP))
|
dao.ServerList[clientID].Name, dao.ServerList[clientID].Host.IP, host.IP))
|
||||||
}
|
}
|
||||||
dao.ServerList[clientID].Host = &host
|
dao.ServerList[clientID].Host = &host
|
||||||
|
Loading…
Reference in New Issue
Block a user