package handlers import ( "bufio" "encoding/json" "fmt" "net/http" "os" "strconv" "strings" "syscall" "time" ) type MetricsResponse struct { Timestamp string `json:"timestamp"` CPU CPUMetrics `json:"cpu"` RAM RAMMetrics `json:"ram"` Disk DiskMetrics `json:"disk"` Processes ProcMetrics `json:"processes"` } type CPUMetrics struct { UsagePercent float64 `json:"usage_percent"` } type RAMMetrics struct { TotalBytes uint64 `json:"total_bytes"` UsedBytes uint64 `json:"used_bytes"` FreeBytes uint64 `json:"free_bytes"` AvailableBytes uint64 `json:"available_bytes"` UsagePercent float64 `json:"usage_percent"` } type DiskMetrics struct { Path string `json:"path"` TotalBytes uint64 `json:"total_bytes"` UsedBytes uint64 `json:"used_bytes"` FreeBytes uint64 `json:"free_bytes"` UsagePercent float64 `json:"usage_percent"` } type ProcMetrics struct { Total int `json:"total"` Running int `json:"running"` } func MetricsHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } cpu, err := readCPU() if err != nil { http.Error(w, fmt.Sprintf("cpu read error: %v", err), http.StatusInternalServerError) return } ram, err := readRAM() if err != nil { http.Error(w, fmt.Sprintf("ram read error: %v", err), http.StatusInternalServerError) return } disk, err := readDisk("/") if err != nil { http.Error(w, fmt.Sprintf("disk read error: %v", err), http.StatusInternalServerError) return } procs, err := readProcesses() if err != nil { http.Error(w, fmt.Sprintf("proc read error: %v", err), http.StatusInternalServerError) return } resp := MetricsResponse{ Timestamp: time.Now().UTC().Format(time.RFC3339), CPU: cpu, RAM: ram, Disk: disk, Processes: procs, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // readCPU samples /proc/stat twice with a short sleep and returns usage %. type cpuSample struct { user, nice, system, idleTime, iowait, irq, softirq, steal uint64 } func parseCPUSample() (cpuSample, error) { f, err := os.Open("/proc/stat") if err != nil { return cpuSample{}, err } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "cpu ") { continue } fields := strings.Fields(line) if len(fields) < 8 { return cpuSample{}, fmt.Errorf("unexpected cpu line: %s", line) } var s cpuSample vals := []*uint64{&s.user, &s.nice, &s.system, &s.idleTime, &s.iowait, &s.irq, &s.softirq, &s.steal} for i, vp := range vals { v, err := strconv.ParseUint(fields[i+1], 10, 64) if err != nil { return cpuSample{}, err } *vp = v } return s, nil } return cpuSample{}, fmt.Errorf("/proc/stat: cpu line not found") } func (s cpuSample) idleTotal() uint64 { return s.idleTime + s.iowait } func (s cpuSample) total() uint64 { return s.user + s.nice + s.system + s.idleTime + s.iowait + s.irq + s.softirq + s.steal } func readCPU() (CPUMetrics, error) { s1, err := parseCPUSample() if err != nil { return CPUMetrics{}, err } time.Sleep(200 * time.Millisecond) s2, err := parseCPUSample() if err != nil { return CPUMetrics{}, err } totalDelta := s2.total() - s1.total() idleDelta := s2.idleTotal() - s1.idleTotal() if totalDelta == 0 { return CPUMetrics{UsagePercent: 0}, nil } usage := 100.0 * float64(totalDelta-idleDelta) / float64(totalDelta) return CPUMetrics{UsagePercent: roundTwo(usage)}, nil } func readRAM() (RAMMetrics, error) { f, err := os.Open("/proc/meminfo") if err != nil { return RAMMetrics{}, err } defer f.Close() info := make(map[string]uint64) scanner := bufio.NewScanner(f) for scanner.Scan() { fields := strings.Fields(scanner.Text()) if len(fields) < 2 { continue } key := strings.TrimSuffix(fields[0], ":") val, err := strconv.ParseUint(fields[1], 10, 64) if err != nil { continue } info[key] = val * 1024 // kB → bytes } total := info["MemTotal"] free := info["MemFree"] available := info["MemAvailable"] used := total - free - info["Buffers"] - info["Cached"] - info["SReclaimable"] + info["Shmem"] var pct float64 if total > 0 { pct = roundTwo(100.0 * float64(used) / float64(total)) } return RAMMetrics{ TotalBytes: total, UsedBytes: used, FreeBytes: free, AvailableBytes: available, UsagePercent: pct, }, nil } func readDisk(path string) (DiskMetrics, error) { var stat syscall.Statfs_t if err := syscall.Statfs(path, &stat); err != nil { return DiskMetrics{}, err } total := stat.Blocks * uint64(stat.Bsize) free := stat.Bfree * uint64(stat.Bsize) avail := stat.Bavail * uint64(stat.Bsize) used := total - free var pct float64 if total > 0 { pct = roundTwo(100.0 * float64(used) / float64(total)) } return DiskMetrics{ Path: path, TotalBytes: total, UsedBytes: used, FreeBytes: avail, UsagePercent: pct, }, nil } // readProcesses counts entries in /proc that are numeric (one per process). func readProcesses() (ProcMetrics, error) { entries, err := os.ReadDir("/proc") if err != nil { return ProcMetrics{}, err } total := 0 running := 0 for _, e := range entries { if !e.IsDir() { continue } if _, err := strconv.Atoi(e.Name()); err != nil { continue } total++ statusPath := fmt.Sprintf("/proc/%s/status", e.Name()) data, err := os.ReadFile(statusPath) if err != nil { continue } for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, "State:") { // R = running, S = sleeping, D = disk sleep, Z = zombie, T = stopped if strings.Contains(line, "R (running)") { running++ } break } } } return ProcMetrics{Total: total, Running: running}, nil } func roundTwo(v float64) float64 { return float64(int(v*100+0.5)) / 100 }