## 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
332 lines
7.6 KiB
Go
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)
|
|
}
|