ops_realtime_handler.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. package admin
  2. import (
  3. "context"
  4. "errors"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/gin-gonic/gin"
  12. )
  13. // GetConcurrencyStats returns real-time concurrency usage aggregated by platform/group/account.
  14. // GET /api/v1/admin/ops/concurrency
  15. func (h *OpsHandler) GetConcurrencyStats(c *gin.Context) {
  16. if h.opsService == nil {
  17. response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
  18. return
  19. }
  20. if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
  21. response.ErrorFrom(c, err)
  22. return
  23. }
  24. if !h.opsService.IsRealtimeMonitoringEnabled(c.Request.Context()) {
  25. response.Success(c, gin.H{
  26. "enabled": false,
  27. "platform": map[string]*service.PlatformConcurrencyInfo{},
  28. "group": map[int64]*service.GroupConcurrencyInfo{},
  29. "account": map[int64]*service.AccountConcurrencyInfo{},
  30. "timestamp": time.Now().UTC(),
  31. })
  32. return
  33. }
  34. platformFilter := strings.TrimSpace(c.Query("platform"))
  35. var groupID *int64
  36. if v := strings.TrimSpace(c.Query("group_id")); v != "" {
  37. id, err := strconv.ParseInt(v, 10, 64)
  38. if err != nil || id <= 0 {
  39. response.BadRequest(c, "Invalid group_id")
  40. return
  41. }
  42. groupID = &id
  43. }
  44. platform, group, account, collectedAt, err := h.opsService.GetConcurrencyStats(c.Request.Context(), platformFilter, groupID)
  45. if err != nil {
  46. if isOpsRealtimeRequestCanceled(c, err) {
  47. return
  48. }
  49. response.ErrorFrom(c, err)
  50. return
  51. }
  52. payload := gin.H{
  53. "enabled": true,
  54. "platform": platform,
  55. "group": group,
  56. "account": account,
  57. }
  58. if collectedAt != nil {
  59. payload["timestamp"] = collectedAt.UTC()
  60. }
  61. response.Success(c, payload)
  62. }
  63. // GetUserConcurrencyStats returns real-time concurrency usage for all active users.
  64. // GET /api/v1/admin/ops/user-concurrency
  65. func (h *OpsHandler) GetUserConcurrencyStats(c *gin.Context) {
  66. if h.opsService == nil {
  67. response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
  68. return
  69. }
  70. if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
  71. response.ErrorFrom(c, err)
  72. return
  73. }
  74. if !h.opsService.IsRealtimeMonitoringEnabled(c.Request.Context()) {
  75. response.Success(c, gin.H{
  76. "enabled": false,
  77. "user": map[int64]*service.UserConcurrencyInfo{},
  78. "timestamp": time.Now().UTC(),
  79. })
  80. return
  81. }
  82. users, collectedAt, err := h.opsService.GetUserConcurrencyStats(c.Request.Context())
  83. if err != nil {
  84. if isOpsRealtimeRequestCanceled(c, err) {
  85. return
  86. }
  87. response.ErrorFrom(c, err)
  88. return
  89. }
  90. payload := gin.H{
  91. "enabled": true,
  92. "user": users,
  93. }
  94. if collectedAt != nil {
  95. payload["timestamp"] = collectedAt.UTC()
  96. }
  97. response.Success(c, payload)
  98. }
  99. // GetAccountAvailability returns account availability statistics.
  100. // GET /api/v1/admin/ops/account-availability
  101. //
  102. // Query params:
  103. // - platform: optional
  104. // - group_id: optional
  105. func (h *OpsHandler) GetAccountAvailability(c *gin.Context) {
  106. if h.opsService == nil {
  107. response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
  108. return
  109. }
  110. if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
  111. response.ErrorFrom(c, err)
  112. return
  113. }
  114. if !h.opsService.IsRealtimeMonitoringEnabled(c.Request.Context()) {
  115. response.Success(c, gin.H{
  116. "enabled": false,
  117. "platform": map[string]*service.PlatformAvailability{},
  118. "group": map[int64]*service.GroupAvailability{},
  119. "account": map[int64]*service.AccountAvailability{},
  120. "timestamp": time.Now().UTC(),
  121. })
  122. return
  123. }
  124. platform := strings.TrimSpace(c.Query("platform"))
  125. var groupID *int64
  126. if v := strings.TrimSpace(c.Query("group_id")); v != "" {
  127. id, err := strconv.ParseInt(v, 10, 64)
  128. if err != nil || id <= 0 {
  129. response.BadRequest(c, "Invalid group_id")
  130. return
  131. }
  132. groupID = &id
  133. }
  134. platformStats, groupStats, accountStats, collectedAt, err := h.opsService.GetAccountAvailabilityStats(c.Request.Context(), platform, groupID)
  135. if err != nil {
  136. if isOpsRealtimeRequestCanceled(c, err) {
  137. return
  138. }
  139. response.ErrorFrom(c, err)
  140. return
  141. }
  142. payload := gin.H{
  143. "enabled": true,
  144. "platform": platformStats,
  145. "group": groupStats,
  146. "account": accountStats,
  147. }
  148. if collectedAt != nil {
  149. payload["timestamp"] = collectedAt.UTC()
  150. }
  151. response.Success(c, payload)
  152. }
  153. func isOpsRealtimeRequestCanceled(c *gin.Context, err error) bool {
  154. if err == nil {
  155. return false
  156. }
  157. if errors.Is(err, context.Canceled) {
  158. return true
  159. }
  160. if c != nil && c.Request != nil && errors.Is(c.Request.Context().Err(), context.Canceled) {
  161. return true
  162. }
  163. return strings.Contains(err.Error(), "canceling statement due to user request")
  164. }
  165. func parseOpsRealtimeWindow(v string) (time.Duration, string, bool) {
  166. switch strings.ToLower(strings.TrimSpace(v)) {
  167. case "", "1min", "1m":
  168. return 1 * time.Minute, "1min", true
  169. case "5min", "5m":
  170. return 5 * time.Minute, "5min", true
  171. case "30min", "30m":
  172. return 30 * time.Minute, "30min", true
  173. case "1h", "60m", "60min":
  174. return 1 * time.Hour, "1h", true
  175. default:
  176. return 0, "", false
  177. }
  178. }
  179. // GetRealtimeTrafficSummary returns QPS/TPS current/peak/avg for the selected window.
  180. // GET /api/v1/admin/ops/realtime-traffic
  181. //
  182. // Query params:
  183. // - window: 1min|5min|30min|1h (default: 1min)
  184. // - platform: optional
  185. // - group_id: optional
  186. func (h *OpsHandler) GetRealtimeTrafficSummary(c *gin.Context) {
  187. if h.opsService == nil {
  188. response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
  189. return
  190. }
  191. if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
  192. response.ErrorFrom(c, err)
  193. return
  194. }
  195. windowDur, windowLabel, ok := parseOpsRealtimeWindow(c.Query("window"))
  196. if !ok {
  197. response.BadRequest(c, "Invalid window")
  198. return
  199. }
  200. platform := strings.TrimSpace(c.Query("platform"))
  201. var groupID *int64
  202. if v := strings.TrimSpace(c.Query("group_id")); v != "" {
  203. id, err := strconv.ParseInt(v, 10, 64)
  204. if err != nil || id <= 0 {
  205. response.BadRequest(c, "Invalid group_id")
  206. return
  207. }
  208. groupID = &id
  209. }
  210. endTime := time.Now().UTC()
  211. startTime := endTime.Add(-windowDur)
  212. if !h.opsService.IsRealtimeMonitoringEnabled(c.Request.Context()) {
  213. disabledSummary := &service.OpsRealtimeTrafficSummary{
  214. Window: windowLabel,
  215. StartTime: startTime,
  216. EndTime: endTime,
  217. Platform: platform,
  218. GroupID: groupID,
  219. QPS: service.OpsRateSummary{},
  220. TPS: service.OpsRateSummary{},
  221. }
  222. response.Success(c, gin.H{
  223. "enabled": false,
  224. "summary": disabledSummary,
  225. "timestamp": endTime,
  226. })
  227. return
  228. }
  229. filter := &service.OpsDashboardFilter{
  230. StartTime: startTime,
  231. EndTime: endTime,
  232. Platform: platform,
  233. GroupID: groupID,
  234. QueryMode: service.OpsQueryModeRaw,
  235. }
  236. summary, err := h.opsService.GetRealtimeTrafficSummary(c.Request.Context(), filter)
  237. if err != nil {
  238. if isOpsRealtimeRequestCanceled(c, err) {
  239. return
  240. }
  241. response.ErrorFrom(c, err)
  242. return
  243. }
  244. if summary != nil {
  245. summary.Window = windowLabel
  246. }
  247. response.Success(c, gin.H{
  248. "enabled": true,
  249. "summary": summary,
  250. "timestamp": endTime,
  251. })
  252. }