Files
BackupX/server/internal/service/settings_service.go
Wu Qing 5021fe665e 功能: 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 扫描
2026-04-20 23:26:04 +08:00

99 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"backupx/server/internal/apperror"
"backupx/server/internal/model"
"backupx/server/internal/repository"
)
// AuditWebhookConfigurer 抽象审计 webhook 配置接口,由 AuditService 实现。
// 用接口解耦避免 settings_service 直接依赖 AuditService 具体类型。
type AuditWebhookConfigurer interface {
SetWebhook(url, secret string)
}
type SettingsService struct {
configs repository.SystemConfigRepository
auditWebhook AuditWebhookConfigurer
}
func NewSettingsService(configs repository.SystemConfigRepository) *SettingsService {
return &SettingsService{configs: configs}
}
// SetAuditWebhookConfigurer 注入 audit webhook 配置接收方。
// 启动时立即用当前 DB 中的设置调用一次,后续每次 Update 变更 webhook key 时同步推送。
func (s *SettingsService) SetAuditWebhookConfigurer(ctx context.Context, configurer AuditWebhookConfigurer) {
if s == nil || configurer == nil {
return
}
s.auditWebhook = configurer
// 启动时同步一次,保证重启后配置不丢失
all, err := s.GetAll(ctx)
if err == nil {
configurer.SetWebhook(all[SettingKeyAuditWebhookURL], all[SettingKeyAuditWebhookSecret])
}
}
// 可被前端写入的系统设置键。新增键必须同步加入此清单,
// 否则 Update 会忽略(安全原则:显式 allow-list
const (
SettingKeySiteName = "site_name"
SettingKeyLanguage = "language"
SettingKeyTimezone = "timezone"
SettingKeyBackupNotificationEnabled = "backup_notification_enabled"
SettingKeyBandwidthLimit = "bandwidth_limit"
SettingKeyAuditWebhookURL = "audit_webhook_url"
SettingKeyAuditWebhookSecret = "audit_webhook_secret"
)
var settingsKeys = []string{
SettingKeySiteName,
SettingKeyLanguage,
SettingKeyTimezone,
SettingKeyBackupNotificationEnabled,
SettingKeyBandwidthLimit,
SettingKeyAuditWebhookURL,
SettingKeyAuditWebhookSecret,
}
func (s *SettingsService) GetAll(ctx context.Context) (map[string]string, error) {
items, err := s.configs.List(ctx)
if err != nil {
return nil, apperror.Internal("SETTINGS_LIST_FAILED", "无法获取系统设置", err)
}
result := make(map[string]string, len(items))
for _, item := range items {
result[item.Key] = item.Value
}
return result, nil
}
func (s *SettingsService) Update(ctx context.Context, settings map[string]string) (map[string]string, error) {
allowed := make(map[string]bool, len(settingsKeys))
for _, key := range settingsKeys {
allowed[key] = true
}
auditWebhookTouched := false
for key, value := range settings {
if !allowed[key] {
continue
}
item := &model.SystemConfig{Key: key, Value: value}
if err := s.configs.Upsert(ctx, item); err != nil {
return nil, apperror.Internal("SETTINGS_UPDATE_FAILED", "无法更新系统设置", err)
}
if key == SettingKeyAuditWebhookURL || key == SettingKeyAuditWebhookSecret {
auditWebhookTouched = true
}
}
// audit webhook 配置变化:立即同步到 AuditService避免重启才生效
if auditWebhookTouched && s.auditWebhook != nil {
all, _ := s.GetAll(ctx)
s.auditWebhook.SetWebhook(all[SettingKeyAuditWebhookURL], all[SettingKeyAuditWebhookSecret])
}
return s.GetAll(ctx)
}