[Feature] Basic systemd service monitoring (#1153)

* basic systemd service monitoring

* update to work after /internal rename

* monitor systemd service cpu and memory usage

---------

Co-authored-by: henrygd <hank@henrygd.me>
This commit is contained in:
Shelby Tucker
2025-11-10 15:29:21 -05:00
committed by GitHub
parent ca58ff66ba
commit 40b3951615
16 changed files with 734 additions and 88 deletions

View File

@@ -31,6 +31,7 @@ type Agent struct {
netInterfaces map[string]struct{} // Stores all valid network interfaces
netIoStats system.NetIoStats // Keeps track of bandwidth usage
dockerManager *dockerManager // Manages Docker API requests
systemdManager *systemdManager // Manages systemd services
sensorConfig *SensorConfig // Sensors config
systemInfo system.Info // Host system info
gpuManager *GPUManager // Manages GPU data
@@ -88,6 +89,13 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
// initialize docker manager
agent.dockerManager = newDockerManager(agent)
// initialize systemd manager
if sm, err := newSystemdManager(); err != nil {
slog.Debug("Systemd", "err", err)
} else {
agent.systemdManager = sm
}
// initialize GPU manager
if gm, err := NewGPUManager(); err != nil {
slog.Debug("GPU", "err", err)
@@ -137,6 +145,11 @@ func (a *Agent) gatherStats(sessionID string) *system.CombinedData {
}
}
if a.systemdManager != nil {
data.SystemdServices = a.systemdManager.getServiceStats()
slog.Debug("Systemd services", "data", data.SystemdServices)
}
data.Stats.ExtraFs = make(map[string]*system.FsStats)
for name, stats := range a.fsStats {
if !stats.Root && stats.DiskTotal > 0 {

107
agent/systemd.go Normal file
View File

@@ -0,0 +1,107 @@
//go:build linux
package agent
import (
"context"
"log/slog"
"math"
"strings"
"sync"
"github.com/coreos/go-systemd/v22/dbus"
"github.com/henrygd/beszel/internal/entities/systemd"
)
// systemdManager manages the collection of systemd service statistics.
type systemdManager struct {
conn *dbus.Conn
serviceStatsMap map[string]*systemd.Service
mu sync.Mutex
}
// newSystemdManager creates a new systemdManager.
func newSystemdManager() (*systemdManager, error) {
conn, err := dbus.New()
if err != nil {
if strings.Contains(err.Error(), "permission denied") {
slog.Error("Permission denied when connecting to systemd. Run as root or with appropriate user permissions.", "err", err)
return nil, err
}
slog.Error("Error connecting to systemd", "err", err)
return nil, err
}
return &systemdManager{
conn: conn,
serviceStatsMap: make(map[string]*systemd.Service),
}, nil
}
// getServiceStats collects statistics for all running systemd services.
func (sm *systemdManager) getServiceStats() []*systemd.Service {
units, err := sm.conn.ListUnitsContext(context.Background())
if err != nil {
slog.Error("Error listing systemd units", "err", err)
return nil
}
var services []*systemd.Service
for _, unit := range units {
if strings.HasSuffix(unit.Name, ".service") {
service := sm.updateServiceStats(unit)
services = append(services, service)
}
}
return services
}
// updateServiceStats updates the statistics for a single systemd service.
func (sm *systemdManager) updateServiceStats(unit dbus.UnitStatus) *systemd.Service {
sm.mu.Lock()
defer sm.mu.Unlock()
props, err := sm.conn.GetUnitTypeProperties(unit.Name, "Service")
if err != nil {
slog.Debug("could not get unit type properties", "unit", unit.Name, "err", err)
return &systemd.Service{
Name: unit.Name,
Status: unit.ActiveState,
}
}
var cpuUsage uint64
if val, ok := props["CPUUsageNSec"]; ok {
if v, ok := val.(uint64); ok {
cpuUsage = v
}
}
var memUsage uint64
if val, ok := props["MemoryCurrent"]; ok {
if v, ok := val.(uint64); ok {
memUsage = v
}
}
service, exists := sm.serviceStatsMap[unit.Name]
if !exists {
service = &systemd.Service{
Name: unit.Name,
Status: unit.ActiveState,
}
sm.serviceStatsMap[unit.Name] = service
}
service.Status = unit.ActiveState
// If memUsage is MaxUint64 the api is saying it's not available, return 0
if memUsage == math.MaxUint64 {
memUsage = 0
}
service.Mem = float64(memUsage) / (1024 * 1024) // Convert to MB
service.CalculateCPUPercent(cpuUsage)
return service
}

View File

@@ -0,0 +1,18 @@
//go:build !linux
package agent
import "github.com/henrygd/beszel/internal/entities/systemd"
// systemdManager manages the collection of systemd service statistics.
type systemdManager struct{}
// newSystemdManager creates a new systemdManager.
func newSystemdManager() (*systemdManager, error) {
return &systemdManager{}, nil
}
// getServiceStats returns nil for non-linux systems.
func (sm *systemdManager) getServiceStats() []*systemd.Service {
return nil
}