Files
nvllama-watchdog/readers/system.go
2026-10-11 14:39:54 +02:00

350 lines
11 KiB
Go

package readers
import (
"context"
"fmt"
"sort"
"time"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/host"
"github.com/shirou/gopsutil/v4/load"
"github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/net"
"github.com/shirou/gopsutil/v4/process"
"git.govekar.net/LovroG05/nvllama-watchdog/core"
)
type SystemReader struct {
enabled map[string]bool
}
func NewSystemReader(enabled map[string]bool) *SystemReader {
if enabled == nil {
enabled = map[string]bool{
"cpu": true,
"memory": true,
"disk": true,
"network": true,
"load": true,
"uptime": true,
}
}
return &SystemReader{enabled: enabled}
}
func (r *SystemReader) Name() string { return "system" }
func (r *SystemReader) Read(ctx context.Context) ([]core.Metric, error) {
now := time.Now()
var metrics []core.Metric
var errs []error
if r.enabled["cpu"] {
ms, err := r.readCPU(ctx)
if err != nil {
errs = append(errs, fmt.Errorf("cpu: %w", err))
}
metrics = append(metrics, ms...)
}
if r.enabled["memory"] {
ms, err := r.readMemory(ctx)
if err != nil {
errs = append(errs, fmt.Errorf("memory: %w", err))
}
metrics = append(metrics, ms...)
}
if r.enabled["disk"] {
ms, err := r.readDisk(ctx)
if err != nil {
errs = append(errs, fmt.Errorf("disk: %w", err))
}
metrics = append(metrics, ms...)
}
if r.enabled["network"] {
ms, err := r.readNetwork(ctx)
if err != nil {
errs = append(errs, fmt.Errorf("network: %w", err))
}
metrics = append(metrics, ms...)
}
if r.enabled["load"] {
ms, err := r.readLoad(ctx)
if err != nil {
errs = append(errs, fmt.Errorf("load: %w", err))
}
metrics = append(metrics, ms...)
}
if r.enabled["uptime"] {
ms, err := r.readUptime(ctx)
if err != nil {
errs = append(errs, fmt.Errorf("uptime: %w", err))
}
metrics = append(metrics, ms...)
}
for i := range metrics {
metrics[i].Timestamp = now
}
if len(errs) > 0 {
return metrics, fmt.Errorf("system reader: %v", errs)
}
return metrics, nil
}
// ─── CPU ────────────────────────────────────────────────────────────────────
func (r *SystemReader) readCPU(_ context.Context) ([]core.Metric, error) {
// Per-core usage (sample over 250ms to get a meaningful percentage)
percents, err := cpu.Percent(250*time.Millisecond, true)
if err != nil {
return nil, err
}
var metrics []core.Metric
for i, p := range percents {
metrics = append(metrics, core.Metric{
Name: "cpu_usage_percent",
Help: "CPU usage per core in percent",
Type: "gauge",
Value: p,
Labels: map[string]string{"core": fmt.Sprintf("%d", i)},
})
}
// Total CPU times (user, system, idle, iowait, irq, softirq, steal)
times, err := cpu.Times(false)
if err != nil {
return metrics, err
}
if len(times) > 0 {
t := times[0]
total := t.User + t.System + t.Idle + t.Iowait + t.Irq + t.Softirq + t.Steal + t.Nice
if total > 0 {
metrics = append(metrics,
core.Metric{Name: "cpu_user_percent", Value: t.User / total * 100, Labels: map[string]string{}},
core.Metric{Name: "cpu_system_percent", Value: t.System / total * 100, Labels: map[string]string{}},
core.Metric{Name: "cpu_idle_percent", Value: t.Idle / total * 100, Labels: map[string]string{}},
core.Metric{Name: "cpu_iowait_percent", Value: t.Iowait / total * 100, Labels: map[string]string{}},
core.Metric{Name: "cpu_steal_percent", Value: t.Steal / total * 100, Labels: map[string]string{}},
)
}
}
// Core count
count, _ := cpu.Counts(true)
metrics = append(metrics, core.Metric{
Name: "cpu_cores",
Help: "Number of logical CPU cores",
Type: "gauge",
Value: float64(count),
Labels: map[string]string{},
})
return metrics, nil
}
// ─── Memory ─────────────────────────────────────────────────────────────────
func (r *SystemReader) readMemory(_ context.Context) ([]core.Metric, error) {
vm, err := mem.VirtualMemory()
if err != nil {
return nil, err
}
used := vm.Total - vm.Available
labels := map[string]string{}
metrics := []core.Metric{
{Name: "memory_total_bytes", Value: float64(vm.Total), Labels: labels},
{Name: "memory_used_bytes", Value: float64(used), Labels: labels},
{Name: "memory_available_bytes", Value: float64(vm.Available), Labels: labels},
{Name: "memory_buffers_bytes", Value: float64(vm.Buffers), Labels: labels},
{Name: "memory_cached_bytes", Value: float64(vm.Cached), Labels: labels},
{Name: "memory_used_percent", Value: vm.UsedPercent, Labels: labels},
}
// Swap
sw, err := mem.SwapMemory()
if err == nil {
metrics = append(metrics,
core.Metric{Name: "memory_swap_total_bytes", Value: float64(sw.Total), Labels: labels},
core.Metric{Name: "memory_swap_used_bytes", Value: float64(sw.Used), Labels: labels},
core.Metric{Name: "memory_swap_used_percent", Value: sw.UsedPercent, Labels: labels},
)
}
return metrics, nil
}
// ─── Disk I/O ───────────────────────────────────────────────────────────────
func (r *SystemReader) readDisk(_ context.Context) ([]core.Metric, error) {
ioCounters, err := disk.IOCounters()
if err != nil {
return nil, err
}
var metrics []core.Metric
for name, io := range ioCounters {
labels := map[string]string{"device": name}
metrics = append(metrics,
core.Metric{Name: "disk_read_bytes_total", Value: float64(io.ReadBytes), Labels: labels},
core.Metric{Name: "disk_write_bytes_total", Value: float64(io.WriteBytes), Labels: labels},
core.Metric{Name: "disk_reads_total", Value: float64(io.ReadCount), Labels: labels},
core.Metric{Name: "disk_writes_total", Value: float64(io.WriteCount), Labels: labels},
core.Metric{Name: "disk_io_time_seconds_total", Value: float64(io.IoTime) / 1000.0, Labels: labels},
core.Metric{Name: "disk_weighted_io_time_seconds_total", Value: float64(io.WeightedIO) / 1000.0, Labels: labels},
core.Metric{Name: "disk_read_time_seconds_total", Value: float64(io.ReadTime) / 1000.0, Labels: labels},
core.Metric{Name: "disk_write_time_seconds_total", Value: float64(io.WriteTime) / 1000.0, Labels: labels},
)
}
// Disk usage (partition-level)
parts, err := disk.Partitions(false)
if err == nil {
for _, p := range parts {
usage, err := disk.Usage(p.Mountpoint)
if err != nil {
continue
}
labels := map[string]string{"mountpoint": p.Mountpoint, "fstype": p.Fstype}
metrics = append(metrics,
core.Metric{Name: "disk_total_bytes", Value: float64(usage.Total), Labels: labels},
core.Metric{Name: "disk_used_bytes", Value: float64(usage.Used), Labels: labels},
core.Metric{Name: "disk_free_bytes", Value: float64(usage.Free), Labels: labels},
core.Metric{Name: "disk_used_percent", Value: usage.UsedPercent, Labels: labels},
)
}
}
return metrics, nil
}
// ─── Network ────────────────────────────────────────────────────────────────
func (r *SystemReader) readNetwork(_ context.Context) ([]core.Metric, error) {
counters, err := net.IOCounters(true) // per-interface
if err != nil {
return nil, err
}
var metrics []core.Metric
for _, c := range counters {
labels := map[string]string{"interface": c.Name}
metrics = append(metrics,
core.Metric{Name: "network_receive_bytes_total", Value: float64(c.BytesRecv), Labels: labels},
core.Metric{Name: "network_transmit_bytes_total", Value: float64(c.BytesSent), Labels: labels},
core.Metric{Name: "network_receive_packets_total", Value: float64(c.PacketsRecv), Labels: labels},
core.Metric{Name: "network_transmit_packets_total", Value: float64(c.PacketsSent), Labels: labels},
core.Metric{Name: "network_receive_errors_total", Value: float64(c.Errin), Labels: labels},
core.Metric{Name: "network_transmit_errors_total", Value: float64(c.Errout), Labels: labels},
core.Metric{Name: "network_receive_dropped_total", Value: float64(c.Dropin), Labels: labels},
core.Metric{Name: "network_transmit_dropped_total", Value: float64(c.Dropout), Labels: labels},
)
}
return metrics, nil
}
// ─── Load ───────────────────────────────────────────────────────────────────
func (r *SystemReader) readLoad(_ context.Context) ([]core.Metric, error) {
loads, err := load.Avg()
if err != nil {
return nil, err
}
return []core.Metric{
{Name: "load_1m", Value: loads.Load1, Labels: map[string]string{}},
{Name: "load_5m", Value: loads.Load5, Labels: map[string]string{}},
{Name: "load_15m", Value: loads.Load15, Labels: map[string]string{}},
}, nil
}
// ─── Uptime ─────────────────────────────────────────────────────────────────
func (r *SystemReader) readUptime(_ context.Context) ([]core.Metric, error) {
secs, err := host.Uptime()
if err != nil {
return nil, err
}
info, _ := host.Info()
metrics := []core.Metric{
{Name: "system_uptime_seconds", Value: float64(secs), Labels: map[string]string{}},
}
if info != nil {
metrics = append(metrics, core.Metric{
Name: "system_boot_time_seconds",
Value: float64(info.BootTime),
Labels: map[string]string{"os": info.OS, "platform": info.Platform, "kernel_version": info.KernelVersion},
})
}
return metrics, nil
}
// ─── Optional: top-N processes (expensive, off by default) ─────────────────
func (r *SystemReader) ReadTopProcesses(n int) ([]core.Metric, error) {
procs, err := process.Processes()
if err != nil {
return nil, err
}
type procInfo struct {
pid int32
name string
cpu float64
memRss uint64
}
var infos []procInfo
for _, p := range procs {
pid := p.Pid
name, _ := p.Name()
cpuPct, _ := p.CPUPercent()
memInfo, _ := p.MemoryInfo()
var rss uint64
if memInfo != nil {
rss = memInfo.RSS
}
infos = append(infos, procInfo{pid, name, cpuPct, rss})
}
// Simple top-N by CPU (avoid importing container/heap for a small slice)
// For large process lists you'd want sort.Slice or a heap.
if len(infos) > n {
// partial selection: just sort and truncate
sort.Slice(infos, func(i, j int) bool {
return infos[i].cpu > infos[j].cpu
})
infos = infos[:n]
}
var metrics []core.Metric
for _, p := range infos {
labels := map[string]string{
"pid": fmt.Sprintf("%d", p.pid),
"name": p.name,
}
metrics = append(metrics,
core.Metric{Name: "process_cpu_percent", Value: p.cpu, Labels: labels},
core.Metric{Name: "process_memory_rss_bytes", Value: float64(p.memRss), Labels: labels},
)
}
return metrics, nil
}