Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/http-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 34 additions & 1 deletion internal/daemon/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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})
Expand Down
28 changes: 28 additions & 0 deletions internal/daemon/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
30 changes: 19 additions & 11 deletions pkg/client/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
}
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkg/client/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading