mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-30 17:05:19 +08:00
4e28fc60a3
- 通知渠道支持配置多个 SMTP 账号,配置整体加密落库,删除时清理规则与事件设置中的引用 - 告警规则支持 18 种指标:CPU/内存/Swap/负载/磁盘用量与 inode/磁盘与网卡速率/网站 5xx 与错误率、 服务与项目与容器与应用与数据库运行状态、证书与网站剩余天数,可配连续命中次数与静默期 - 系统事件通知覆盖证书续签失败、备份失败、后台任务失败、计划任务失败、网站到期、防篡改拦截、 面板健康问题、面板登录与爆破、SSH 登录与爆破 - 计划任务统一经 wrapper 执行以捕获退出码,flock 抢锁失败不计为失败 - 监控页改造为系统/告警/设置三 Tab - 数据库驱动建连打通 context 并补齐连接超时,探测不再阻塞告警评估 close #1041 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
125 lines
3.8 KiB
Go
125 lines
3.8 KiB
Go
package data
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/libtnb/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/acepanel/panel/v3/internal/app"
|
|
"github.com/acepanel/panel/v3/internal/biz"
|
|
)
|
|
|
|
func newDBForTest(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
// 渠道配置落库前会用 app.Key 加密
|
|
app.Key = "0123456789abcdef0123456789abcdef"
|
|
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{SkipDefaultTransaction: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.AutoMigrate(&biz.NotifyChannel{}, &biz.AlertRule{}, &biz.Alert{}, &biz.Setting{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
// 渠道配置以原始 JSON 存取,读回后应与写入完全一致
|
|
func TestNotifyChannelConfigRoundTrip(t *testing.T) {
|
|
repo := ¬ifyChannelRepo{db: newDBForTest(t)}
|
|
|
|
config := json.RawMessage(`{"host":"smtp.example.com","port":465,"encryption":"ssl","to":["a@b.c"]}`)
|
|
channel := &biz.NotifyChannel{Name: "mail", Type: "smtp", Config: config, Enabled: true}
|
|
if err := repo.Create(channel); err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
got, err := repo.Get(channel.ID)
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
|
|
var want, actual map[string]any
|
|
if err = json.Unmarshal(config, &want); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = json.Unmarshal(got.Config, &actual); err != nil {
|
|
t.Fatalf("unmarshal stored config %q: %v", string(got.Config), err)
|
|
}
|
|
if len(actual) != len(want) || actual["host"] != want["host"] {
|
|
t.Fatalf("config mismatch: %s", string(got.Config))
|
|
}
|
|
}
|
|
|
|
// 告警规则的渠道列表以 JSON 序列化存储
|
|
func TestAlertRuleChannelsRoundTrip(t *testing.T) {
|
|
repo := &alertRepo{db: newDBForTest(t)}
|
|
|
|
rule := &biz.AlertRule{Name: "cpu", Type: biz.AlertTypeCPU, Operator: biz.AlertOperatorGT, Threshold: 90, Duration: 3, Silence: 30, Channels: []uint{1, 2}, Enabled: true}
|
|
if err := repo.CreateRule(rule); err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
got, err := repo.GetRule(rule.ID)
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
if len(got.Channels) != 2 || got.Channels[0] != 1 || got.Channels[1] != 2 {
|
|
t.Fatalf("channels mismatch: %v", got.Channels)
|
|
}
|
|
}
|
|
|
|
// 删除渠道后,告警规则与事件设置中的引用应一并清除,否则通知会静默失效
|
|
func TestNotifyChannelDeleteCleansReferences(t *testing.T) {
|
|
db := newDBForTest(t)
|
|
channels := ¬ifyChannelRepo{db: db}
|
|
alerts := &alertRepo{db: db}
|
|
|
|
kept := &biz.NotifyChannel{Name: "kept", Type: "smtp", Config: json.RawMessage(`{}`), Enabled: true}
|
|
if err := channels.Create(kept); err != nil {
|
|
t.Fatalf("create kept: %v", err)
|
|
}
|
|
removed := &biz.NotifyChannel{Name: "removed", Type: "smtp", Config: json.RawMessage(`{}`), Enabled: true}
|
|
if err := channels.Create(removed); err != nil {
|
|
t.Fatalf("create removed: %v", err)
|
|
}
|
|
|
|
rule := &biz.AlertRule{Name: "cpu", Type: biz.AlertTypeCPU, Operator: biz.AlertOperatorGT, Threshold: 90, Duration: 1, Channels: []uint{kept.ID, removed.ID}, Enabled: true}
|
|
if err := alerts.CreateRule(rule); err != nil {
|
|
t.Fatalf("create rule: %v", err)
|
|
}
|
|
|
|
events, err := json.Marshal([]uint{kept.ID, removed.ID})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.Create(&biz.Setting{Key: biz.SettingKeyNotifyEventChannels, Value: string(events)}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err = channels.Delete(removed.ID); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
|
|
got, err := alerts.GetRule(rule.ID)
|
|
if err != nil {
|
|
t.Fatalf("get rule: %v", err)
|
|
}
|
|
if len(got.Channels) != 1 || got.Channels[0] != kept.ID {
|
|
t.Fatalf("rule channels not cleaned: %v", got.Channels)
|
|
}
|
|
|
|
setting := new(biz.Setting)
|
|
if err = db.Where("key = ?", biz.SettingKeyNotifyEventChannels).First(setting).Error; err != nil {
|
|
t.Fatalf("get setting: %v", err)
|
|
}
|
|
var remain []uint
|
|
if err = json.Unmarshal([]byte(setting.Value), &remain); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(remain) != 1 || remain[0] != kept.ID {
|
|
t.Fatalf("event channels not cleaned: %v", remain)
|
|
}
|
|
}
|