350 lines
11 KiB
Go
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
|
|
}
|