Files
tokens-reef/backend/internal/server/routes/common.go
User eb5d32553d
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
feat: add webhook notification service and refactor data management
## 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
2026-04-15 23:03:48 +08:00

332 lines
7.6 KiB
Go

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"
)
// HealthChecker defines the interface for health check dependencies
type HealthChecker interface {
CheckDatabase() bool
CheckRedis() bool
}
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)
})
// Setup status endpoint (always returns needs_setup: false in normal mode)
// This is used by the frontend to detect when the service has restarted after setup
r.GET("/setup/status", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{
"needs_setup": false,
"step": "completed",
},
})
})
}
// 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)
}