Features: - Beautiful system statistics display using IBM Plex Mono font - Direct framebuffer rendering without X11/Wayland - Multiple screens with automatic carousel rotation - Real-time system monitoring (CPU, memory, disk, network, processes) - Systemd service integration with install command - Clean architecture using uber/fx dependency injection Architecture: - Cobra CLI with daemon, install, status, and info commands - Modular design with separate packages for display, rendering, and stats - Font embedding for zero runtime dependencies - Layout API for clean text rendering - Support for multiple screen types (overview, top CPU, top memory) Technical details: - Uses gopsutil for cross-platform system stats collection - Direct Linux framebuffer access via memory mapping - Anti-aliased text rendering with freetype - Configurable screen rotation and update intervals - Structured logging with slog - Comprehensive test coverage and linting setup This initial version provides a solid foundation for displaying rich system information on resource-constrained devices like Raspberry Pis.
225 lines
5.0 KiB
Go
225 lines
5.0 KiB
Go
package statcollector
|
|
|
|
import (
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
// 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
|
|
PacketsSent uint64
|
|
PacketsRecv uint64
|
|
}
|
|
|
|
// 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
|
|
lastNetStats map[string]psnet.IOCountersStat
|
|
lastCollectTime time.Time
|
|
}
|
|
|
|
// NewSystemCollector creates a new system collector
|
|
func NewSystemCollector(logger *slog.Logger) *SystemCollector {
|
|
return &SystemCollector{
|
|
logger: logger,
|
|
lastNetStats: make(map[string]psnet.IOCountersStat),
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
info.Uptime = time.Duration(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
|
|
interfaces, err := psnet.Interfaces()
|
|
if err != nil {
|
|
c.logger.Warn("getting network interfaces", "error", err)
|
|
} else {
|
|
ioCounters, _ := psnet.IOCounters(true)
|
|
ioMap := make(map[string]psnet.IOCountersStat)
|
|
for _, counter := range ioCounters {
|
|
ioMap[counter.Name] = counter
|
|
}
|
|
|
|
for _, iface := range interfaces {
|
|
if iface.Name == "lo" || strings.HasPrefix(iface.Name, "docker") {
|
|
continue
|
|
}
|
|
|
|
netInfo := NetworkInfo{
|
|
Name: iface.Name,
|
|
}
|
|
|
|
// Get IP addresses
|
|
for _, addr := range iface.Addrs {
|
|
netInfo.IPAddresses = append(netInfo.IPAddresses, addr.Addr)
|
|
}
|
|
|
|
// Get stats
|
|
if stats, ok := ioMap[iface.Name]; ok {
|
|
netInfo.BytesSent = stats.BytesSent
|
|
netInfo.BytesRecv = stats.BytesRecv
|
|
netInfo.PacketsSent = stats.PacketsSent
|
|
netInfo.PacketsRecv = stats.PacketsRecv
|
|
}
|
|
|
|
info.Network = append(info.Network, netInfo)
|
|
}
|
|
}
|
|
|
|
// Processes
|
|
processes, err := process.Processes()
|
|
if err != nil {
|
|
c.logger.Warn("getting processes", "error", err)
|
|
} else {
|
|
for _, p := range processes {
|
|
name, _ := p.Name()
|
|
cpuPercent, _ := p.CPUPercent()
|
|
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,
|
|
})
|
|
}
|
|
}
|
|
|
|
c.lastCollectTime = time.Now()
|
|
return info, nil
|
|
}
|