- 新增 ops 错误日志记录器(ops_error_logger.go) - 新增 ops 主处理器(ops_handler.go) - 新增告警管理处理器(ops_alerts_handler.go) - 新增仪表板处理器(ops_dashboard_handler.go) - 新增实时监控处理器(ops_realtime_handler.go) - 新增配置管理处理器(ops_settings_handler.go) - 新增 WebSocket 处理器(ops_ws_handler.go) - 扩展设置 DTO 支持 ops 配置 - 新增客户端请求 ID 中间件(client_request_id.go) - 新增 WebSocket 查询令牌认证中间件(ws_query_token_auth.go) - 更新管理员认证中间件支持 ops 路由 - 注册 handler 依赖注入
244 lines
6.3 KiB
Go
244 lines
6.3 KiB
Go
package admin
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// GetDashboardOverview returns vNext ops dashboard overview (raw path).
|
|
// GET /api/v1/admin/ops/dashboard/overview
|
|
func (h *OpsHandler) GetDashboardOverview(c *gin.Context) {
|
|
if h.opsService == nil {
|
|
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
|
|
return
|
|
}
|
|
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
startTime, endTime, err := parseOpsTimeRange(c, "1h")
|
|
if err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
filter := &service.OpsDashboardFilter{
|
|
StartTime: startTime,
|
|
EndTime: endTime,
|
|
Platform: strings.TrimSpace(c.Query("platform")),
|
|
QueryMode: parseOpsQueryMode(c),
|
|
}
|
|
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
|
|
id, err := strconv.ParseInt(v, 10, 64)
|
|
if err != nil || id <= 0 {
|
|
response.BadRequest(c, "Invalid group_id")
|
|
return
|
|
}
|
|
filter.GroupID = &id
|
|
}
|
|
|
|
data, err := h.opsService.GetDashboardOverview(c.Request.Context(), filter)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
|
|
// GetDashboardThroughputTrend returns throughput time series (raw path).
|
|
// GET /api/v1/admin/ops/dashboard/throughput-trend
|
|
func (h *OpsHandler) GetDashboardThroughputTrend(c *gin.Context) {
|
|
if h.opsService == nil {
|
|
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
|
|
return
|
|
}
|
|
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
startTime, endTime, err := parseOpsTimeRange(c, "1h")
|
|
if err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
filter := &service.OpsDashboardFilter{
|
|
StartTime: startTime,
|
|
EndTime: endTime,
|
|
Platform: strings.TrimSpace(c.Query("platform")),
|
|
QueryMode: parseOpsQueryMode(c),
|
|
}
|
|
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
|
|
id, err := strconv.ParseInt(v, 10, 64)
|
|
if err != nil || id <= 0 {
|
|
response.BadRequest(c, "Invalid group_id")
|
|
return
|
|
}
|
|
filter.GroupID = &id
|
|
}
|
|
|
|
bucketSeconds := pickThroughputBucketSeconds(endTime.Sub(startTime))
|
|
data, err := h.opsService.GetThroughputTrend(c.Request.Context(), filter, bucketSeconds)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
|
|
// GetDashboardLatencyHistogram returns the latency distribution histogram (success requests).
|
|
// GET /api/v1/admin/ops/dashboard/latency-histogram
|
|
func (h *OpsHandler) GetDashboardLatencyHistogram(c *gin.Context) {
|
|
if h.opsService == nil {
|
|
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
|
|
return
|
|
}
|
|
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
startTime, endTime, err := parseOpsTimeRange(c, "1h")
|
|
if err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
filter := &service.OpsDashboardFilter{
|
|
StartTime: startTime,
|
|
EndTime: endTime,
|
|
Platform: strings.TrimSpace(c.Query("platform")),
|
|
QueryMode: parseOpsQueryMode(c),
|
|
}
|
|
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
|
|
id, err := strconv.ParseInt(v, 10, 64)
|
|
if err != nil || id <= 0 {
|
|
response.BadRequest(c, "Invalid group_id")
|
|
return
|
|
}
|
|
filter.GroupID = &id
|
|
}
|
|
|
|
data, err := h.opsService.GetLatencyHistogram(c.Request.Context(), filter)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
|
|
// GetDashboardErrorTrend returns error counts time series (raw path).
|
|
// GET /api/v1/admin/ops/dashboard/error-trend
|
|
func (h *OpsHandler) GetDashboardErrorTrend(c *gin.Context) {
|
|
if h.opsService == nil {
|
|
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
|
|
return
|
|
}
|
|
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
startTime, endTime, err := parseOpsTimeRange(c, "1h")
|
|
if err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
filter := &service.OpsDashboardFilter{
|
|
StartTime: startTime,
|
|
EndTime: endTime,
|
|
Platform: strings.TrimSpace(c.Query("platform")),
|
|
QueryMode: parseOpsQueryMode(c),
|
|
}
|
|
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
|
|
id, err := strconv.ParseInt(v, 10, 64)
|
|
if err != nil || id <= 0 {
|
|
response.BadRequest(c, "Invalid group_id")
|
|
return
|
|
}
|
|
filter.GroupID = &id
|
|
}
|
|
|
|
bucketSeconds := pickThroughputBucketSeconds(endTime.Sub(startTime))
|
|
data, err := h.opsService.GetErrorTrend(c.Request.Context(), filter, bucketSeconds)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
|
|
// GetDashboardErrorDistribution returns error distribution by status code (raw path).
|
|
// GET /api/v1/admin/ops/dashboard/error-distribution
|
|
func (h *OpsHandler) GetDashboardErrorDistribution(c *gin.Context) {
|
|
if h.opsService == nil {
|
|
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
|
|
return
|
|
}
|
|
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
startTime, endTime, err := parseOpsTimeRange(c, "1h")
|
|
if err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
filter := &service.OpsDashboardFilter{
|
|
StartTime: startTime,
|
|
EndTime: endTime,
|
|
Platform: strings.TrimSpace(c.Query("platform")),
|
|
QueryMode: parseOpsQueryMode(c),
|
|
}
|
|
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
|
|
id, err := strconv.ParseInt(v, 10, 64)
|
|
if err != nil || id <= 0 {
|
|
response.BadRequest(c, "Invalid group_id")
|
|
return
|
|
}
|
|
filter.GroupID = &id
|
|
}
|
|
|
|
data, err := h.opsService.GetErrorDistribution(c.Request.Context(), filter)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
|
|
func pickThroughputBucketSeconds(window time.Duration) int {
|
|
// Keep buckets predictable and avoid huge responses.
|
|
switch {
|
|
case window <= 2*time.Hour:
|
|
return 60
|
|
case window <= 24*time.Hour:
|
|
return 300
|
|
default:
|
|
return 3600
|
|
}
|
|
}
|
|
|
|
func parseOpsQueryMode(c *gin.Context) service.OpsQueryMode {
|
|
if c == nil {
|
|
return ""
|
|
}
|
|
raw := strings.TrimSpace(c.Query("mode"))
|
|
if raw == "" {
|
|
// Empty means "use server default" (DB setting ops_query_mode_default).
|
|
return ""
|
|
}
|
|
return service.ParseOpsQueryMode(raw)
|
|
}
|