| package admin |
|
|
| import ( |
| "encoding/json" |
| "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" |
| "golang.org/x/sync/errgroup" |
| ) |
|
|
| var opsDashboardSnapshotV2Cache = newSnapshotCache(30 * time.Second) |
|
|
| type opsDashboardSnapshotV2Response struct { |
| GeneratedAt string `json:"generated_at"` |
|
|
| Overview *service.OpsDashboardOverview `json:"overview"` |
| ThroughputTrend *service.OpsThroughputTrendResponse `json:"throughput_trend"` |
| ErrorTrend *service.OpsErrorTrendResponse `json:"error_trend"` |
| } |
|
|
| type opsDashboardSnapshotV2CacheKey struct { |
| StartTime string `json:"start_time"` |
| EndTime string `json:"end_time"` |
| Platform string `json:"platform"` |
| GroupID *int64 `json:"group_id"` |
| QueryMode service.OpsQueryMode `json:"mode"` |
| BucketSecond int `json:"bucket_second"` |
| } |
|
|
| |
| |
| func (h *OpsHandler) GetDashboardSnapshotV2(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)) |
|
|
| keyRaw, _ := json.Marshal(opsDashboardSnapshotV2CacheKey{ |
| StartTime: startTime.UTC().Format(time.RFC3339), |
| EndTime: endTime.UTC().Format(time.RFC3339), |
| Platform: filter.Platform, |
| GroupID: filter.GroupID, |
| QueryMode: filter.QueryMode, |
| BucketSecond: bucketSeconds, |
| }) |
| cacheKey := string(keyRaw) |
|
|
| if cached, ok := opsDashboardSnapshotV2Cache.Get(cacheKey); ok { |
| if cached.ETag != "" { |
| c.Header("ETag", cached.ETag) |
| c.Header("Vary", "If-None-Match") |
| if ifNoneMatchMatched(c.GetHeader("If-None-Match"), cached.ETag) { |
| c.Status(http.StatusNotModified) |
| return |
| } |
| } |
| c.Header("X-Snapshot-Cache", "hit") |
| response.Success(c, cached.Payload) |
| return |
| } |
|
|
| var ( |
| overview *service.OpsDashboardOverview |
| trend *service.OpsThroughputTrendResponse |
| errTrend *service.OpsErrorTrendResponse |
| ) |
| g, gctx := errgroup.WithContext(c.Request.Context()) |
| g.Go(func() error { |
| f := *filter |
| result, err := h.opsService.GetDashboardOverview(gctx, &f) |
| if err != nil { |
| return err |
| } |
| overview = result |
| return nil |
| }) |
| g.Go(func() error { |
| f := *filter |
| result, err := h.opsService.GetThroughputTrend(gctx, &f, bucketSeconds) |
| if err != nil { |
| return err |
| } |
| trend = result |
| return nil |
| }) |
| g.Go(func() error { |
| f := *filter |
| result, err := h.opsService.GetErrorTrend(gctx, &f, bucketSeconds) |
| if err != nil { |
| return err |
| } |
| errTrend = result |
| return nil |
| }) |
| if err := g.Wait(); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| resp := &opsDashboardSnapshotV2Response{ |
| GeneratedAt: time.Now().UTC().Format(time.RFC3339), |
| Overview: overview, |
| ThroughputTrend: trend, |
| ErrorTrend: errTrend, |
| } |
|
|
| cached := opsDashboardSnapshotV2Cache.Set(cacheKey, resp) |
| if cached.ETag != "" { |
| c.Header("ETag", cached.ETag) |
| c.Header("Vary", "If-None-Match") |
| } |
| c.Header("X-Snapshot-Cache", "miss") |
| response.Success(c, resp) |
| } |
|
|