140 lines
2.8 KiB
Go
140 lines
2.8 KiB
Go
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:
|
||
}
|
||
}
|