new & opt.

- new: add time for msg
- new: `rate` for push
- opt.: interval
This commit is contained in:
lollipopkit 2023-07-15 22:36:57 +08:00
parent 5913686cec
commit 61947378d9
13 changed files with 146 additions and 104 deletions

View File

@ -1,10 +1,13 @@
{
"version": 1,
"version": 2,
// Interval of checking
// Valid formats: 1s 1m 1h
// Default: 30s
// Values less than 10s will be ignored
"interval": "1m",
// Rate limiter for msg push
// eg: 3/1m (3 times every minute), 1/10s (1 time every 10 seconds)
"rate": "1/10s",
// Check rules
//
// Type:
@ -75,7 +78,7 @@
// body_regex: regex to match the response body
// code: response code to match
//
// {{key}} and {{value}} will be replaced with the key and value of the check
// {{kvs}} will be replaced with the key and value of the check
"pushes": [
{
// This is a example for QQ Group message
@ -97,7 +100,7 @@
"action": "send_group_msg",
"params": {
"group_id": 123456789,
"message": "ServerBox Notification\n{{key}}: {{value}}"
"message": "ServerBox Notification\n{{kvs}}"
}
},
// Check push is successful or not:
@ -116,7 +119,7 @@
// You can get it from settings page of ServerBox iOS app
"token": "YOUR_TOKEN",
"title": "Server Notification",
"content": "{{key}}: {{value}}",
"content": "{{kvs}}",
"body_regex": ".*",
"code": 200
}
@ -128,7 +131,7 @@
// Details please refer to https://sct.ftqq.com/
"sckey": "YOUR_SCKEY",
"title": "Server Notification",
"desp": "{{key}}: {{value}}",
"desp": "{{kvs}}",
"body_regex": ".*",
"code": 200
}

View File

