Files
boc/aamos-admin-upgrade/backend/handlers/metrics.go
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- Added GLOBAL_MARKETS_TITLE to all translation files
- Updated footer with 12 markets (4 active + 8 upcoming)
- Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi
- Built and deployed to production
- CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
2026-07-08 19:56:03 +00:00

258 lines
5.9 KiB
Go

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
}