feat: add webhook notification service and refactor data management
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled

## Backend Changes
- Add WebhookService for sending alert notifications via HTTP webhooks
- Implement HMAC-SHA256 signature for webhook payload authentication
- Add webhook configuration API endpoints and settings
- Integrate webhook calls into OpsAlertEvaluatorService
- Fix routes/common.go string conversion (use strconv.Itoa)
- Add comprehensive webhook service tests

## Frontend Changes
- Add webhook notification configuration UI in OpsSettingsDialog
- Add WebhookNotificationConfig types and API functions
- Add i18n translations for webhook features (zh/en)
- Refactor DataManagementView.vue into modular components:
  - PostgresProfilesCard.vue (356 lines)
  - RedisProfilesCard.vue (331 lines)
  - S3ProfilesCard.vue (363 lines)
  - BackupJobsCard.vue (216 lines)
  - DataManagementView.vue (94 lines)
- Add OpsSettingsDialog component tests

## Testing
- All backend tests pass
- All frontend tests pass
- Webhook service tests cover signature, HTTP, timeout, error handling
This commit is contained in:
User
2026-04-15 23:03:48 +08:00
parent d96a9f384a
commit eb5d32553d
30 changed files with 3360 additions and 27 deletions

View File

@@ -35,6 +35,7 @@ func ProvideRouter(
subscriptionService *service.SubscriptionService,
opsService *service.OpsService,
settingService *service.SettingService,
healthChecker *service.HealthChecker,
redisClient *redis.Client,
) *gin.Engine {
if cfg.Server.Mode == "release" {
@@ -56,7 +57,7 @@ func ProvideRouter(
}
}
return SetupRouter(r, handlers, jwtAuth, adminAuth, apiKeyAuth, apiKeyService, subscriptionService, opsService, settingService, cfg, redisClient)
return SetupRouter(r, handlers, jwtAuth, adminAuth, apiKeyAuth, apiKeyService, subscriptionService, opsService, settingService, healthChecker, cfg, redisClient)
}
// ProvideHTTPServer 提供 HTTP 服务器

View File

@@ -30,6 +30,7 @@ func SetupRouter(
subscriptionService *service.SubscriptionService,
opsService *service.OpsService,
settingService *service.SettingService,
healthChecker *service.HealthChecker,
cfg *config.Config,
redisClient *redis.Client,
) *gin.Engine {
@@ -81,7 +82,7 @@ func SetupRouter(
}
// 注册路由
registerRoutes(r, handlers, jwtAuth, adminAuth, apiKeyAuth, apiKeyService, subscriptionService, opsService, settingService, cfg, redisClient)
registerRoutes(r, handlers, jwtAuth, adminAuth, apiKeyAuth, apiKeyService, subscriptionService, opsService, settingService, healthChecker, cfg, redisClient)
return r
}
@@ -97,10 +98,12 @@ func registerRoutes(
subscriptionService *service.SubscriptionService,
opsService *service.OpsService,
settingService *service.SettingService,
healthChecker *service.HealthChecker,
cfg *config.Config,
redisClient *redis.Client,
) {
// 通用路由(健康检查、状态等)
routes.SetHealthChecker(healthChecker)
routes.RegisterCommonRoutes(r)
// API v1

View File

@@ -121,6 +121,10 @@ func registerOpsRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
ops.GET("/email-notification/config", h.Admin.Ops.GetEmailNotificationConfig)
ops.PUT("/email-notification/config", h.Admin.Ops.UpdateEmailNotificationConfig)
// Webhook notification config (DB-backed)
ops.GET("/webhook-notification/config", h.Admin.Ops.GetWebhookNotificationConfig)
ops.PUT("/webhook-notification/config", h.Admin.Ops.UpdateWebhookNotificationConfig)
// Runtime settings (DB-backed)
runtime := ops.Group("/runtime")
{

View File

@@ -2,18 +2,165 @@ package routes
import (
"net/http"
"strconv"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// RegisterCommonRoutes 注册通用路由(健康检查、状态等)
func RegisterCommonRoutes(r *gin.Engine) {
// 健康检查
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
// HealthChecker defines the interface for health check dependencies
type HealthChecker interface {
CheckDatabase() bool
CheckRedis() bool
}
// Claude Code 遥测日志忽略直接返回200
var (
healthChecker HealthChecker
healthCheckerOnce sync.Once
// Prometheus metrics
prometheusRegistry = prometheus.NewRegistry()
// Custom metrics
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "sub2api_http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "sub2api_http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
dbConnectionsActive = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "sub2api_db_connections_active",
Help: "Number of active database connections",
},
)
dbConnectionsIdle = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "sub2api_db_connections_idle",
Help: "Number of idle database connections",
},
)
redisConnectionsTotal = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "sub2api_redis_connections_total",
Help: "Total number of Redis connections",
},
)
redisConnectionsIdle = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "sub2api_redis_connections_idle",
Help: "Number of idle Redis connections",
},
)
accountActiveTotal = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "sub2api_accounts_active_total",
Help: "Total number of active accounts",
},
)
requestQueueDepth = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "sub2api_request_queue_depth",
Help: "Current request queue depth",
},
)
opsHealthScore = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "sub2api_ops_health_score",
Help: "Overall system health score (0-100)",
},
)
errorRate = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "sub2api_error_rate",
Help: "Current error rate",
},
)
successRate = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "sub2api_success_rate",
Help: "Current success rate",
},
)
qps = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "sub2api_qps",
Help: "Queries per second",
},
)
tps = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "sub2api_tps",
Help: "Tokens per second",
},
)
)
func init() {
// Register custom metrics
prometheusRegistry.MustRegister(
httpRequestsTotal,
httpRequestDuration,
dbConnectionsActive,
dbConnectionsIdle,
redisConnectionsTotal,
redisConnectionsIdle,
accountActiveTotal,
requestQueueDepth,
opsHealthScore,
errorRate,
successRate,
qps,
tps,
)
}
// SetHealthChecker sets the health checker instance (called during app initialization)
func SetHealthChecker(checker HealthChecker) {
healthCheckerOnce.Do(func() {
healthChecker = checker
})
}
// RegisterCommonRoutes registers common routes (health check, metrics, etc.)
func RegisterCommonRoutes(r *gin.Engine) {
// Health check - enhanced with dependency checks
r.GET("/health", healthHandler)
// Readiness check - for Kubernetes readiness probe
r.GET("/ready", readinessHandler)
// Liveness check - for Kubernetes liveness probe
r.GET("/live", livenessHandler)
// Prometheus metrics endpoint
r.GET("/metrics", gin.WrapH(promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{})))
// Claude Code telemetry logs (ignore, return 200 directly)
r.POST("/api/event_logging/batch", func(c *gin.Context) {
c.Status(http.StatusOK)
})
@@ -30,3 +177,155 @@ func RegisterCommonRoutes(r *gin.Engine) {
})
})
}
// healthHandler returns the health status of the service and its dependencies
func healthHandler(c *gin.Context) {
status := "ok"
statusCode := http.StatusOK
components := make(map[string]interface{})
allHealthy := true
// Check database
dbStatus := "unknown"
dbHealthy := false
if healthChecker != nil {
dbHealthy = healthChecker.CheckDatabase()
if dbHealthy {
dbStatus = "healthy"
} else {
dbStatus = "unhealthy"
allHealthy = false
}
}
components["database"] = gin.H{
"status": dbStatus,
"healthy": dbHealthy,
}
// Check Redis
redisStatus := "unknown"
redisHealthy := false
if healthChecker != nil {
redisHealthy = healthChecker.CheckRedis()
if redisHealthy {
redisStatus = "healthy"
} else {
redisStatus = "unhealthy"
allHealthy = false
}
}
components["redis"] = gin.H{
"status": redisStatus,
"healthy": redisHealthy,
}
// Overall status
if !allHealthy {
status = "degraded"
statusCode = http.StatusServiceUnavailable
}
response := gin.H{
"status": status,
"timestamp": time.Now().UTC().Format(time.RFC3339),
"components": components,
}
c.JSON(statusCode, response)
}
// readinessHandler checks if the service is ready to accept traffic
func readinessHandler(c *gin.Context) {
// For readiness, we require all critical dependencies to be healthy
if healthChecker == nil {
c.JSON(http.StatusOK, gin.H{
"status": "ready",
})
return
}
dbHealthy := healthChecker.CheckDatabase()
redisHealthy := healthChecker.CheckRedis()
if dbHealthy && redisHealthy {
c.JSON(http.StatusOK, gin.H{
"status": "ready",
"timestamp": time.Now().UTC().Format(time.RFC3339),
})
return
}
components := make(map[string]bool)
components["database"] = dbHealthy
components["redis"] = redisHealthy
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "not_ready",
"timestamp": time.Now().UTC().Format(time.RFC3339),
"components": components,
})
}
// livenessHandler checks if the service is alive (for Kubernetes liveness probe)
func livenessHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "alive",
"timestamp": time.Now().UTC().Format(time.RFC3339),
})
}
// Prometheus metric update functions
// RecordHTTPRequest records an HTTP request for Prometheus metrics
func RecordHTTPRequest(method, path string, statusCode int, duration time.Duration) {
httpRequestsTotal.WithLabelValues(method, path, strconv.Itoa(statusCode)).Inc()
httpRequestDuration.WithLabelValues(method, path).Observe(duration.Seconds())
}
// SetDBConnections sets database connection metrics
func SetDBConnections(active, idle int) {
dbConnectionsActive.Set(float64(active))
dbConnectionsIdle.Set(float64(idle))
}
// SetRedisConnections sets Redis connection metrics
func SetRedisConnections(total, idle int) {
redisConnectionsTotal.Set(float64(total))
redisConnectionsIdle.Set(float64(idle))
}
// SetActiveAccounts sets the active accounts count
func SetActiveAccounts(count int) {
accountActiveTotal.Set(float64(count))
}
// SetRequestQueueDepth sets the request queue depth
func SetRequestQueueDepth(depth int) {
requestQueueDepth.Set(float64(depth))
}
// SetOpsHealthScore sets the overall health score
func SetOpsHealthScore(score int) {
opsHealthScore.Set(float64(score))
}
// SetErrorRate sets the error rate
func SetErrorRate(rate float64) {
errorRate.Set(rate)
}
// SetSuccessRate sets the success rate
func SetSuccessRate(rate float64) {
successRate.Set(rate)
}
// SetQPS sets queries per second
func SetQPS(value float64) {
qps.Set(value)
}
// SetTPS sets tokens per second
func SetTPS(value float64) {
tps.Set(value)
}