| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- package admin
- import (
- "fmt"
- "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)
- }
- // GetDashboardOpenAITokenStats returns OpenAI token efficiency stats grouped by model.
- // GET /api/v1/admin/ops/dashboard/openai-token-stats
- func (h *OpsHandler) GetDashboardOpenAITokenStats(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
- }
- filter, err := parseOpsOpenAITokenStatsFilter(c)
- if err != nil {
- response.BadRequest(c, err.Error())
- return
- }
- data, err := h.opsService.GetOpenAITokenStats(c.Request.Context(), filter)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, data)
- }
- func parseOpsOpenAITokenStatsFilter(c *gin.Context) (*service.OpsOpenAITokenStatsFilter, error) {
- if c == nil {
- return nil, fmt.Errorf("invalid request")
- }
- timeRange := strings.TrimSpace(c.Query("time_range"))
- if timeRange == "" {
- timeRange = "30d"
- }
- dur, ok := parseOpsOpenAITokenStatsDuration(timeRange)
- if !ok {
- return nil, fmt.Errorf("invalid time_range")
- }
- end := time.Now().UTC()
- start := end.Add(-dur)
- filter := &service.OpsOpenAITokenStatsFilter{
- TimeRange: timeRange,
- StartTime: start,
- EndTime: end,
- Platform: strings.TrimSpace(c.Query("platform")),
- }
- if v := strings.TrimSpace(c.Query("group_id")); v != "" {
- id, err := strconv.ParseInt(v, 10, 64)
- if err != nil || id <= 0 {
- return nil, fmt.Errorf("invalid group_id")
- }
- filter.GroupID = &id
- }
- topNRaw := strings.TrimSpace(c.Query("top_n"))
- pageRaw := strings.TrimSpace(c.Query("page"))
- pageSizeRaw := strings.TrimSpace(c.Query("page_size"))
- if topNRaw != "" && (pageRaw != "" || pageSizeRaw != "") {
- return nil, fmt.Errorf("invalid query: top_n cannot be used with page/page_size")
- }
- if topNRaw != "" {
- topN, err := strconv.Atoi(topNRaw)
- if err != nil || topN < 1 || topN > 100 {
- return nil, fmt.Errorf("invalid top_n")
- }
- filter.TopN = topN
- return filter, nil
- }
- filter.Page = 1
- filter.PageSize = 20
- if pageRaw != "" {
- page, err := strconv.Atoi(pageRaw)
- if err != nil || page < 1 {
- return nil, fmt.Errorf("invalid page")
- }
- filter.Page = page
- }
- if pageSizeRaw != "" {
- pageSize, err := strconv.Atoi(pageSizeRaw)
- if err != nil || pageSize < 1 || pageSize > 100 {
- return nil, fmt.Errorf("invalid page_size")
- }
- filter.PageSize = pageSize
- }
- return filter, nil
- }
- func parseOpsOpenAITokenStatsDuration(v string) (time.Duration, bool) {
- switch strings.TrimSpace(v) {
- case "30m":
- return 30 * time.Minute, true
- case "1h":
- return time.Hour, true
- case "1d":
- return 24 * time.Hour, true
- case "15d":
- return 15 * 24 * time.Hour, true
- case "30d":
- return 30 * 24 * time.Hour, true
- default:
- return 0, false
- }
- }
- 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)
- }
|