|
@@ -10,7 +10,6 @@ import (
|
|
|
"net/http"
|
|
"net/http"
|
|
|
"os"
|
|
"os"
|
|
|
"os/exec"
|
|
"os/exec"
|
|
|
- "runtime"
|
|
|
|
|
"strconv"
|
|
"strconv"
|
|
|
"strings"
|
|
"strings"
|
|
|
"sync"
|
|
"sync"
|
|
@@ -40,6 +39,10 @@ type APIServer struct {
|
|
|
// Session management
|
|
// Session management
|
|
|
sessions map[string]time.Time
|
|
sessions map[string]time.Time
|
|
|
sessionsMu sync.RWMutex
|
|
sessionsMu sync.RWMutex
|
|
|
|
|
+
|
|
|
|
|
+ // Cached CPU metrics (updated in background)
|
|
|
|
|
+ cachedCPUPercent float64
|
|
|
|
|
+ cpuMu sync.RWMutex
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// StatusResponse is the response for /api/status
|
|
// StatusResponse is the response for /api/status
|
|
@@ -92,7 +95,9 @@ type MetricsResponse struct {
|
|
|
MemUsedMB float64 `json:"mem_used_mb"`
|
|
MemUsedMB float64 `json:"mem_used_mb"`
|
|
|
MemTotalMB float64 `json:"mem_total_mb"`
|
|
MemTotalMB float64 `json:"mem_total_mb"`
|
|
|
Temperature float64 `json:"temperature"`
|
|
Temperature float64 `json:"temperature"`
|
|
|
- LoadAvg float64 `json:"load_avg"`
|
|
|
|
|
|
|
+ Load1m float64 `json:"load_1m"`
|
|
|
|
|
+ Load5m float64 `json:"load_5m"`
|
|
|
|
|
+ Load15m float64 `json:"load_15m"`
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// SettingsRequest is the request for /api/settings
|
|
// SettingsRequest is the request for /api/settings
|
|
@@ -155,6 +160,9 @@ func (s *APIServer) Start(addr string) error {
|
|
|
return fmt.Errorf("HTTP server already running")
|
|
return fmt.Errorf("HTTP server already running")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Start CPU monitoring goroutine
|
|
|
|
|
+ go s.updateCPUMetrics()
|
|
|
|
|
+
|
|
|
mux := http.NewServeMux()
|
|
mux := http.NewServeMux()
|
|
|
|
|
|
|
|
// API endpoints
|
|
// API endpoints
|
|
@@ -290,14 +298,34 @@ func (s *APIServer) handleStatus(w http.ResponseWriter, r *http.Request) {
|
|
|
s.jsonResponse(w, status)
|
|
s.jsonResponse(w, status)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// updateCPUMetrics updates CPU metrics in background
|
|
|
|
|
+func (s *APIServer) updateCPUMetrics() {
|
|
|
|
|
+ for {
|
|
|
|
|
+ cpuPercent := getCPUPercent()
|
|
|
|
|
+ s.cpuMu.Lock()
|
|
|
|
|
+ s.cachedCPUPercent = cpuPercent
|
|
|
|
|
+ s.cpuMu.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ time.Sleep(2 * time.Second)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// handleMetrics returns system metrics
|
|
// handleMetrics returns system metrics
|
|
|
func (s *APIServer) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
|
func (s *APIServer) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
+ load1m, load5m, load15m := getLoadAvg()
|
|
|
|
|
+
|
|
|
|
|
+ s.cpuMu.RLock()
|
|
|
|
|
+ cpuPercent := s.cachedCPUPercent
|
|
|
|
|
+ s.cpuMu.RUnlock()
|
|
|
|
|
+
|
|
|
metrics := MetricsResponse{
|
|
metrics := MetricsResponse{
|
|
|
- CPUPercent: getCPUPercent(),
|
|
|
|
|
|
|
+ CPUPercent: cpuPercent,
|
|
|
MemUsedMB: getMemUsedMB(),
|
|
MemUsedMB: getMemUsedMB(),
|
|
|
MemTotalMB: getMemTotalMB(),
|
|
MemTotalMB: getMemTotalMB(),
|
|
|
Temperature: getTemperature(),
|
|
Temperature: getTemperature(),
|
|
|
- LoadAvg: getLoadAvg(),
|
|
|
|
|
|
|
+ Load1m: load1m,
|
|
|
|
|
+ Load5m: load5m,
|
|
|
|
|
+ Load15m: load15m,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
s.jsonResponse(w, metrics)
|
|
s.jsonResponse(w, metrics)
|
|
@@ -653,14 +681,99 @@ func parseFloat(s string, f *float64) error {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func getCPUPercent() float64 {
|
|
func getCPUPercent() float64 {
|
|
|
- // Simplified - read from /proc/stat
|
|
|
|
|
- return 0 // TODO: implement proper CPU usage
|
|
|
|
|
|
|
+ // Read CPU stats from /proc/stat twice with 100ms interval
|
|
|
|
|
+ stat1 := readCPUStat()
|
|
|
|
|
+ time.Sleep(100 * time.Millisecond)
|
|
|
|
|
+ stat2 := readCPUStat()
|
|
|
|
|
+
|
|
|
|
|
+ if stat1 == nil || stat2 == nil {
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate totals (sum all fields)
|
|
|
|
|
+ var total1, total2, idle1, idle2 int64
|
|
|
|
|
+ for i := 0; i < len(stat1) && i < 10; i++ {
|
|
|
|
|
+ total1 += stat1[i]
|
|
|
|
|
+ if i < len(stat2) {
|
|
|
|
|
+ total2 += stat2[i]
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Idle = idle + iowait (fields 3 and 4)
|
|
|
|
|
+ if len(stat1) >= 5 {
|
|
|
|
|
+ idle1 = stat1[3] + stat1[4]
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(stat2) >= 5 {
|
|
|
|
|
+ idle2 = stat2[3] + stat2[4]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ totalDelta := total2 - total1
|
|
|
|
|
+ idleDelta := idle2 - idle1
|
|
|
|
|
+
|
|
|
|
|
+ if totalDelta == 0 {
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ cpuPercent := 100.0 * (1.0 - float64(idleDelta)/float64(totalDelta))
|
|
|
|
|
+ return cpuPercent
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func readCPUStat() []int64 {
|
|
|
|
|
+ data, err := os.ReadFile("/proc/stat")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // First line: cpu user nice system idle iowait irq softirq steal guest guest_nice
|
|
|
|
|
+ scanner := bufio.NewScanner(strings.NewReader(string(data)))
|
|
|
|
|
+ if !scanner.Scan() {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ line := scanner.Text()
|
|
|
|
|
+ if !strings.HasPrefix(line, "cpu ") {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fields := strings.Fields(line)
|
|
|
|
|
+ if len(fields) < 5 {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var stats []int64
|
|
|
|
|
+ // Read all available fields (up to 10)
|
|
|
|
|
+ for i := 1; i < len(fields) && i < 11; i++ {
|
|
|
|
|
+ var v int64
|
|
|
|
|
+ json.Unmarshal([]byte(fields[i]), &v)
|
|
|
|
|
+ stats = append(stats, v)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return stats
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func getMemUsedMB() float64 {
|
|
func getMemUsedMB() float64 {
|
|
|
- var m runtime.MemStats
|
|
|
|
|
- runtime.ReadMemStats(&m)
|
|
|
|
|
- return float64(m.Alloc) / 1024 / 1024
|
|
|
|
|
|
|
+ data, err := os.ReadFile("/proc/meminfo")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var memTotal, memAvailable int64
|
|
|
|
|
+ scanner := bufio.NewScanner(strings.NewReader(string(data)))
|
|
|
|
|
+ for scanner.Scan() {
|
|
|
|
|
+ line := scanner.Text()
|
|
|
|
|
+ if strings.HasPrefix(line, "MemTotal:") {
|
|
|
|
|
+ parseMemInfo(line, &memTotal)
|
|
|
|
|
+ } else if strings.HasPrefix(line, "MemAvailable:") {
|
|
|
|
|
+ parseMemInfo(line, &memAvailable)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if memTotal == 0 {
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ memUsed := memTotal - memAvailable
|
|
|
|
|
+ return float64(memUsed) / 1024 // Convert KB to MB
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func getMemTotalMB() float64 {
|
|
func getMemTotalMB() float64 {
|
|
@@ -701,16 +814,24 @@ func getTemperature() float64 {
|
|
|
return float64(temp) / 1000
|
|
return float64(temp) / 1000
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func getLoadAvg() float64 {
|
|
|
|
|
|
|
+func getLoadAvg() (float64, float64, float64) {
|
|
|
data, err := os.ReadFile("/proc/loadavg")
|
|
data, err := os.ReadFile("/proc/loadavg")
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- return 0
|
|
|
|
|
|
|
+ return 0, 0, 0
|
|
|
}
|
|
}
|
|
|
- var load float64
|
|
|
|
|
- if err := parseFloat(string(data), &load); err != nil {
|
|
|
|
|
- return 0
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // /proc/loadavg format: "0.10 0.40 0.49 1/98 2452"
|
|
|
|
|
+ parts := strings.Fields(string(data))
|
|
|
|
|
+ if len(parts) < 3 {
|
|
|
|
|
+ return 0, 0, 0
|
|
|
}
|
|
}
|
|
|
- return load
|
|
|
|
|
|
|
+
|
|
|
|
|
+ var load1m, load5m, load15m float64
|
|
|
|
|
+ json.Unmarshal([]byte(parts[0]), &load1m)
|
|
|
|
|
+ json.Unmarshal([]byte(parts[1]), &load5m)
|
|
|
|
|
+ json.Unmarshal([]byte(parts[2]), &load15m)
|
|
|
|
|
+
|
|
|
|
|
+ return load1m, load5m, load15m
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func generateToken(length int) string {
|
|
func generateToken(length int) string {
|