feat(stats): resource metrics dashboard + sites logs/stats
Build / build (push) Successful in 10m50s
Build / build (push) Successful in 10m50s
Background collector samples CPU/memory/network/block I/O for every
instance and site on a configurable interval (default 15s, range
5-300s), persists samples to SQLite with a configurable retention
window (default 2h, range 0-24h), and skips ticks gracefully when
the Docker daemon is unreachable. Settings are reloadable without
a restart — each tick re-reads them.
New API endpoints:
- GET /api/system/stats (host snapshot: info + df)
- GET /api/system/stats/history
- GET /api/system/stats/top?by=cpu|memory
- GET /api/projects/{id}/stages/{s}/instances/{iid}/stats/history
- GET /api/sites/{id}/stats[/history]
- GET /api/sites/{id}/logs (SSE + JSON, reuses instance log streamer)
Frontend:
- ECharts added with tree-shaken imports (~180KB gzip) for
future-proof time-series/gantt/graph visualizations
- CollapsibleSection wraps all dashboard sections (system health,
daemons, system resources, static sites, projects) with
localStorage-persisted open state
- SystemResourcesCard shows capacity tiles, workload utilization
chart with 30m/2h/6h/24h window picker, disk breakdown with
reclaimable callouts, and top 5 consumers
- ContainerStats and ContainerLogs take a source discriminated union
so sites reuse the same components as instances; sites detail page
embeds both for Deno backend debugging
- Settings › Maintenance exposes collection interval + retention
- Docker-unavailable state returns 503 and renders an amber banner
instead of a generic 500
Full i18n coverage (en + ru) for all new strings.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
// SystemStats is a host-level snapshot combining daemon capacity
|
||||
// (NCPU, memory total) with container counts and disk usage broken down
|
||||
// by category. Workload CPU/memory utilization is aggregated from
|
||||
// per-container samples by the stats collector, not here.
|
||||
type SystemStats struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
|
||||
// Capacity from Docker daemon.
|
||||
NCPU int `json:"ncpu"`
|
||||
MemoryTotal int64 `json:"memory_total"`
|
||||
|
||||
// Container/image counts.
|
||||
Containers int `json:"containers"`
|
||||
Running int `json:"running"`
|
||||
Paused int `json:"paused"`
|
||||
Stopped int `json:"stopped"`
|
||||
Images int `json:"images"`
|
||||
|
||||
// Disk usage by category (bytes).
|
||||
DiskImagesBytes int64 `json:"disk_images_bytes"`
|
||||
DiskContainersBytes int64 `json:"disk_containers_bytes"`
|
||||
DiskVolumesBytes int64 `json:"disk_volumes_bytes"`
|
||||
DiskBuildCacheBytes int64 `json:"disk_build_cache_bytes"`
|
||||
|
||||
// Reclaimable disk space by category (bytes).
|
||||
DiskImagesReclaimable int64 `json:"disk_images_reclaimable"`
|
||||
DiskContainersReclaimable int64 `json:"disk_containers_reclaimable"`
|
||||
DiskVolumesReclaimable int64 `json:"disk_volumes_reclaimable"`
|
||||
DiskBuildCacheReclaimable int64 `json:"disk_build_cache_reclaimable"`
|
||||
|
||||
// DiskTotalBytes is the sum of the category totals.
|
||||
DiskTotalBytes int64 `json:"disk_total_bytes"`
|
||||
}
|
||||
|
||||
// GetSystemStats returns a one-shot host-level snapshot. The Info() call
|
||||
// and disk usage call are made in sequence. Disk usage failures do not
|
||||
// fail the whole call — the result degrades gracefully with zero disk fields.
|
||||
func (c *Client) GetSystemStats(ctx context.Context) (SystemStats, error) {
|
||||
info, err := c.Info(ctx)
|
||||
if err != nil {
|
||||
return SystemStats{}, fmt.Errorf("system stats: %w", err)
|
||||
}
|
||||
|
||||
stats := SystemStats{
|
||||
Timestamp: time.Now().UTC(),
|
||||
NCPU: info.NCPU,
|
||||
MemoryTotal: info.MemoryTotal,
|
||||
Containers: info.Containers,
|
||||
Running: info.Running,
|
||||
Paused: info.Paused,
|
||||
Stopped: info.Stopped,
|
||||
Images: info.Images,
|
||||
}
|
||||
|
||||
du, derr := c.api.DiskUsage(ctx, client.DiskUsageOptions{
|
||||
Containers: true,
|
||||
Images: true,
|
||||
Volumes: true,
|
||||
BuildCache: true,
|
||||
})
|
||||
if derr == nil {
|
||||
stats.DiskImagesBytes = du.Images.TotalSize
|
||||
stats.DiskContainersBytes = du.Containers.TotalSize
|
||||
stats.DiskVolumesBytes = du.Volumes.TotalSize
|
||||
stats.DiskBuildCacheBytes = du.BuildCache.TotalSize
|
||||
stats.DiskImagesReclaimable = du.Images.Reclaimable
|
||||
stats.DiskContainersReclaimable = du.Containers.Reclaimable
|
||||
stats.DiskVolumesReclaimable = du.Volumes.Reclaimable
|
||||
stats.DiskBuildCacheReclaimable = du.BuildCache.Reclaimable
|
||||
stats.DiskTotalBytes = stats.DiskImagesBytes +
|
||||
stats.DiskContainersBytes +
|
||||
stats.DiskVolumesBytes +
|
||||
stats.DiskBuildCacheBytes
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
Reference in New Issue
Block a user