6de2455917
- 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
258 lines
5.9 KiB
Go
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
|
|
}
|