guenther/internal/collector/systemctl.go

140 lines
2.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package collector
import (
"bytes"
"context"
"log"
"os/exec"
"strings"
"sync"
"time"
"codeberg.org/pata1704/guenther/pkg/types"
)
// SystemctlCollector periodically checks the status of systemd services.
type SystemctlCollector struct {
services []string
interval time.Duration
outputChan chan<- types.ServiceStatus
healthChan chan<- types.StageHealth
wg sync.WaitGroup
mu sync.Mutex
processed uint64
}
// NewSystemctlCollector creates a new collector for the given services.
func NewSystemctlCollector(
services []string,
interval time.Duration,
output chan<- types.ServiceStatus,
health chan<- types.StageHealth,
) *SystemctlCollector {
return &SystemctlCollector{
services: services,
interval: interval,
outputChan: output,
healthChan: health,
}
}
// Start launches the collection loop.
func (c *SystemctlCollector) Start(ctx context.Context) {
if len(c.services) == 0 {
log.Println("systemctl: no services configured for monitoring")
return
}
c.wg.Go(func() {
ticker := time.NewTicker(c.interval)
reportTicker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
defer reportTicker.Stop()
// Immediate first collection.
c.collect()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
c.collect()
case <-reportTicker.C:
c.emitHealth()
}
}
})
}
// Wait waits for the collector to stop.
func (c *SystemctlCollector) Wait() {
c.wg.Wait()
}
func (c *SystemctlCollector) collect() {
for _, service := range c.services {
status, err := c.getServiceStatus(service)
if err != nil {
log.Printf("systemctl: error getting status for %s: %v", service, err)
continue
}
select {
case c.outputChan <- status:
c.mu.Lock()
c.processed++
c.mu.Unlock()
default:
log.Printf("systemctl: output channel full dropping status for %s", service)
}
}
}
func (c *SystemctlCollector) getServiceStatus(service string) (types.ServiceStatus, error) {
// Use systemctl show to get machine-readable properties.
cmd := exec.Command("systemctl", "show", "-p", "ActiveState,SubState", service)
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return types.ServiceStatus{}, err
}
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
status := types.ServiceStatus{
Timestamp: time.Now(),
ServiceName: service,
}
for _, line := range lines {
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
switch parts[0] {
case "ActiveState":
status.ActiveState = parts[1]
case "SubState":
status.SubState = parts[1]
}
}
return status, nil
}
func (c *SystemctlCollector) emitHealth() {
c.mu.Lock()
count := c.processed
c.mu.Unlock()
select {
case c.healthChan <- types.StageHealth{
StageName: "systemctl_collector",
EventsProcessed: count,
LastUpdate: time.Now(),
}:
default:
}
}