344 lines
8.1 KiB
Go
344 lines
8.1 KiB
Go
// Package statcollector provides system information collection
|
|
package statcollector
|
|
|
|
import (
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/hdmistat/internal/netmon"
|
|
"github.com/dustin/go-humanize"
|
|
"github.com/shirou/gopsutil/v3/cpu"
|
|
"github.com/shirou/gopsutil/v3/disk"
|
|
"github.com/shirou/gopsutil/v3/host"
|
|
"github.com/shirou/gopsutil/v3/mem"
|
|
psnet "github.com/shirou/gopsutil/v3/net"
|
|
"github.com/shirou/gopsutil/v3/process"
|
|
)
|
|
|
|
const (
|
|
// Process collection constants
|
|
maxProcesses = 100
|
|
processTimeout = 50 * time.Millisecond
|
|
processStableTime = 100 * time.Millisecond
|
|
msToSecondsDivisor = 1000
|
|
|
|
// Network constants
|
|
bitsPerMegabit = 1000 * 1000
|
|
)
|
|
|
|
// SystemInfo represents overall system information
|
|
type SystemInfo struct {
|
|
Hostname string
|
|
Uptime time.Duration
|
|
MemoryTotal uint64
|
|
MemoryUsed uint64
|
|
MemoryFree uint64
|
|
CPUPercent []float64
|
|
Temperature map[string]float64
|
|
DiskUsage []DiskInfo
|
|
Network []NetworkInfo
|
|
Processes []ProcessInfo
|
|
CollectedAt time.Time
|
|
}
|
|
|
|
// DiskInfo represents disk usage information
|
|
type DiskInfo struct {
|
|
Path string
|
|
Total uint64
|
|
Used uint64
|
|
Free uint64
|
|
UsedPercent float64
|
|
}
|
|
|
|
// NetworkInfo represents network interface information
|
|
type NetworkInfo struct {
|
|
Name string
|
|
IPAddresses []string
|
|
LinkSpeed uint64
|
|
BytesSent uint64
|
|
BytesRecv uint64
|
|
BitsSentRate uint64 // bits per second
|
|
BitsRecvRate uint64 // bits per second
|
|
}
|
|
|
|
// FormatSentRate returns the send rate as a human-readable string
|
|
func (n *NetworkInfo) FormatSentRate() string {
|
|
return humanize.SI(float64(n.BitsSentRate), "bit/s")
|
|
}
|
|
|
|
// FormatRecvRate returns the receive rate as a human-readable string
|
|
func (n *NetworkInfo) FormatRecvRate() string {
|
|
return humanize.SI(float64(n.BitsRecvRate), "bit/s")
|
|
}
|
|
|
|
// ProcessInfo represents process information
|
|
type ProcessInfo struct {
|
|
PID int32
|
|
Name string
|
|
CPUPercent float64
|
|
MemoryRSS uint64
|
|
MemoryVMS uint64
|
|
Username string
|
|
}
|
|
|
|
// Collector interface for collecting system information
|
|
type Collector interface {
|
|
Collect() (*SystemInfo, error)
|
|
}
|
|
|
|
// SystemCollector implements Collector
|
|
type SystemCollector struct {
|
|
logger *slog.Logger
|
|
netMonitor *netmon.Monitor
|
|
lastCollectTime time.Time
|
|
}
|
|
|
|
// NewSystemCollector creates a new system collector
|
|
func NewSystemCollector(logger *slog.Logger) *SystemCollector {
|
|
nm := netmon.New(logger)
|
|
nm.Start()
|
|
|
|
return &SystemCollector{
|
|
logger: logger,
|
|
netMonitor: nm,
|
|
}
|
|
}
|
|
|
|
// Stop stops the system collector
|
|
func (c *SystemCollector) Stop() {
|
|
if c.netMonitor != nil {
|
|
c.netMonitor.Stop()
|
|
}
|
|
}
|
|
|
|
// Collect gathers system information
|
|
func (c *SystemCollector) Collect() (*SystemInfo, error) {
|
|
info := &SystemInfo{
|
|
CollectedAt: time.Now(),
|
|
Temperature: make(map[string]float64),
|
|
}
|
|
|
|
// Hostname
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
c.logger.Warn("getting hostname", "error", err)
|
|
info.Hostname = "unknown"
|
|
} else {
|
|
info.Hostname = hostname
|
|
}
|
|
|
|
// Uptime
|
|
uptimeSecs, err := host.Uptime()
|
|
if err != nil {
|
|
c.logger.Warn("getting uptime", "error", err)
|
|
} else {
|
|
if uptimeSecs > 0 {
|
|
// Convert uint64 to int64 safely to avoid overflow
|
|
maxInt64 := ^uint64(0) >> 1
|
|
if uptimeSecs <= maxInt64 {
|
|
info.Uptime = time.Duration(int64(uptimeSecs)) * time.Second
|
|
}
|
|
}
|
|
}
|
|
|
|
// Memory
|
|
vmStat, err := mem.VirtualMemory()
|
|
if err != nil {
|
|
c.logger.Warn("getting memory stats", "error", err)
|
|
} else {
|
|
info.MemoryTotal = vmStat.Total
|
|
info.MemoryUsed = vmStat.Used
|
|
info.MemoryFree = vmStat.Available
|
|
}
|
|
|
|
// CPU
|
|
cpuPercent, err := cpu.Percent(time.Second, true)
|
|
if err != nil {
|
|
c.logger.Warn("getting cpu percent", "error", err)
|
|
} else {
|
|
info.CPUPercent = cpuPercent
|
|
}
|
|
|
|
// Temperature
|
|
temps, err := host.SensorsTemperatures()
|
|
if err != nil {
|
|
c.logger.Warn("getting temperatures", "error", err)
|
|
} else {
|
|
for _, temp := range temps {
|
|
if temp.Temperature > 0 {
|
|
info.Temperature[temp.SensorKey] = temp.Temperature
|
|
}
|
|
}
|
|
}
|
|
|
|
// Disk usage
|
|
partitions, err := disk.Partitions(false)
|
|
if err != nil {
|
|
c.logger.Warn("getting partitions", "error", err)
|
|
} else {
|
|
for _, partition := range partitions {
|
|
if strings.HasPrefix(partition.Mountpoint, "/dev") ||
|
|
strings.HasPrefix(partition.Mountpoint, "/sys") ||
|
|
strings.HasPrefix(partition.Mountpoint, "/proc") {
|
|
continue
|
|
}
|
|
|
|
usage, err := disk.Usage(partition.Mountpoint)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
info.DiskUsage = append(info.DiskUsage, DiskInfo{
|
|
Path: partition.Mountpoint,
|
|
Total: usage.Total,
|
|
Used: usage.Used,
|
|
Free: usage.Free,
|
|
UsedPercent: usage.UsedPercent,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Network - get rates from network monitor
|
|
netStats := c.netMonitor.GetStats()
|
|
|
|
// Also get interface details for IP addresses
|
|
interfaces, err := psnet.Interfaces()
|
|
if err != nil {
|
|
c.logger.Warn("getting network interfaces", "error", err)
|
|
} else {
|
|
// Create a map of interface names to IPs and link speeds
|
|
ifaceIPs := make(map[string][]string)
|
|
ifaceSpeeds := make(map[string]uint64)
|
|
|
|
for _, iface := range interfaces {
|
|
if iface.Name == "lo" || strings.HasPrefix(iface.Name, "docker") {
|
|
continue
|
|
}
|
|
var ips []string
|
|
for _, addr := range iface.Addrs {
|
|
ips = append(ips, addr.Addr)
|
|
}
|
|
ifaceIPs[iface.Name] = ips
|
|
|
|
// Try to get link speed with ethtool
|
|
if speed := c.getLinkSpeed(iface.Name); speed > 0 {
|
|
ifaceSpeeds[iface.Name] = speed
|
|
}
|
|
}
|
|
|
|
// Combine network monitor stats with interface details
|
|
for _, stat := range netStats {
|
|
netInfo := NetworkInfo{
|
|
Name: stat.Name,
|
|
BytesSent: stat.BytesSent,
|
|
BytesRecv: stat.BytesRecv,
|
|
BitsSentRate: stat.BitsSentRate,
|
|
BitsRecvRate: stat.BitsRecvRate,
|
|
}
|
|
|
|
// Add IP addresses if available
|
|
if ips, ok := ifaceIPs[stat.Name]; ok {
|
|
netInfo.IPAddresses = ips
|
|
}
|
|
|
|
// Add link speed if available
|
|
if speed, ok := ifaceSpeeds[stat.Name]; ok {
|
|
netInfo.LinkSpeed = speed
|
|
}
|
|
|
|
info.Network = append(info.Network, netInfo)
|
|
}
|
|
}
|
|
|
|
// Processes
|
|
processes, err := process.Processes()
|
|
if err != nil {
|
|
c.logger.Warn("getting processes", "error", err)
|
|
} else {
|
|
// Limit to top processes to avoid hanging
|
|
processCount := 0
|
|
|
|
for _, p := range processes {
|
|
if processCount >= maxProcesses {
|
|
break
|
|
}
|
|
|
|
// Skip kernel threads and very short-lived processes
|
|
name, err := p.Name()
|
|
if err != nil || name == "" {
|
|
continue
|
|
}
|
|
|
|
// Use CreateTime to skip very new processes that might not have stable stats
|
|
createTime, err := p.CreateTime()
|
|
if err != nil || time.Since(time.Unix(createTime/msToSecondsDivisor, 0)) < processStableTime {
|
|
continue
|
|
}
|
|
|
|
// Get CPU percent with timeout - this is the call that can hang
|
|
cpuPercent := 0.0
|
|
cpuChan := make(chan float64, 1)
|
|
go func() {
|
|
cpu, _ := p.CPUPercent()
|
|
cpuChan <- cpu
|
|
}()
|
|
|
|
select {
|
|
case cpu := <-cpuChan:
|
|
cpuPercent = cpu
|
|
case <-time.After(processTimeout):
|
|
// Skip this process if CPU sampling takes too long
|
|
c.logger.Debug("skipping process due to CPU timeout", "pid", p.Pid, "name", name)
|
|
continue
|
|
}
|
|
|
|
memInfo, _ := p.MemoryInfo()
|
|
username, _ := p.Username()
|
|
|
|
info.Processes = append(info.Processes, ProcessInfo{
|
|
PID: p.Pid,
|
|
Name: name,
|
|
CPUPercent: cpuPercent,
|
|
MemoryRSS: memInfo.RSS,
|
|
MemoryVMS: memInfo.VMS,
|
|
Username: username,
|
|
})
|
|
|
|
processCount++
|
|
}
|
|
}
|
|
|
|
c.lastCollectTime = time.Now()
|
|
return info, nil
|
|
}
|
|
|
|
// getLinkSpeed gets the link speed for an interface using ethtool
|
|
func (c *SystemCollector) getLinkSpeed(ifaceName string) uint64 {
|
|
// Run ethtool to get link speed
|
|
output, err := exec.Command("ethtool", ifaceName).Output()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
|
|
// Parse the output for speed
|
|
// Look for lines like "Speed: 1000Mb/s" or "Speed: 10000Mb/s"
|
|
speedRegex := regexp.MustCompile(`Speed:\s+(\d+)Mb/s`)
|
|
matches := speedRegex.FindSubmatch(output)
|
|
if len(matches) < 2 {
|
|
return 0
|
|
}
|
|
|
|
// Convert from Mb/s to bits/s
|
|
mbps, err := strconv.ParseUint(string(matches[1]), 10, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
|
|
return mbps * bitsPerMegabit // Convert to bits per second
|
|
}
|