201 lines
5.1 KiB
Go

package report
import (
"context"
"os"
"strconv"
"strings"
"time"
)
// collectSystem gathers system-level metrics using local commands and /proc files.
func collectSystem() *SystemReport {
r := &SystemReport{}
// 1. Uptime seconds: read /proc/uptime, parse first field
if data, err := os.ReadFile("/proc/uptime"); err == nil {
fields := strings.Fields(string(data))
if len(fields) >= 1 {
if f, err := strconv.ParseFloat(fields[0], 64); err == nil {
r.UptimeSeconds = int64(f)
}
}
}
// 2. Uptime since: run `uptime -s`
{
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
if out, err := runCmd(ctx, "uptime", "-s"); err == nil {
r.UptimeSince = strings.TrimSpace(out)
}
}
// 3. CPU count: run `nproc`
{
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
if out, err := runCmd(ctx, "nproc"); err == nil {
if n, err := strconv.Atoi(strings.TrimSpace(out)); err == nil {
r.CPUCount = n
}
}
}
// 4. Load averages: read /proc/loadavg, parse first 3 fields
if data, err := os.ReadFile("/proc/loadavg"); err == nil {
fields := strings.Fields(string(data))
if len(fields) >= 3 {
if f, err := strconv.ParseFloat(fields[0], 64); err == nil {
r.LoadAvg1 = f
}
if f, err := strconv.ParseFloat(fields[1], 64); err == nil {
r.LoadAvg5 = f
}
if f, err := strconv.ParseFloat(fields[2], 64); err == nil {
r.LoadAvg15 = f
}
}
}
// 5 & 6. Memory and swap: run `free -m`, parse Mem: and Swap: lines
{
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
if out, err := runCmd(ctx, "free", "-m"); err == nil {
for _, line := range strings.Split(out, "\n") {
fields := strings.Fields(line)
if len(fields) >= 4 && fields[0] == "Mem:" {
// Mem: total used free shared buff/cache available
if n, err := strconv.Atoi(fields[1]); err == nil {
r.MemTotalMB = n
}
if n, err := strconv.Atoi(fields[2]); err == nil {
r.MemUsedMB = n
}
if n, err := strconv.Atoi(fields[3]); err == nil {
r.MemFreeMB = n
}
if len(fields) >= 7 {
if n, err := strconv.Atoi(fields[6]); err == nil {
r.MemAvailMB = n
}
}
if r.MemTotalMB > 0 {
r.MemUsePct = (r.MemTotalMB - r.MemAvailMB) * 100 / r.MemTotalMB
}
}
if len(fields) >= 3 && fields[0] == "Swap:" {
if n, err := strconv.Atoi(fields[1]); err == nil {
r.SwapTotalMB = n
}
if n, err := strconv.Atoi(fields[2]); err == nil {
r.SwapUsedMB = n
}
}
}
}
}
// 7. Disk usage: run `df -h /` and `df -h /opt/orama`, use whichever has higher usage
{
type diskInfo struct {
total string
used string
avail string
usePct int
}
parseDf := func(out string) *diskInfo {
lines := strings.Split(out, "\n")
if len(lines) < 2 {
return nil
}
fields := strings.Fields(lines[1])
if len(fields) < 5 {
return nil
}
pctStr := strings.TrimSuffix(fields[4], "%")
pct, err := strconv.Atoi(pctStr)
if err != nil {
return nil
}
return &diskInfo{
total: fields[1],
used: fields[2],
avail: fields[3],
usePct: pct,
}
}
ctx1, cancel1 := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel1()
rootDisk := (*diskInfo)(nil)
if out, err := runCmd(ctx1, "df", "-h", "/"); err == nil {
rootDisk = parseDf(out)
}
ctx2, cancel2 := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel2()
optDisk := (*diskInfo)(nil)
if out, err := runCmd(ctx2, "df", "-h", "/opt/orama"); err == nil {
optDisk = parseDf(out)
}
best := rootDisk
if optDisk != nil && (best == nil || optDisk.usePct > best.usePct) {
best = optDisk
}
if best != nil {
r.DiskTotalGB = best.total
r.DiskUsedGB = best.used
r.DiskAvailGB = best.avail
r.DiskUsePct = best.usePct
}
}
// 8. Inode usage: run `df -i /`, parse Use% from second line
{
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
if out, err := runCmd(ctx, "df", "-i", "/"); err == nil {
lines := strings.Split(out, "\n")
if len(lines) >= 2 {
fields := strings.Fields(lines[1])
if len(fields) >= 5 {
pctStr := strings.TrimSuffix(fields[4], "%")
if n, err := strconv.Atoi(pctStr); err == nil {
r.InodePct = n
}
}
}
}
}
// 9. OOM kills: run `dmesg 2>/dev/null | grep -ci 'out of memory'` via bash -c
{
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
if out, err := runCmd(ctx, "bash", "-c", "dmesg 2>/dev/null | grep -ci 'out of memory'"); err == nil {
if n, err := strconv.Atoi(strings.TrimSpace(out)); err == nil {
r.OOMKills = n
}
}
// On error, OOMKills stays 0 (zero value)
}
// 10. Kernel version: run `uname -r`
{
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
if out, err := runCmd(ctx, "uname", "-r"); err == nil {
r.KernelVersion = strings.TrimSpace(out)
}
}
// 11. Current unix timestamp
r.TimeUnix = time.Now().Unix()
return r
}