diff --git a/internal/api/handlers_metrics.go b/internal/api/handlers_metrics.go index 1efbafa..a2ab0b9 100644 --- a/internal/api/handlers_metrics.go +++ b/internal/api/handlers_metrics.go @@ -3,6 +3,7 @@ package api import ( "context" "net/http" + "time" "connectrpc.com/connect" "github.com/go-chi/chi/v5" @@ -45,8 +46,9 @@ func (h *sandboxMetricsHandler) GetMetrics(w http.ResponseWriter, r *http.Reques if rangeTier == "" { rangeTier = "10m" } - if rangeTier != "10m" && rangeTier != "2h" && rangeTier != "24h" { - writeError(w, http.StatusBadRequest, "invalid_request", "range must be 10m, 2h, or 24h") + validRanges := map[string]bool{"5m": true, "10m": true, "1h": true, "2h": true, "6h": true, "12h": true, "24h": true} + if !validRanges[rangeTier] { + writeError(w, http.StatusBadRequest, "invalid_request", "range must be one of: 5m, 10m, 1h, 2h, 6h, 12h, 24h") return } @@ -102,23 +104,41 @@ func (h *sandboxMetricsHandler) getFromAgent(w http.ResponseWriter, r *http.Requ }) } +// rangeToDB maps a user-facing range filter to the DB tier and cutoff duration. +var rangeToDB = map[string]struct { + tier string + cutoff time.Duration +}{ + "5m": {"10m", 5 * time.Minute}, + "10m": {"10m", 10 * time.Minute}, + "1h": {"2h", 1 * time.Hour}, + "2h": {"2h", 2 * time.Hour}, + "6h": {"24h", 6 * time.Hour}, + "12h": {"24h", 12 * time.Hour}, + "24h": {"24h", 24 * time.Hour}, +} + func (h *sandboxMetricsHandler) getFromDB(ctx context.Context, w http.ResponseWriter, sandboxID, rangeTier string) { + mapping := rangeToDB[rangeTier] rows, err := h.db.GetSandboxMetricPoints(ctx, db.GetSandboxMetricPointsParams{ SandboxID: sandboxID, - Tier: rangeTier, + Tier: mapping.tier, }) if err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "failed to read metrics") return } - points := make([]metricPointResponse, len(rows)) - for i, row := range rows { - points[i] = metricPointResponse{ - TimestampUnix: row.Ts, - CPUPct: row.CpuPct, - MemBytes: row.MemBytes, - DiskBytes: row.DiskBytes, + threshold := time.Now().Add(-mapping.cutoff).Unix() + var points []metricPointResponse + for _, row := range rows { + if row.Ts >= threshold { + points = append(points, metricPointResponse{ + TimestampUnix: row.Ts, + CPUPct: row.CpuPct, + MemBytes: row.MemBytes, + DiskBytes: row.DiskBytes, + }) } } diff --git a/internal/api/openapi.yaml b/internal/api/openapi.yaml index 2b627f1..f5ae7a7 100644 --- a/internal/api/openapi.yaml +++ b/internal/api/openapi.yaml @@ -782,9 +782,9 @@ paths: required: false schema: type: string - enum: ["10m", "2h", "24h"] + enum: ["5m", "10m", "1h", "2h", "6h", "12h", "24h"] default: "10m" - description: Time range tier to query + description: Time range filter to query responses: "200": description: Metrics retrieved @@ -2042,7 +2042,7 @@ components: type: string range: type: string - enum: ["10m", "2h", "24h"] + enum: ["5m", "10m", "1h", "2h", "6h", "12h", "24h"] points: type: array items: diff --git a/internal/sandbox/manager.go b/internal/sandbox/manager.go index 87a3289..2da0944 100644 --- a/internal/sandbox/manager.go +++ b/internal/sandbox/manager.go @@ -1348,16 +1348,44 @@ func (m *Manager) GetMetrics(sandboxID, rangeTier string) ([]MetricPoint, error) return nil, nil } + // Map the requested range to the appropriate ring tier and time cutoff. + var points []MetricPoint + var cutoff time.Duration switch rangeTier { + case "5m": + points = sb.ring.Get10m() + cutoff = 5 * time.Minute case "10m": - return sb.ring.Get10m(), nil + points = sb.ring.Get10m() + cutoff = 10 * time.Minute + case "1h": + points = sb.ring.Get2h() + cutoff = 1 * time.Hour case "2h": - return sb.ring.Get2h(), nil + points = sb.ring.Get2h() + cutoff = 2 * time.Hour + case "6h": + points = sb.ring.Get24h() + cutoff = 6 * time.Hour + case "12h": + points = sb.ring.Get24h() + cutoff = 12 * time.Hour case "24h": - return sb.ring.Get24h(), nil + points = sb.ring.Get24h() + cutoff = 24 * time.Hour default: - return nil, fmt.Errorf("invalid range: %s (valid: 10m, 2h, 24h)", rangeTier) + return nil, fmt.Errorf("invalid range: %s (valid: 5m, 10m, 1h, 2h, 6h, 12h, 24h)", rangeTier) } + + // Filter points to the requested time window. + threshold := time.Now().Add(-cutoff) + filtered := points[:0:0] + for _, p := range points { + if !p.Timestamp.Before(threshold) { + filtered = append(filtered, p) + } + } + return filtered, nil } // FlushMetrics returns all three tier ring buffers, clears the ring, and