forked from wrenn/wrenn
Add daily usage metrics (CPU-minutes, RAM GB-minutes)
Introduce pre-computed daily usage rollups from sandbox_metrics_snapshots. An hourly background worker aggregates completed days, while today's usage is computed live from snapshots at query time for freshness. Backend: new daily_usage table, rollup worker, UsageService, and GET /v1/capsules/usage endpoint with date range filtering (up to 92 days). Frontend: replace Usage page placeholder with bar charts (Chart.js), summary total cards, and preset/custom date range controls.
This commit is contained in:
@ -210,6 +210,10 @@ func Run(opts ...Option) {
|
||||
sampler := api.NewMetricsSampler(queries, 10*time.Second)
|
||||
sampler.Start(ctx)
|
||||
|
||||
// Start daily usage rollup (pre-computes CPU-minutes and RAM-MB-minutes).
|
||||
rollup := api.NewDailyUsageRollup(queries, time.Hour)
|
||||
rollup.Start(ctx)
|
||||
|
||||
// Start extension background workers.
|
||||
for _, ext := range o.extensions {
|
||||
for _, worker := range ext.BackgroundWorkers(sctx) {
|
||||
|
||||
@ -11,6 +11,43 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const computeDailyUsageForDay = `-- name: ComputeDailyUsageForDay :one
|
||||
SELECT
|
||||
COALESCE(SUM(vcpus_reserved * 10.0 / 60.0), 0)::NUMERIC(18,4) AS cpu_minutes,
|
||||
COALESCE(SUM(memory_mb_reserved * 10.0 / 60.0), 0)::NUMERIC(18,4) AS ram_mb_minutes
|
||||
FROM sandbox_metrics_snapshots
|
||||
WHERE team_id = $1
|
||||
AND sampled_at >= $2
|
||||
AND sampled_at < $3
|
||||
`
|
||||
|
||||
type ComputeDailyUsageForDayParams struct {
|
||||
TeamID pgtype.UUID `json:"team_id"`
|
||||
SampledAt pgtype.Timestamptz `json:"sampled_at"`
|
||||
SampledAt_2 pgtype.Timestamptz `json:"sampled_at_2"`
|
||||
}
|
||||
|
||||
type ComputeDailyUsageForDayRow struct {
|
||||
CpuMinutes pgtype.Numeric `json:"cpu_minutes"`
|
||||
RamMbMinutes pgtype.Numeric `json:"ram_mb_minutes"`
|
||||
}
|
||||
|
||||
func (q *Queries) ComputeDailyUsageForDay(ctx context.Context, arg ComputeDailyUsageForDayParams) (ComputeDailyUsageForDayRow, error) {
|
||||
row := q.db.QueryRow(ctx, computeDailyUsageForDay, arg.TeamID, arg.SampledAt, arg.SampledAt_2)
|
||||
var i ComputeDailyUsageForDayRow
|
||||
err := row.Scan(&i.CpuMinutes, &i.RamMbMinutes)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteDailyUsageByTeam = `-- name: DeleteDailyUsageByTeam :exec
|
||||
DELETE FROM daily_usage WHERE team_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteDailyUsageByTeam(ctx context.Context, teamID pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteDailyUsageByTeam, teamID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteMetricPointsByTeam = `-- name: DeleteMetricPointsByTeam :exec
|
||||
DELETE FROM sandbox_metric_points
|
||||
WHERE sandbox_id IN (SELECT id FROM sandboxes WHERE team_id = $1)
|
||||
@ -55,6 +92,47 @@ func (q *Queries) DeleteSandboxMetricPointsByTier(ctx context.Context, arg Delet
|
||||
return err
|
||||
}
|
||||
|
||||
const getDailyUsage = `-- name: GetDailyUsage :many
|
||||
SELECT day, cpu_minutes, ram_mb_minutes
|
||||
FROM daily_usage
|
||||
WHERE team_id = $1
|
||||
AND day >= $2
|
||||
AND day <= $3
|
||||
ORDER BY day ASC
|
||||
`
|
||||
|
||||
type GetDailyUsageParams struct {
|
||||
TeamID pgtype.UUID `json:"team_id"`
|
||||
Day pgtype.Date `json:"day"`
|
||||
Day_2 pgtype.Date `json:"day_2"`
|
||||
}
|
||||
|
||||
type GetDailyUsageRow struct {
|
||||
Day pgtype.Date `json:"day"`
|
||||
CpuMinutes pgtype.Numeric `json:"cpu_minutes"`
|
||||
RamMbMinutes pgtype.Numeric `json:"ram_mb_minutes"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetDailyUsage(ctx context.Context, arg GetDailyUsageParams) ([]GetDailyUsageRow, error) {
|
||||
rows, err := q.db.Query(ctx, getDailyUsage, arg.TeamID, arg.Day, arg.Day_2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetDailyUsageRow
|
||||
for rows.Next() {
|
||||
var i GetDailyUsageRow
|
||||
if err := rows.Scan(&i.Day, &i.CpuMinutes, &i.RamMbMinutes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getLiveMetrics = `-- name: GetLiveMetrics :one
|
||||
SELECT
|
||||
(COUNT(*) FILTER (WHERE status IN ('running', 'starting')))::INTEGER AS running_count,
|
||||
@ -149,6 +227,32 @@ func (q *Queries) GetSandboxMetricPoints(ctx context.Context, arg GetSandboxMetr
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getTeamsWithSnapshots = `-- name: GetTeamsWithSnapshots :many
|
||||
SELECT DISTINCT team_id
|
||||
FROM sandbox_metrics_snapshots
|
||||
WHERE sampled_at > NOW() - INTERVAL '93 days'
|
||||
`
|
||||
|
||||
func (q *Queries) GetTeamsWithSnapshots(ctx context.Context) ([]pgtype.UUID, error) {
|
||||
rows, err := q.db.Query(ctx, getTeamsWithSnapshots)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []pgtype.UUID
|
||||
for rows.Next() {
|
||||
var team_id pgtype.UUID
|
||||
if err := rows.Scan(&team_id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, team_id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertMetricsSnapshot = `-- name: InsertMetricsSnapshot :exec
|
||||
INSERT INTO sandbox_metrics_snapshots (team_id, running_count, vcpus_reserved, memory_mb_reserved)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
@ -267,3 +371,28 @@ func (q *Queries) SampleSandboxMetrics(ctx context.Context) ([]SampleSandboxMetr
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const upsertDailyUsage = `-- name: UpsertDailyUsage :exec
|
||||
INSERT INTO daily_usage (team_id, day, cpu_minutes, ram_mb_minutes)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (team_id, day) DO UPDATE
|
||||
SET cpu_minutes = EXCLUDED.cpu_minutes,
|
||||
ram_mb_minutes = EXCLUDED.ram_mb_minutes
|
||||
`
|
||||
|
||||
type UpsertDailyUsageParams struct {
|
||||
TeamID pgtype.UUID `json:"team_id"`
|
||||
Day pgtype.Date `json:"day"`
|
||||
CpuMinutes pgtype.Numeric `json:"cpu_minutes"`
|
||||
RamMbMinutes pgtype.Numeric `json:"ram_mb_minutes"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertDailyUsage(ctx context.Context, arg UpsertDailyUsageParams) error {
|
||||
_, err := q.db.Exec(ctx, upsertDailyUsage,
|
||||
arg.TeamID,
|
||||
arg.Day,
|
||||
arg.CpuMinutes,
|
||||
arg.RamMbMinutes,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -41,6 +41,13 @@ type Channel struct {
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type DailyUsage struct {
|
||||
TeamID pgtype.UUID `json:"team_id"`
|
||||
Day pgtype.Date `json:"day"`
|
||||
CpuMinutes pgtype.Numeric `json:"cpu_minutes"`
|
||||
RamMbMinutes pgtype.Numeric `json:"ram_mb_minutes"`
|
||||
}
|
||||
|
||||
type Host struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Type string `json:"type"`
|
||||
|
||||
@ -158,3 +158,91 @@ func (s *StatsService) queryTimeSeries(ctx context.Context, teamID pgtype.UUID,
|
||||
}
|
||||
return points, rows.Err()
|
||||
}
|
||||
|
||||
// UsagePoint is one daily usage data point.
|
||||
type UsagePoint struct {
|
||||
Day time.Time
|
||||
CPUMinutes float64
|
||||
RAMMBMinutes float64
|
||||
}
|
||||
|
||||
// UsageService queries pre-computed daily usage rollups. For the current
|
||||
// day it computes usage live from sandbox_metrics_snapshots so the value
|
||||
// is always up-to-date rather than stale until the next hourly rollup.
|
||||
type UsageService struct {
|
||||
DB *db.Queries
|
||||
}
|
||||
|
||||
// GetUsage returns daily CPU-minute and RAM-MB-minute totals for a team
|
||||
// within the given date range (inclusive). Past days come from the
|
||||
// pre-computed daily_usage table; today is computed live from snapshots.
|
||||
func (s *UsageService) GetUsage(ctx context.Context, teamID pgtype.UUID, from, to time.Time) ([]UsagePoint, error) {
|
||||
now := time.Now().UTC()
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// Clamp the pre-computed query to exclude today (it hasn't been rolled up).
|
||||
precomputedTo := to
|
||||
if !to.Before(today) {
|
||||
precomputedTo = today.AddDate(0, 0, -1)
|
||||
}
|
||||
|
||||
var points []UsagePoint
|
||||
|
||||
// Fetch pre-computed days (from..min(to, yesterday)).
|
||||
if !from.After(precomputedTo) {
|
||||
rows, err := s.DB.GetDailyUsage(ctx, db.GetDailyUsageParams{
|
||||
TeamID: teamID,
|
||||
Day: pgtype.Date{Time: from, Valid: true},
|
||||
Day_2: pgtype.Date{Time: precomputedTo, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get daily usage: %w", err)
|
||||
}
|
||||
|
||||
points = make([]UsagePoint, 0, len(rows)+1)
|
||||
for _, r := range rows {
|
||||
cpu, err := r.CpuMinutes.Float64Value()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert cpu_minutes: %w", err)
|
||||
}
|
||||
ram, err := r.RamMbMinutes.Float64Value()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert ram_mb_minutes: %w", err)
|
||||
}
|
||||
points = append(points, UsagePoint{
|
||||
Day: r.Day.Time,
|
||||
CPUMinutes: cpu.Float64,
|
||||
RAMMBMinutes: ram.Float64,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Compute today live from snapshots if the range includes today.
|
||||
if !to.Before(today) && !from.After(today) {
|
||||
todayEnd := today.Add(24 * time.Hour)
|
||||
row, err := s.DB.ComputeDailyUsageForDay(ctx, db.ComputeDailyUsageForDayParams{
|
||||
TeamID: teamID,
|
||||
SampledAt: pgtype.Timestamptz{Time: today, Valid: true},
|
||||
SampledAt_2: pgtype.Timestamptz{Time: todayEnd, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("compute today usage: %w", err)
|
||||
}
|
||||
|
||||
cpu, err := row.CpuMinutes.Float64Value()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert today cpu_minutes: %w", err)
|
||||
}
|
||||
ram, err := row.RamMbMinutes.Float64Value()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert today ram_mb_minutes: %w", err)
|
||||
}
|
||||
points = append(points, UsagePoint{
|
||||
Day: today,
|
||||
CPUMinutes: cpu.Float64,
|
||||
RAMMBMinutes: ram.Float64,
|
||||
})
|
||||
}
|
||||
|
||||
return points, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user