@ -1,10 +1,13 @@
{
"version": 1,
"version": 2,
// 时间间隔,用于推送
// 有效格式: 1s 1m 1h
// 默认: 30s
// 小于 10s 的值将被忽略
"interval": "30s",
// 推送速率限制
// 示例: 3/1m (每分钟三次), 1/10s (10秒一次)
"rate": "1/10s",
// 监测规则
// 可用类型(type): cpu, mem, net, disk, temp (温度), swap
//

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.20
require (
github.com/labstack/echo/v4 v4.11.0
github.com/lollipopkit/gommon v0.3.3
github.com/lollipopkit/gommon v0.3.7
github.com/urfave/cli/v2 v2.25.7
)

4
go.sum
View File

@ -9,8 +9,8 @@ github.com/labstack/echo/v4 v4.11.0 h1:4Dmi59tmrnFzOchz4EXuGjJhUfcEkU28iDKsiZVOQ
github.com/labstack/echo/v4 v4.11.0/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/lollipopkit/gommon v0.3.3 h1:LL6jvoyj0bNZUFgVBRNCyJ+aNR3AVRSxFwPtZHn+BQ0=
github.com/lollipopkit/gommon v0.3.3/go.mod h1:DEnIxhHmPQjDSkKFDxwX6oFxMjlHd87G+Dt7U4ZUyRs=
github.com/lollipopkit/gommon v0.3.7 h1:kROUlge2+/zp6jKzVNLVZ0jVoxqcx6mqhlPhPLlHMWo=
github.com/lollipopkit/gommon v0.3.7/go.mod h1:DEnIxhHmPQjDSkKFDxwX6oFxMjlHd87G+Dt7U4ZUyRs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=

View File

@ -1,9 +1,14 @@
package main
import (
"github.com/lollipopkit/gommon/log"
"github.com/lollipopkit/server_box_monitor/cmd"
)
func main() {
cmd.Run()
log.Setup(log.Config{
PrintTime: true,
})
}

View File

@ -3,15 +3,20 @@ package model
import (
"encoding/json"
"os"
"strconv"
"strings"
"time"
"github.com/lollipopkit/gommon/log"
"github.com/lollipopkit/gommon/util"
os_ "github.com/lollipopkit/gommon/os"
"github.com/lollipopkit/gommon/rate"
"github.com/lollipopkit/server_box_monitor/res"
)
var (
Config = &AppConfig{}
Config = new(AppConfig)
CheckInterval time.Duration
RateLimiter *rate.RateLimiter[string]
)
type AppConfig struct {
@ -20,12 +25,15 @@ type AppConfig struct {
// Valid time units are "s".
// Values bigger than 10 seconds are not allowed.
Interval string `json:"interval"`
Rate string `json:"rate"`
Rules []Rule `json:"rules"`
Pushes []Push `json:"pushes"`
}
func ReadAppConfig() error {
if !util.Exist(res.AppConfigPath) {
defer initInterval()
defer initRateLimiter()
if !os_.Exist(res.AppConfigPath) {
configBytes, err := json.MarshalIndent(DefaultappConfig, "", "\t")
if err != nil {
log.Err("[CONFIG] marshal default app config failed: %v", err)
@ -54,25 +62,41 @@ func ReadAppConfig() error {
return err
}
func GetInterval() time.Duration {
ac := DefaultappConfig
if Config != nil {
ac = Config
}
d, err := time.ParseDuration(ac.Interval)
func initInterval() {
d, err := time.ParseDuration(Config.Interval)
if err == nil {
if d > res.DefaultInterval {
log.Warn("[CONFIG] interval is too long, use default interval")
return res.DefaultInterval
if d > res.MaxInterval || d < time.Second {
log.Warn("[CONFIG] use default interval")
CheckInterval = res.DefaultInterval
return
}
return d
CheckInterval = d
return
}
log.Warn("[CONFIG] parse interval failed: %v", err)
return res.DefaultInterval
CheckInterval = res.DefaultInterval
}
func GetIntervalInSeconds() float64 {
return GetInterval().Seconds()
func initRateLimiter() {
splited := strings.Split(Config.Rate, "/")
if len(splited) != 2 {
log.Warn("[CONFIG] parse rate failed")
RateLimiter = res.DefaultRateLimiter
return
}
times, err := strconv.Atoi(splited[0])
if err != nil {
log.Warn("[CONFIG] parse rate failed: %v", err)
RateLimiter = res.DefaultRateLimiter
return
}
duration, err := time.ParseDuration(splited[1])
if err != nil {
log.Warn("[CONFIG] parse rate failed: %v", err)
RateLimiter = res.DefaultRateLimiter
return
}
RateLimiter = rate.NewLimiter[string](duration, times)
}
var (
@ -116,8 +140,9 @@ var (
defaultServerChanIfaceBytes, _ = json.Marshal(defaultServerChanIface)
DefaultappConfig = &AppConfig{
Version: 1,
Interval: "30s",
Version: 2,
Interval: "7s",
Rate: "1/1m",
Rules: []Rule{
{
MonitorType: MonitorTypeCPU,
@ -136,16 +161,6 @@ var (
Name: "QQ Group",
Iface: defaultWebhookIfaceBytes,
},
{
Type: PushTypeIOS,
Name: "My iPhone",
Iface: defaultIOSIfaceBytes,
},
{
Type: PushTypeServerChan,
Name: "ServerChan",
Iface: defaultServerChanIfaceBytes,
},
},
}
)

View File

@ -6,8 +6,9 @@ import (
"fmt"
"regexp"
"strings"
"time"
"github.com/lollipopkit/gommon/util"
"github.com/lollipopkit/gommon/http"
)
type Push struct {
@ -54,19 +55,25 @@ func (p *Push) Push(args []*PushPair) error {
// {{key}} {{value}}
type PushFormat string
type PushPair struct {
Key string
Value string
key string
value string
time string
}
func NewPushPair(key, value string) *PushPair {
return &PushPair{
key: key,
value: value,
time: time.Now().Format("2006-01-02 15:04:05"),
}
}
func (pf PushFormat) Format(args []*PushPair) string {
ss := []string{}
for _, arg := range args {
s := string(pf)
s = strings.ReplaceAll(s, "{{key}}", arg.Key)
s = strings.ReplaceAll(s, "{{value}}", arg.Value)
ss = append(ss, s)
kv := fmt.Sprintf("%s\n%s: %s", arg.time, arg.key, arg.value)
ss = append(ss, kv)
}
return strings.Join(ss, "\n")
return strings.ReplaceAll(string(pf), "{{kvs}}", strings.Join(ss, "\n"))
}
type PushType string
@ -83,25 +90,24 @@ type PushIface interface {
type PushIfaceIOS struct {
Token string `json:"token"`
Title PushFormat `json:"title"`
Title string `json:"title"`
Content PushFormat `json:"content"`
BodyRegex string `json:"body_regex"`
Code int `json:"code"`
}
func (p PushIfaceIOS) push(args []*PushPair) error {
title := p.Title.Format(args)
content := p.Content.Format(args)
body := map[string]string{
"token": p.Token,
"title": title,
"title": p.Title,
"content": content,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return err
}
resp, code, err := util.HttpDo(
resp, code, err := http.Do(
"POST",
"https://push.lolli.tech/v1/ios",
bodyBytes,
@ -137,9 +143,10 @@ type PushIfaceWebhook struct {
func (p PushIfaceWebhook) push(args []*PushPair) error {
body := PushFormat(p.Body).Format(args)
println(body)
switch p.Method {
case "GET", "POST":
resp, code, err := util.HttpDo(p.Method, p.Url, body, p.Headers)
resp, code, err := http.Do(p.Method, p.Url, body, p.Headers)
if err != nil {
return err
}
@ -162,17 +169,16 @@ func (p PushIfaceWebhook) push(args []*PushPair) error {
type PushIfaceServerChan struct {
SCKey string `json:"sckey"`
Title PushFormat `json:"title"`
Title string `json:"title"`
Desp PushFormat `json:"desp"`
BodyRegex string `json:"body_regex"`
Code int `json:"code"`
}
func (p PushIfaceServerChan) push(args []*PushPair) error {
title := p.Title.Format(args)
desp := p.Desp.Format(args)
url := fmt.Sprintf("https://sctapi.ftqq.com/%s.send?title=%s&desp=%s", p.SCKey, title, desp)
resp, code, err := util.HttpDo("GET", url, nil, nil)
url := fmt.Sprintf("https://sctapi.ftqq.com/%s.send?title=%s&desp=%s", p.SCKey, p.Title, desp)
resp, code, err := http.Do("GET", url, nil, nil)
if err != nil {
return err
}

View File

@ -96,10 +96,10 @@ func (r *Rule) shouldNotifyCPU(ss []oneCpuStatus, t *Threshold) (bool, *PushPair
if idx > 0 {
key = fmt.Sprintf("cpu%d", idx-1)
}
return ok, &PushPair{
Key: key,
Value: fmt.Sprintf("%.2f%%", usedPercent),
}, nil
return ok, NewPushPair(
key,
fmt.Sprintf("%.2f%%", usedPercent),
), nil
default:
return false, nil, errors.Join(ErrInvalidRule, fmt.Errorf("invalid threshold type for cpu: %s", t.ThresholdType.Name()))
}
@ -131,19 +131,19 @@ func (r *Rule) shouldNotifyMemory(s *memStatus, t *Threshold) (bool, *PushPair,
if err != nil {
return false, nil, err
}
return ok, &PushPair{
Key: r.Matcher + "of Memory",
Value: size.String(),
}, nil
return ok, NewPushPair(
"Mem " + r.Matcher,
size.String(),
), nil
case ThresholdTypePercent:
ok, err := t.True(percent)
if err != nil {
return false, nil, err
}
return ok, &PushPair{
Key: r.Matcher + "of Memory",
Value: fmt.Sprintf("%.2f%%", percent*100),
}, nil
return ok, NewPushPair(
"Mem " + r.Matcher,
fmt.Sprintf("%.2f%%", percent*100),
), nil
default:
return false, nil, errors.Join(ErrInvalidRule, fmt.Errorf("invalid threshold type for memory: %s", t.ThresholdType.Name()))
}
@ -172,19 +172,19 @@ func (r *Rule) shouldNotifySwap(s *swapStatus, t *Threshold) (bool, *PushPair, e
if err != nil {
return false, nil, err
}
return ok, &PushPair{
Key: r.Matcher + "of Swap",
Value: size.String(),
}, nil
return ok, NewPushPair(
"Swap " + r.Matcher,
size.String(),
), nil
case ThresholdTypePercent:
ok, err := t.True(percent)
if err != nil {
return false, nil, err
}
return ok, &PushPair{
Key: r.Matcher + "of Swap",
Value: fmt.Sprintf("%.2f%%", percent*100),
}, nil
return ok, NewPushPair(
"Swap " + r.Matcher,
fmt.Sprintf("%.2f%%", percent*100),
), nil
default:
return false, nil, errors.Join(ErrInvalidRule, fmt.Errorf("invalid threshold type for swap: %s", t.ThresholdType.Name()))
}
@ -213,19 +213,19 @@ func (r *Rule) shouldNotifyDisk(s []diskStatus, t *Threshold) (bool, *PushPair,
if err != nil {
return false, nil, err
}
return ok, &PushPair{
Key: r.Matcher,
Value: disk.Used.String(),
}, nil
return ok, NewPushPair(
r.Matcher,
disk.Used.String(),
), nil
case ThresholdTypePercent:
ok, err := t.True(disk.UsedPercent)
if err != nil {
return false, nil, err
}
return ok, &PushPair{
Key: r.Matcher,
Value: fmt.Sprintf("%.2f%%", disk.UsedPercent),
}, nil
return ok, NewPushPair(
r.Matcher,
fmt.Sprintf("%.2f%%", disk.UsedPercent),
), nil
default:
return false, nil, errors.Join(ErrInvalidRule, fmt.Errorf("invalid threshold type for disk: %s", t.ThresholdType.Name()))
}
@ -278,10 +278,10 @@ func (r *Rule) shouldNotifyNetwork(s []networkStatus, t *Threshold) (bool, *Push
if err != nil {
return false, nil, err
}
return ok, &PushPair{
Key: r.Matcher,
Value: speed.String()+"/s",
}, nil
return ok, NewPushPair(
r.Matcher,
speed.String() + "/s",
), nil
case ThresholdTypeSize:
size := Size(0)
if in {
@ -294,10 +294,10 @@ func (r *Rule) shouldNotifyNetwork(s []networkStatus, t *Threshold) (bool, *Push
if err != nil {
return false, nil, err
}
return ok, &PushPair{
Key: r.Matcher,
Value: size.String(),
}, nil
return ok, NewPushPair(
r.Matcher,
size.String(),
), nil
default:
return false, nil, errors.Join(ErrInvalidRule, fmt.Errorf("invalid threshold type for network: %s", t.ThresholdType.Name()))
}
@ -328,10 +328,10 @@ func (r *Rule) shouldNotifyTemperature(s []temperatureStatus, t *Threshold) (boo
if err != nil {
return false, nil, err
}
return ok, &PushPair{
Key: r.Matcher,
Value: fmt.Sprintf("%.2f°C", temp.Value),
}, nil
return ok, NewPushPair(
r.Matcher,
fmt.Sprintf("%.2f°C", temp.Value),
), nil
default:
return false, nil, errors.Join(ErrInvalidRule, fmt.Errorf("invalid threshold type for temperature: %s", t.ThresholdType.Name()))
}

View File

@ -21,4 +21,4 @@ func _parseSize(s string, expect model.Size, t *testing.T) {
if size != expect {
t.Errorf("expect %s, got %s", expect, size)
}
}
}

View File

@ -9,7 +9,7 @@ import (
"strings"
"github.com/lollipopkit/gommon/log"
"github.com/lollipopkit/gommon/util"
os_ "github.com/lollipopkit/gommon/os"
"github.com/lollipopkit/server_box_monitor/res"
)
@ -96,14 +96,14 @@ func (ns *networkStatus) TransmitSpeed() (Size, error) {
return 0, ErrNotReady
}
diff := float64(ns.TimeSequence.New.Transmit - ns.TimeSequence.Old.Transmit)
return Size(diff / GetIntervalInSeconds()), nil
return Size(diff / CheckInterval.Seconds()), nil
}
func (ns *networkStatus) ReceiveSpeed() (Size, error) {
if ns.TimeSequence.New == nil || ns.TimeSequence.Old == nil {
return 0, ErrNotReady
}
diff := float64(ns.TimeSequence.New.Receive - ns.TimeSequence.Old.Receive)
return Size(diff / GetIntervalInSeconds()), nil
return Size(diff / CheckInterval.Seconds()), nil
}
type AllNetworkStatus []networkStatus
@ -132,7 +132,7 @@ func (nss AllNetworkStatus) ReceiveSpeed() (Size, error) {
}
func RefreshStatus() error {
output, _ := util.Execute("bash", res.ServerBoxShellPath)
output, _ := os_.Execute("bash", res.ServerBoxShellPath)
err := os.WriteFile(filepath.Join(res.ServerBoxDirPath, "shell_output.log"), []byte(output), 0644)
if err != nil {
log.Warn("[STATUS] write shell output log failed: %s", err)

View File

@ -84,7 +84,7 @@ func ParseToThreshold(s string) (*Threshold, error) {
return nil, err
}
}
return &Threshold{
ThresholdType: thresholdType,
Value: value,

View File

@ -7,7 +7,8 @@ import (
"time"
"github.com/lollipopkit/gommon/log"
"github.com/lollipopkit/gommon/util"
os_ "github.com/lollipopkit/gommon/os"
"github.com/lollipopkit/gommon/rate"
)
var (
@ -27,14 +28,17 @@ var (
AppConfigFileName = "config.json"
AppConfigPath = filepath.Join(ServerBoxDirPath, AppConfigFileName)
DefaultRateLimiter = rate.NewLimiter[string](time.Second * 10, 1)
)
const (
DefaultInterval = time.Second * 7
MaxInterval = time.Second * 10
)
func init() {
if !util.Exist(ServerBoxDirPath) {
if !os_.Exist(ServerBoxDirPath) {
err := os.MkdirAll(ServerBoxDirPath, 0755)
if err != nil {
log.Err("[INIT] Create dir error: %v", err)

View File

@ -46,7 +46,7 @@ func runCheck() {
panic(err)
}
for range time.NewTicker(model.GetInterval()).C {
for range time.NewTicker(model.CheckInterval).C {
err = model.RefreshStatus()
status := model.Status
if err != nil {
@ -77,11 +77,17 @@ func runCheck() {
pushPairsLock.RLock()
for _, push := range model.Config.Pushes {
if !model.RateLimiter.Check(push.Name) {
log.Warn("[PUSH] %s rate limit reached", push.Name)
continue
}
err := push.Push(pushPairs)
if err != nil {
log.Warn("[PUSH] %s error: %v", push.Name, err)
continue
}
// 仅推送成功才计数
model.RateLimiter.Acquire(push.Name)
log.Suc("[PUSH] %s success", push.Name)
}
pushPairsLock.RUnlock()
@ -104,5 +110,5 @@ func runWeb() {
e.GET("/status", web.Status)
e.Logger.Fatal(e.Start(":3770"))
e.Logger.Fatal(e.Start("0.0.0.0:3770"))
}