From c4007afe6b6f6f03065fb9eb72ef22353bb58358 Mon Sep 17 00:00:00 2001 From: User Date: Thu, 16 Apr 2026 12:01:12 +0800 Subject: [PATCH] feat: add Sora admin page and integrate DB/Redis Prometheus metrics - Create SoraAdminView with overview, user stats, and generations tabs - Add /admin/sora route for Sora management - Add i18n support (zh/en) for Sora admin page - Extract Prometheus metrics to prommetrics package to avoid import cycles - Integrate SetDBConnections/SetRedisConnections in OpsMetricsCollector --- backend/internal/prommetrics/metrics.go | 182 ++++++++ backend/internal/server/routes/common.go | 149 +----- .../internal/service/ops_metrics_collector.go | 7 + frontend/src/i18n/locales/en.ts | 23 + frontend/src/i18n/locales/zh.ts | 23 + frontend/src/router/index.ts | 12 + frontend/src/views/admin/SoraAdminView.vue | 430 ++++++++++++++++++ 7 files changed, 691 insertions(+), 135 deletions(-) create mode 100644 backend/internal/prommetrics/metrics.go create mode 100644 frontend/src/views/admin/SoraAdminView.vue diff --git a/backend/internal/prommetrics/metrics.go b/backend/internal/prommetrics/metrics.go new file mode 100644 index 00000000..b5d278b2 --- /dev/null +++ b/backend/internal/prommetrics/metrics.go @@ -0,0 +1,182 @@ +// Package prommetrics provides Prometheus metrics registration and update functions. +// This package is intentionally separate from routes to avoid import cycles. +package prommetrics + +import ( + "strconv" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + // Prometheus registry + Registry = 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 + Registry.MustRegister( + httpRequestsTotal, + httpRequestDuration, + dbConnectionsActive, + dbConnectionsIdle, + redisConnectionsTotal, + redisConnectionsIdle, + accountActiveTotal, + requestQueueDepth, + opsHealthScore, + errorRate, + successRate, + qps, + tps, + ) +} + +// 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) +} diff --git a/backend/internal/server/routes/common.go b/backend/internal/server/routes/common.go index fe6a90cd..27d5a23d 100644 --- a/backend/internal/server/routes/common.go +++ b/backend/internal/server/routes/common.go @@ -2,12 +2,11 @@ package routes import ( "net/http" - "strconv" "sync" "time" + "github.com/Wei-Shaw/sub2api/internal/prommetrics" "github.com/gin-gonic/gin" - "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -20,125 +19,8 @@ type HealthChecker interface { 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() { @@ -157,8 +39,8 @@ func RegisterCommonRoutes(r *gin.Engine) { // Liveness check - for Kubernetes liveness probe r.GET("/live", livenessHandler) - // Prometheus metrics endpoint - r.GET("/metrics", gin.WrapH(promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{}))) + // Prometheus metrics endpoint - use the shared registry from prommetrics package + r.GET("/metrics", gin.WrapH(promhttp.HandlerFor(prommetrics.Registry, promhttp.HandlerOpts{}))) // Claude Code telemetry logs (ignore, return 200 directly) r.POST("/api/event_logging/batch", func(c *gin.Context) { @@ -275,57 +157,54 @@ func livenessHandler(c *gin.Context) { }) } -// Prometheus metric update functions +// Prometheus metric update functions - delegate to prommetrics package // 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()) + prommetrics.RecordHTTPRequest(method, path, statusCode, duration) } // SetDBConnections sets database connection metrics func SetDBConnections(active, idle int) { - dbConnectionsActive.Set(float64(active)) - dbConnectionsIdle.Set(float64(idle)) + prommetrics.SetDBConnections(active, idle) } // SetRedisConnections sets Redis connection metrics func SetRedisConnections(total, idle int) { - redisConnectionsTotal.Set(float64(total)) - redisConnectionsIdle.Set(float64(idle)) + prommetrics.SetRedisConnections(total, idle) } // SetActiveAccounts sets the active accounts count func SetActiveAccounts(count int) { - accountActiveTotal.Set(float64(count)) + prommetrics.SetActiveAccounts(count) } // SetRequestQueueDepth sets the request queue depth func SetRequestQueueDepth(depth int) { - requestQueueDepth.Set(float64(depth)) + prommetrics.SetRequestQueueDepth(depth) } // SetOpsHealthScore sets the overall health score func SetOpsHealthScore(score int) { - opsHealthScore.Set(float64(score)) + prommetrics.SetOpsHealthScore(score) } // SetErrorRate sets the error rate func SetErrorRate(rate float64) { - errorRate.Set(rate) + prommetrics.SetErrorRate(rate) } // SetSuccessRate sets the success rate func SetSuccessRate(rate float64) { - successRate.Set(rate) + prommetrics.SetSuccessRate(rate) } // SetQPS sets queries per second func SetQPS(value float64) { - qps.Set(value) + prommetrics.SetQPS(value) } // SetTPS sets tokens per second func SetTPS(value float64) { - tps.Set(value) + prommetrics.SetTPS(value) } diff --git a/backend/internal/service/ops_metrics_collector.go b/backend/internal/service/ops_metrics_collector.go index f93481e7..5d8600d7 100644 --- a/backend/internal/service/ops_metrics_collector.go +++ b/backend/internal/service/ops_metrics_collector.go @@ -16,6 +16,7 @@ import ( "unicode/utf8" "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/Wei-Shaw/sub2api/internal/prommetrics" "github.com/google/uuid" "github.com/redis/go-redis/v9" "github.com/shirou/gopsutil/v4/cpu" @@ -270,6 +271,12 @@ func (c *OpsMetricsCollector) collectAndPersist(ctx context.Context) error { active, idle := c.dbPoolStats() redisTotal, redisIdle, redisStatsOK := c.redisPoolStats() + // Update Prometheus metrics for DB/Redis connections + prommetrics.SetDBConnections(active, idle) + if redisStatsOK { + prommetrics.SetRedisConnections(redisTotal, redisIdle) + } + successCount, tokenConsumed, err := c.queryUsageCounts(ctx, windowStart, windowEnd) if err != nil { return fmt.Errorf("query usage counts: %w", err) diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index f9b840fd..74eaf9b3 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -3124,6 +3124,29 @@ export default { "Are you sure you want to delete '{name}'? Accounts using this proxy will have their proxy removed." }, + // Sora Management + sora: { + title: 'Sora Management', + description: 'Manage Sora video generation service and user quotas', + overview: 'Overview', + userStats: 'User Stats', + generations: 'Generations', + totalUsers: 'Total Users', + totalGenerations: 'Total Generations', + activeGenerations: 'Active Generations', + totalStorage: 'Total Storage', + byStatus: 'By Status', + byModel: 'By Model', + quota: 'Quota', + used: 'Used', + model: 'Model', + status: 'Status', + size: 'Size', + createdAt: 'Created At', + clearStorage: 'Clear Storage', + confirmClearStorage: 'Are you sure you want to clear this user\'s Sora storage? This action cannot be undone.' + }, + // Redeem Codes redeem: { title: 'Redeem Code Management', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index b9b72944..95adc664 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -3250,6 +3250,29 @@ export default { deleteConfirm: "确定要删除代理 '{name}' 吗?使用此代理的账号将被移除代理设置。" }, + // Sora Management + sora: { + title: 'Sora 管理', + description: '管理 Sora 视频生成服务和用户配额', + overview: '概览', + userStats: '用户统计', + generations: '生成记录', + totalUsers: '总用户数', + totalGenerations: '总生成数', + activeGenerations: '活跃生成', + totalStorage: '总存储', + byStatus: '按状态分布', + byModel: '按模型分布', + quota: '配额', + used: '已用', + model: '模型', + status: '状态', + size: '大小', + createdAt: '创建时间', + clearStorage: '清除存储', + confirmClearStorage: '确定要清除该用户的 Sora 存储吗?此操作不可撤销。' + }, + // Redeem Codes Management redeem: { title: '兑换码管理', diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 1aa37dd5..347dc3d7 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -407,6 +407,18 @@ const routes: RouteRecordRaw[] = [ descriptionKey: 'admin.proxies.description' } }, + { + path: '/admin/sora', + name: 'AdminSora', + component: () => import('@/views/admin/SoraAdminView.vue'), + meta: { + requiresAuth: true, + requiresAdmin: true, + title: 'Sora Management', + titleKey: 'admin.sora.title', + descriptionKey: 'admin.sora.description' + } + }, { path: '/admin/redeem', name: 'AdminRedeem', diff --git a/frontend/src/views/admin/SoraAdminView.vue b/frontend/src/views/admin/SoraAdminView.vue new file mode 100644 index 00000000..17a42838 --- /dev/null +++ b/frontend/src/views/admin/SoraAdminView.vue @@ -0,0 +1,430 @@ + + +