mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-09-05 07:26:43 +08:00
功能: v2.1 可观测性与流控 (#47)
* 功能: v2.1 可观测性与流控 — Prometheus + 节点带宽 + 审计 Webhook 核心能力: - Prometheus /metrics 端点:11 类指标(任务/存储/节点/SLA/验证/恢复/复制) - 节点级带宽限速生效:model.Node.BandwidthLimit 覆盖全局默认 - 审计日志 Webhook 外输:HMAC-SHA256 签名,配合 SIEM 合规留档 实现: - server/internal/metrics/ 独立 Registry + 异步 Gauge Collector(30s) - backup/restore/verify/replication 服务注入 metrics 钩子,nil 安全 - resolveProviderForNode() 按 task.NodeID 解析 BandwidthLimit - AuditService.SetWebhook + 动态 settings 推送,无需重启 测试: - metrics/registry_test.go: 注册/采集/nil safety/HTTP handler - service/audit_service_webhook_test.go: 签名正确性/异步投递/禁用路径 - go test ./... 全部通过 * chore: 触发 CodeQL 扫描
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"backupx/server/internal/model"
|
||||
"backupx/server/internal/repository"
|
||||
)
|
||||
|
||||
// fakeAuditRepo 用通道同步等待异步写入,避免 sleep。
|
||||
type fakeAuditRepo struct {
|
||||
mu sync.Mutex
|
||||
logs []model.AuditLog
|
||||
created chan struct{}
|
||||
}
|
||||
|
||||
func newFakeAuditRepo() *fakeAuditRepo {
|
||||
return &fakeAuditRepo{created: make(chan struct{}, 4)}
|
||||
}
|
||||
|
||||
func (r *fakeAuditRepo) Create(_ context.Context, log *model.AuditLog) error {
|
||||
r.mu.Lock()
|
||||
log.CreatedAt = time.Now().UTC()
|
||||
r.logs = append(r.logs, *log)
|
||||
r.mu.Unlock()
|
||||
r.created <- struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeAuditRepo) List(context.Context, repository.AuditLogListOptions) (*repository.AuditLogListResult, error) {
|
||||
return &repository.AuditLogListResult{}, nil
|
||||
}
|
||||
|
||||
func (r *fakeAuditRepo) ListAll(context.Context, repository.AuditLogListOptions) ([]model.AuditLog, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestAuditService_WebhookDeliversSignedPayload(t *testing.T) {
|
||||
var hits atomic.Int32
|
||||
var got struct {
|
||||
sig string
|
||||
payload map[string]any
|
||||
received chan struct{}
|
||||
}
|
||||
got.received = make(chan struct{}, 1)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
hits.Add(1)
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
got.sig = r.Header.Get("X-BackupX-Signature")
|
||||
_ = json.Unmarshal(body, &got.payload)
|
||||
|
||||
// 验证 HMAC 正确
|
||||
mac := hmac.New(sha256.New, []byte("s3cret"))
|
||||
mac.Write(body)
|
||||
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
|
||||
if got.sig != expected {
|
||||
t.Errorf("signature mismatch: expected %s, got %s", expected, got.sig)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
got.received <- struct{}{}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
repo := newFakeAuditRepo()
|
||||
svc := NewAuditService(repo)
|
||||
svc.SetWebhook(server.URL, "s3cret")
|
||||
|
||||
svc.Record(AuditEntry{
|
||||
Username: "alice",
|
||||
Category: "auth",
|
||||
Action: "login_success",
|
||||
ClientIP: "10.0.0.1",
|
||||
Detail: "admin login",
|
||||
})
|
||||
|
||||
// 等待异步写入 + webhook
|
||||
select {
|
||||
case <-repo.created:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("audit log not written within 1s")
|
||||
}
|
||||
select {
|
||||
case <-got.received:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("webhook not invoked within 1s")
|
||||
}
|
||||
|
||||
if hits.Load() != 1 {
|
||||
t.Fatalf("expected 1 webhook hit, got %d", hits.Load())
|
||||
}
|
||||
if got.payload["eventType"] != "audit.log" {
|
||||
t.Errorf("eventType wrong: %v", got.payload["eventType"])
|
||||
}
|
||||
actor, ok := got.payload["actor"].(map[string]any)
|
||||
if !ok || actor["username"] != "alice" {
|
||||
t.Errorf("actor.username mismatch: %v", got.payload["actor"])
|
||||
}
|
||||
if got.payload["action"] != "login_success" {
|
||||
t.Errorf("action mismatch: %v", got.payload["action"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditService_WebhookDisabledWhenURLEmpty(t *testing.T) {
|
||||
repo := newFakeAuditRepo()
|
||||
svc := NewAuditService(repo)
|
||||
// 不调用 SetWebhook:应该不发送任何请求
|
||||
svc.Record(AuditEntry{Username: "bob", Action: "logout"})
|
||||
|
||||
select {
|
||||
case <-repo.created:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("audit log not written within 1s")
|
||||
}
|
||||
// 给 webhook 一些时间(即便它不会被调用)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// 无显式断言:能不 panic 即算通过
|
||||
}
|
||||
Reference in New Issue
Block a user