dashboard_snapshot_v2_handler.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. package admin
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
  12. "github.com/Wei-Shaw/sub2api/internal/service"
  13. "github.com/gin-gonic/gin"
  14. )
  15. var dashboardSnapshotV2Cache = newSnapshotCache(30 * time.Second)
  16. type dashboardSnapshotV2Stats struct {
  17. usagestats.DashboardStats
  18. Uptime int64 `json:"uptime"`
  19. }
  20. type dashboardSnapshotV2Response struct {
  21. GeneratedAt string `json:"generated_at"`
  22. StartDate string `json:"start_date"`
  23. EndDate string `json:"end_date"`
  24. Granularity string `json:"granularity"`
  25. Stats *dashboardSnapshotV2Stats `json:"stats,omitempty"`
  26. Trend []usagestats.TrendDataPoint `json:"trend,omitempty"`
  27. Models []usagestats.ModelStat `json:"models,omitempty"`
  28. Groups []usagestats.GroupStat `json:"groups,omitempty"`
  29. UsersTrend []usagestats.UserUsageTrendPoint `json:"users_trend,omitempty"`
  30. }
  31. type dashboardSnapshotV2Filters struct {
  32. UserID int64
  33. APIKeyID int64
  34. AccountID int64
  35. GroupID int64
  36. Model string
  37. RequestType *int16
  38. Stream *bool
  39. NativeCompactionV2 *bool
  40. BillingType *int8
  41. UpstreamModelMismatch *bool
  42. }
  43. type dashboardSnapshotV2CacheKey struct {
  44. StartTime string `json:"start_time"`
  45. EndTime string `json:"end_time"`
  46. Granularity string `json:"granularity"`
  47. UserID int64 `json:"user_id"`
  48. APIKeyID int64 `json:"api_key_id"`
  49. AccountID int64 `json:"account_id"`
  50. GroupID int64 `json:"group_id"`
  51. Model string `json:"model"`
  52. RequestType *int16 `json:"request_type"`
  53. Stream *bool `json:"stream"`
  54. NativeCompactionV2 *bool `json:"native_compaction_v2"`
  55. BillingType *int8 `json:"billing_type"`
  56. UpstreamModelMismatch *bool `json:"upstream_model_mismatch"`
  57. IncludeStats bool `json:"include_stats"`
  58. IncludeTrend bool `json:"include_trend"`
  59. IncludeModels bool `json:"include_models"`
  60. IncludeGroups bool `json:"include_groups"`
  61. IncludeUsersTrend bool `json:"include_users_trend"`
  62. UsersTrendLimit int `json:"users_trend_limit"`
  63. }
  64. func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
  65. startTime, endTime := parseTimeRange(c)
  66. granularity := strings.TrimSpace(c.DefaultQuery("granularity", "day"))
  67. if granularity != "hour" {
  68. granularity = "day"
  69. }
  70. includeStats := parseBoolQueryWithDefault(c.Query("include_stats"), true)
  71. includeTrend := parseBoolQueryWithDefault(c.Query("include_trend"), true)
  72. includeModels := parseBoolQueryWithDefault(c.Query("include_model_stats"), true)
  73. includeGroups := parseBoolQueryWithDefault(c.Query("include_group_stats"), false)
  74. includeUsersTrend := parseBoolQueryWithDefault(c.Query("include_users_trend"), false)
  75. usersTrendLimit := 12
  76. if raw := strings.TrimSpace(c.Query("users_trend_limit")); raw != "" {
  77. if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 50 {
  78. usersTrendLimit = parsed
  79. }
  80. }
  81. filters, err := parseDashboardSnapshotV2Filters(c)
  82. if err != nil {
  83. response.BadRequest(c, err.Error())
  84. return
  85. }
  86. keyRaw, _ := json.Marshal(dashboardSnapshotV2CacheKey{
  87. StartTime: startTime.UTC().Format(time.RFC3339),
  88. EndTime: endTime.UTC().Format(time.RFC3339),
  89. Granularity: granularity,
  90. UserID: filters.UserID,
  91. APIKeyID: filters.APIKeyID,
  92. AccountID: filters.AccountID,
  93. GroupID: filters.GroupID,
  94. Model: filters.Model,
  95. RequestType: filters.RequestType,
  96. Stream: filters.Stream,
  97. NativeCompactionV2: filters.NativeCompactionV2,
  98. BillingType: filters.BillingType,
  99. UpstreamModelMismatch: filters.UpstreamModelMismatch,
  100. IncludeStats: includeStats,
  101. IncludeTrend: includeTrend,
  102. IncludeModels: includeModels,
  103. IncludeGroups: includeGroups,
  104. IncludeUsersTrend: includeUsersTrend,
  105. UsersTrendLimit: usersTrendLimit,
  106. })
  107. cacheKey := string(keyRaw)
  108. cached, hit, err := dashboardSnapshotV2Cache.GetOrLoad(cacheKey, func() (any, error) {
  109. return h.buildSnapshotV2Response(
  110. c.Request.Context(),
  111. startTime,
  112. endTime,
  113. granularity,
  114. filters,
  115. includeStats,
  116. includeTrend,
  117. includeModels,
  118. includeGroups,
  119. includeUsersTrend,
  120. usersTrendLimit,
  121. )
  122. })
  123. if err != nil {
  124. response.Error(c, 500, err.Error())
  125. return
  126. }
  127. if cached.ETag != "" {
  128. c.Header("ETag", cached.ETag)
  129. c.Header("Vary", "If-None-Match")
  130. if ifNoneMatchMatched(c.GetHeader("If-None-Match"), cached.ETag) {
  131. c.Status(http.StatusNotModified)
  132. return
  133. }
  134. }
  135. c.Header("X-Snapshot-Cache", cacheStatusValue(hit))
  136. response.Success(c, cached.Payload)
  137. }
  138. func (h *DashboardHandler) buildSnapshotV2Response(
  139. ctx context.Context,
  140. startTime, endTime time.Time,
  141. granularity string,
  142. filters *dashboardSnapshotV2Filters,
  143. includeStats, includeTrend, includeModels, includeGroups, includeUsersTrend bool,
  144. usersTrendLimit int,
  145. ) (*dashboardSnapshotV2Response, error) {
  146. resp := &dashboardSnapshotV2Response{
  147. GeneratedAt: time.Now().UTC().Format(time.RFC3339),
  148. StartDate: startTime.Format("2006-01-02"),
  149. EndDate: endTime.Add(-24 * time.Hour).Format("2006-01-02"),
  150. Granularity: granularity,
  151. }
  152. if includeStats {
  153. stats, err := h.dashboardService.GetDashboardStats(ctx)
  154. if err != nil {
  155. return nil, errors.New("failed to get dashboard statistics")
  156. }
  157. resp.Stats = &dashboardSnapshotV2Stats{
  158. DashboardStats: *stats,
  159. Uptime: int64(time.Since(h.startTime).Seconds()),
  160. }
  161. }
  162. if includeTrend {
  163. trend, _, err := h.getUsageTrendCached(
  164. ctx,
  165. startTime,
  166. endTime,
  167. granularity,
  168. filters.UserID,
  169. filters.APIKeyID,
  170. filters.AccountID,
  171. filters.GroupID,
  172. filters.Model,
  173. filters.RequestType,
  174. filters.Stream,
  175. filters.NativeCompactionV2,
  176. filters.BillingType,
  177. filters.UpstreamModelMismatch,
  178. )
  179. if err != nil {
  180. return nil, errors.New("failed to get usage trend")
  181. }
  182. resp.Trend = trend
  183. }
  184. if includeModels {
  185. models, _, err := h.getModelStatsCached(
  186. ctx,
  187. startTime,
  188. endTime,
  189. filters.UserID,
  190. filters.APIKeyID,
  191. filters.AccountID,
  192. filters.GroupID,
  193. usagestats.ModelSourceRequested,
  194. filters.RequestType,
  195. filters.Stream,
  196. filters.NativeCompactionV2,
  197. filters.BillingType,
  198. filters.UpstreamModelMismatch,
  199. )
  200. if err != nil {
  201. return nil, errors.New("failed to get model statistics")
  202. }
  203. resp.Models = models
  204. }
  205. if includeGroups {
  206. groups, _, err := h.getGroupStatsCached(
  207. ctx,
  208. startTime,
  209. endTime,
  210. filters.UserID,
  211. filters.APIKeyID,
  212. filters.AccountID,
  213. filters.GroupID,
  214. filters.RequestType,
  215. filters.Stream,
  216. filters.NativeCompactionV2,
  217. filters.BillingType,
  218. filters.UpstreamModelMismatch,
  219. )
  220. if err != nil {
  221. return nil, errors.New("failed to get group statistics")
  222. }
  223. resp.Groups = groups
  224. }
  225. if includeUsersTrend {
  226. usersTrend, _, err := h.getUserUsageTrendCached(ctx, startTime, endTime, granularity, usersTrendLimit)
  227. if err != nil {
  228. return nil, errors.New("failed to get user usage trend")
  229. }
  230. resp.UsersTrend = usersTrend
  231. }
  232. return resp, nil
  233. }
  234. func parseDashboardSnapshotV2Filters(c *gin.Context) (*dashboardSnapshotV2Filters, error) {
  235. filters := &dashboardSnapshotV2Filters{
  236. Model: strings.TrimSpace(c.Query("model")),
  237. }
  238. if userIDStr := strings.TrimSpace(c.Query("user_id")); userIDStr != "" {
  239. id, err := strconv.ParseInt(userIDStr, 10, 64)
  240. if err != nil {
  241. return nil, err
  242. }
  243. filters.UserID = id
  244. }
  245. if apiKeyIDStr := strings.TrimSpace(c.Query("api_key_id")); apiKeyIDStr != "" {
  246. id, err := strconv.ParseInt(apiKeyIDStr, 10, 64)
  247. if err != nil {
  248. return nil, err
  249. }
  250. filters.APIKeyID = id
  251. }
  252. if accountIDStr := strings.TrimSpace(c.Query("account_id")); accountIDStr != "" {
  253. id, err := strconv.ParseInt(accountIDStr, 10, 64)
  254. if err != nil {
  255. return nil, err
  256. }
  257. filters.AccountID = id
  258. }
  259. if groupIDStr := strings.TrimSpace(c.Query("group_id")); groupIDStr != "" {
  260. id, err := strconv.ParseInt(groupIDStr, 10, 64)
  261. if err != nil {
  262. return nil, err
  263. }
  264. filters.GroupID = id
  265. }
  266. if requestTypeStr := strings.TrimSpace(c.Query("request_type")); requestTypeStr != "" {
  267. parsed, err := service.ParseUsageRequestType(requestTypeStr)
  268. if err != nil {
  269. return nil, err
  270. }
  271. value := int16(parsed)
  272. filters.RequestType = &value
  273. } else if streamStr := strings.TrimSpace(c.Query("stream")); streamStr != "" {
  274. streamVal, err := strconv.ParseBool(streamStr)
  275. if err != nil {
  276. return nil, err
  277. }
  278. filters.Stream = &streamVal
  279. }
  280. if nativeCompactionV2Str := strings.TrimSpace(c.Query("native_compaction_v2")); nativeCompactionV2Str != "" {
  281. value, err := strconv.ParseBool(nativeCompactionV2Str)
  282. if err != nil {
  283. return nil, err
  284. }
  285. filters.NativeCompactionV2 = &value
  286. }
  287. if billingTypeStr := strings.TrimSpace(c.Query("billing_type")); billingTypeStr != "" {
  288. v, err := strconv.ParseInt(billingTypeStr, 10, 8)
  289. if err != nil {
  290. return nil, err
  291. }
  292. bt := int8(v)
  293. filters.BillingType = &bt
  294. }
  295. if mismatchStr := strings.TrimSpace(c.Query("upstream_model_mismatch")); mismatchStr != "" {
  296. value, err := strconv.ParseBool(mismatchStr)
  297. if err != nil {
  298. return nil, err
  299. }
  300. filters.UpstreamModelMismatch = &value
  301. }
  302. return filters, nil
  303. }