diff --git a/docs/http-api.md b/docs/http-api.md index eda8036..f0c168f 100644 --- a/docs/http-api.md +++ b/docs/http-api.md @@ -180,6 +180,48 @@ curl -s http://127.0.0.1:9615/api/v1/processes An empty app set returns `[]` (never `null`). +#### System resources usage and availability option + +To include host system resources usage and availability alongside the process list without breaking the default array format, pass a query parameter such as `?system=true` (or `?system_resources=true`, `?stats=true`, `?format=object`, `?include=system`): + +```sh +curl -s "http://127.0.0.1:9615/api/v1/processes?system=true" +``` + +```json +{ + "processes": [ + { + "pid": 7731, + "name": "web-0", + "pm_id": 0, + "monit": { "memory": 48896000, "cpu": 0.4 }, + "pm2_env": { "name": "web-0", "status": "online", "pm_id": 0, "...": "..." } + } + ], + "system": { + "cpu_cores": 8, + "load_1": 0.15, + "load_5": 0.20, + "load_15": 0.18, + "total_memory": 17179869184, + "used_memory": 8589934592, + "available_memory": 8589934592, + "memory_used_pct": 50.0 + }, + "system_resources": { + "cpu_cores": 8, + "load_1": 0.15, + "load_5": 0.20, + "load_15": 0.18, + "total_memory": 17179869184, + "used_memory": 8589934592, + "available_memory": 8589934592, + "memory_used_pct": 50.0 + } +} +``` + ### 5.3 `GET /api/v1/processes/{target}` Describe one app in full (same object shape as one element of the list). On an diff --git a/internal/daemon/http.go b/internal/daemon/http.go index 1b24f17..2f74c56 100644 --- a/internal/daemon/http.go +++ b/internal/daemon/http.go @@ -167,7 +167,10 @@ func (s *Server) httpHealth(w http.ResponseWriter, r *http.Request) { } // httpList returns every app as a PM2-compatible jlist array (the same JSON -// `pm0 jlist` prints, via the shared renderer). +// `pm0 jlist` prints, via the shared renderer). If requested via query param +// (e.g. ?system=true, ?system_resources=true, ?stats=true, ?format=object, +// or ?include=system), it returns an object containing the process list and +// system resource usage and availability metrics. func (s *Server) httpList(w http.ResponseWriter, r *http.Request) { resp, err := s.ListProcesses(r.Context(), &v1.ListProcessesRequest{}) if err != nil { @@ -179,9 +182,39 @@ func (s *Server) httpList(w http.ResponseWriter, r *http.Request) { writeErr(w, err) return } + + if wantsSystemResources(r) { + sys := client.ReadSystemStats() + sysObj := map[string]any{ + "cpu_cores": sys.CPUCores, + "load_1": sys.Load1, + "load_5": sys.Load5, + "load_15": sys.Load15, + "total_memory": sys.TotalMem, + "used_memory": sys.UsedMem, + "available_memory": sys.AvailMem, + "memory_used_pct": sys.MemUsedPct, + } + writeJSON(w, http.StatusOK, map[string]any{ + "processes": json.RawMessage(b), + "system": sysObj, + "system_resources": sysObj, + }) + return + } + writeRawJSON(w, http.StatusOK, b) } +func wantsSystemResources(r *http.Request) bool { + q := r.URL.Query() + return queryBool(q.Get("system"), false) || + queryBool(q.Get("system_resources"), false) || + queryBool(q.Get("stats"), false) || + strings.EqualFold(q.Get("format"), "object") || + strings.Contains(q.Get("include"), "system") +} + func (s *Server) httpDescribe(w http.ResponseWriter, r *http.Request) { sel := selectorFromTarget(r.PathValue("target")) resp, err := s.DescribeProcess(r.Context(), &v1.DescribeProcessRequest{Selector: sel}) diff --git a/internal/daemon/http_test.go b/internal/daemon/http_test.go index d3fa805..56a5ec5 100644 --- a/internal/daemon/http_test.go +++ b/internal/daemon/http_test.go @@ -182,6 +182,34 @@ func TestHTTPProcessesEmptyIsArray(t *testing.T) { } } +func TestHTTPProcessesWithSystemResources(t *testing.T) { + _, _, ts := startHTTP(t) + + params := []string{ + "?system=true", + "?system_resources=true", + "?stats=true", + "?format=object", + "?include=system", + } + + for _, param := range params { + code, body := httpGet(t, ts.URL+"/api/v1/processes"+param) + if code != http.StatusOK { + t.Fatalf("GET /api/v1/processes%s code = %d, want 200", param, code) + } + if !strings.Contains(body, `"processes":[]`) { + t.Fatalf("GET /api/v1/processes%s body = %s, want processes array", param, body) + } + if !strings.Contains(body, `"system":{`) || !strings.Contains(body, `"system_resources":{`) { + t.Fatalf("GET /api/v1/processes%s body = %s, want system/system_resources object", param, body) + } + if !strings.Contains(body, `"total_memory":`) || !strings.Contains(body, `"available_memory":`) || !strings.Contains(body, `"cpu_cores":`) { + t.Fatalf("GET /api/v1/processes%s body = %s, want memory and cpu fields", param, body) + } + } +} + func TestHTTPStartListDescribeActions(t *testing.T) { _, c, ts := startHTTP(t) name := harness.UniqueName("http") diff --git a/pkg/client/table.go b/pkg/client/table.go index 61c127e..2d86e2c 100755 --- a/pkg/client/table.go +++ b/pkg/client/table.go @@ -297,7 +297,7 @@ func printUsageSummary(w io.Writer, ps []*v1.ProcessInfo, tableWidth int, color } } - sys := readSystemStats() + sys := ReadSystemStats() // 1. Process summary var procDisp string @@ -699,18 +699,19 @@ func isTerminal(w io.Writer) bool { } type SystemStats struct { - TotalMem uint64 - UsedMem uint64 - MemUsedPct float64 - CPUCores int - Load1 float64 - Load5 float64 - Load15 float64 - HasMem bool - HasLoad bool + TotalMem uint64 `json:"total_memory"` + UsedMem uint64 `json:"used_memory"` + AvailMem uint64 `json:"available_memory"` + MemUsedPct float64 `json:"memory_used_pct"` + CPUCores int `json:"cpu_cores"` + Load1 float64 `json:"load_1"` + Load5 float64 `json:"load_5"` + Load15 float64 `json:"load_15"` + HasMem bool `json:"has_mem"` + HasLoad bool `json:"has_load"` } -func readSystemStats() SystemStats { +func ReadSystemStats() SystemStats { stats := SystemStats{ CPUCores: runtime.NumCPU(), } @@ -728,15 +729,22 @@ func readSystemStats() SystemStats { if data, err := os.ReadFile("/proc/meminfo"); err == nil { lines := strings.Split(string(data), "\n") var totalKB, availKB uint64 + var foundTotal, foundAvail bool for _, line := range lines { if strings.HasPrefix(line, "MemTotal:") { totalKB = parseMemKB(line) + foundTotal = true } else if strings.HasPrefix(line, "MemAvailable:") { availKB = parseMemKB(line) + foundAvail = true + } + if foundTotal && foundAvail { + break } } if totalKB > 0 { stats.TotalMem = totalKB * 1024 + stats.AvailMem = availKB * 1024 if totalKB >= availKB { stats.UsedMem = (totalKB - availKB) * 1024 stats.MemUsedPct = float64(totalKB-availKB) / float64(totalKB) * 100 diff --git a/pkg/client/table_test.go b/pkg/client/table_test.go index 15b5170..6d28d08 100644 --- a/pkg/client/table_test.go +++ b/pkg/client/table_test.go @@ -144,7 +144,7 @@ func TestWriteListTableEmpty(t *testing.T) { } func TestReadSystemStats(t *testing.T) { - stats := readSystemStats() + stats := ReadSystemStats() if stats.CPUCores <= 0 { t.Errorf("expected CPUCores > 0, got %d", stats.CPUCores) }