Skip to content

Commit ec09e48

Browse files
authored
Merge pull request #67 from coderbuzz/feat/stats-refinement-avg-max
feat: refine app list stats with 15m CPU moving average and 1h Peak RAM
2 parents e3f852a + ae3fb1e commit ec09e48

2 files changed

Lines changed: 97 additions & 11 deletions

File tree

‎internal/app/repository.go‎

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -627,19 +627,65 @@ func (r *Repository) LatestAggregatedStats(appID int64) (*ContainerStats, error)
627627
return s, nil
628628
}
629629

630-
// LatestStatsByApp returns the latest aggregated container stats per app
631-
// (summed across all of an app's containers at the most recent collection tick),
632-
// keyed by app ID. Apps with no collected stats are absent. Two queries total.
630+
// LatestStatsByApp returns refined per-app resource metrics for the app list view:
631+
// - CPU: 15-minute moving average (AVG of total container CPU per tick)
632+
// - Memory: 1-hour peak usage (MAX of total container RAM per tick)
633+
// - Network / Block IO: Latest snapshot
634+
// Falls back to latest available tick if no data exists within the time window.
633635
func (r *Repository) LatestStatsByApp() (map[int64]*ContainerStats, error) {
634636
rows, err := r.db.Query(`
635-
SELECT cs.app_id,
636-
SUM(cs.cpu_percent), SUM(cs.mem_usage_bytes), SUM(cs.mem_limit_bytes), SUM(cs.mem_percent),
637-
SUM(cs.net_io_rx_bytes), SUM(cs.net_io_tx_bytes), SUM(cs.block_io_read), SUM(cs.block_io_write)
638-
FROM container_stats cs
639-
JOIN (
640-
SELECT app_id, MAX(created_at) AS max_ts FROM container_stats GROUP BY app_id
641-
) m ON m.app_id = cs.app_id AND m.max_ts = cs.created_at
642-
GROUP BY cs.app_id
637+
WITH cs_15m AS (
638+
SELECT app_id, AVG(tick_cpu) AS avg_cpu
639+
FROM (
640+
SELECT app_id, created_at, SUM(cpu_percent) AS tick_cpu
641+
FROM container_stats
642+
WHERE created_at >= DATETIME('now', '-15 minutes')
643+
GROUP BY app_id, created_at
644+
)
645+
GROUP BY app_id
646+
),
647+
cs_60m AS (
648+
SELECT app_id,
649+
MAX(tick_mem_bytes) AS max_mem_bytes,
650+
MAX(tick_mem_limit) AS max_mem_limit,
651+
MAX(tick_mem_pct) AS max_mem_pct
652+
FROM (
653+
SELECT app_id, created_at,
654+
SUM(mem_usage_bytes) AS tick_mem_bytes,
655+
SUM(mem_limit_bytes) AS tick_mem_limit,
656+
SUM(mem_percent) AS tick_mem_pct
657+
FROM container_stats
658+
WHERE created_at >= DATETIME('now', '-1 hour')
659+
GROUP BY app_id, created_at
660+
)
661+
GROUP BY app_id
662+
),
663+
cs_latest AS (
664+
SELECT cs.app_id,
665+
SUM(cs.cpu_percent) AS fallback_cpu,
666+
SUM(cs.mem_usage_bytes) AS fallback_mem_bytes,
667+
SUM(cs.mem_limit_bytes) AS fallback_mem_limit,
668+
SUM(cs.mem_percent) AS fallback_mem_pct,
669+
SUM(cs.net_io_rx_bytes) AS latest_rx,
670+
SUM(cs.net_io_tx_bytes) AS latest_tx,
671+
SUM(cs.block_io_read) AS latest_blk_r,
672+
SUM(cs.block_io_write) AS latest_blk_w
673+
FROM container_stats cs
674+
JOIN (
675+
SELECT app_id, MAX(created_at) AS max_ts FROM container_stats GROUP BY app_id
676+
) m ON m.app_id = cs.app_id AND m.max_ts = cs.created_at
677+
GROUP BY cs.app_id
678+
)
679+
SELECT
680+
l.app_id,
681+
COALESCE(c15.avg_cpu, l.fallback_cpu) AS cpu_percent,
682+
COALESCE(c60.max_mem_bytes, l.fallback_mem_bytes) AS mem_usage_bytes,
683+
COALESCE(c60.max_mem_limit, l.fallback_mem_limit) AS mem_limit_bytes,
684+
COALESCE(c60.max_mem_pct, l.fallback_mem_pct) AS mem_percent,
685+
l.latest_rx, l.latest_tx, l.latest_blk_r, l.latest_blk_w
686+
FROM cs_latest l
687+
LEFT JOIN cs_15m c15 ON c15.app_id = l.app_id
688+
LEFT JOIN cs_60m c60 ON c60.app_id = l.app_id
643689
`)
644690
if err != nil {
645691
return nil, err

‎internal/app/repository_test.go‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,43 @@ func TestRepo_LatestBatchedStats(t *testing.T) {
224224
t.Errorf("expected no disk entry for a2")
225225
}
226226
}
227+
228+
func TestRepo_LatestBatchedStats_RefinementAvgAndMax(t *testing.T) {
229+
repo, srvRepo := setupRepo(t)
230+
srvID := createTestServer(t, srvRepo)
231+
232+
a := &App{Name: "avg-max-app", Domain: "avgmax.com", Port: 80, ServerID: srvID, Compose: "x"}
233+
repo.Create(a)
234+
235+
// Tick 1 (5 minutes ago): CPU 10%, Mem 100MB
236+
repo.InsertContainerStats(&ContainerStats{
237+
AppID: a.ID, ServerID: srvID, ContainerName: "web",
238+
CPUPercent: 10.0, MemUsageBytes: 100, MemLimitBytes: 1000, MemPercent: 10.0,
239+
})
240+
repo.db.Exec(`UPDATE container_stats SET created_at = DATETIME('now', '-5 minutes') WHERE app_id = ?`, a.ID)
241+
242+
// Tick 2 (now): CPU 30%, Mem 500MB
243+
repo.InsertContainerStats(&ContainerStats{
244+
AppID: a.ID, ServerID: srvID, ContainerName: "web",
245+
CPUPercent: 30.0, MemUsageBytes: 500, MemLimitBytes: 1000, MemPercent: 50.0,
246+
})
247+
248+
statsMap, err := repo.LatestStatsByApp()
249+
if err != nil {
250+
t.Fatalf("LatestStatsByApp failed: %v", err)
251+
}
252+
st, ok := statsMap[a.ID]
253+
if !ok {
254+
t.Fatalf("expected stats for app %d", a.ID)
255+
}
256+
257+
// 15m CPU moving average: (10 + 30) / 2 = 20.0%
258+
if st.CPUPercent != 20.0 {
259+
t.Errorf("expected 15m CPU AVG 20.0, got %f", st.CPUPercent)
260+
}
261+
262+
// 1h Peak Memory: MAX(100, 500) = 500
263+
if st.MemUsageBytes != 500 {
264+
t.Errorf("expected 1h Peak Memory 500, got %d", st.MemUsageBytes)
265+
}
266+
}

0 commit comments

Comments
 (0)