diff --git a/doc/CONFIG.jsonc b/doc/CONFIG.jsonc index 03e9fcd..f4034b4 100644 --- a/doc/CONFIG.jsonc +++ b/doc/CONFIG.jsonc @@ -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 } diff --git a/doc/CONFIG_zh.jsonc b/doc/CONFIG_zh.jsonc index c3452de..5da77d4 100644 --- a/doc/CONFIG_zh.jsonc +++ b/doc/CONFIG_zh.jsonc @@ -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 // diff --git a/go.mod b/go.mod index 7bfe8a8..661ed93 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 4049bac..4c30ee0 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index ef85884..1a213d6 100644 --- a/main.go +++ b/main.go @@ -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, + }) } diff --git a/model/config.go b/model/config.go index c5fe12e..a6654e6 100644 --- a/model/config.go +++ b/model/config.go @@ -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, - }, }, } ) diff --git a/model/push.go b/model/push.go index 798e5d1..afc3ad2 100644 --- a/model/push.go +++ b/model/push.go @@ -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 } diff --git a/model/rule.go b/model/rule.go index 0e9e807..0e11ea3 100644 --- a/model/rule.go +++ b/model/rule.go @@ -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())) } diff --git a/model/size_test.go b/model/size_test.go index 0d4323f..4d24978 100644 --- a/model/size_test.go +++ b/model/size_test.go @@ -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) } -} \ No newline at end of file +} diff --git a/model/status.go b/model/status.go index aa91592..5824794 100644 --- a/model/status.go +++ b/model/status.go @@ -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) diff --git a/model/threshold.go b/model/threshold.go index ffdcb48..2ebc050 100644 --- a/model/threshold.go +++ b/model/threshold.go @@ -84,7 +84,7 @@ func ParseToThreshold(s string) (*Threshold, error) { return nil, err } } - + return &Threshold{ ThresholdType: thresholdType, Value: value, diff --git a/res/res.go b/res/res.go index 06d6f28..c51f736 100644 --- a/res/res.go +++ b/res/res.go @@ -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) diff --git a/runner/runner.go b/runner/runner.go index d10eba2..443b59f 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -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")) }