package systemd import ( "context" "encoding/json" "fmt" "os/exec" "strings" "time" "git.faercol.me/monitoring/sys-exporter/registry" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" ) type serviceStatus struct { Unit string `json:"unit"` Load string `json:"load"` Active string `json:"active"` Sub string `json:"sub"` Description string `json:"description"` } func (s serviceStatus) unitType() string { split := strings.Split(s.Unit, ".") if len(split) < 2 { return "" } return split[len(split)-1] } type SystemdCollector struct { promExporter *prometheus.GaugeVec updateFreq time.Duration l *zap.SugaredLogger } func (c *SystemdCollector) Collect() interface{} { return nil } func (c *SystemdCollector) getServicesStatus() ([]serviceStatus, error) { out, err := exec.Command("/usr/bin/systemctl", "list-units", "--output", "json").CombinedOutput() if err != nil { return nil, fmt.Errorf("failed to run systemd command: %w", err) } res := []serviceStatus{} if err := json.Unmarshal(out, &res); err != nil { return nil, fmt.Errorf("failed to parse systemd output: %w", err) } return res, nil } func (c *SystemdCollector) update() error { systemd_output, err := c.getServicesStatus() if err != nil { return fmt.Errorf("failed to get services status: %w", err) } mappingPerType := map[string][]serviceStatus{} for _, s := range systemd_output { mappingPerType[s.unitType()] = append(mappingPerType[s.unitType()], s) } totalUnits := 0 runningUnits := 0 failedUnits := 0 for unitType, units := range mappingPerType { unitsForType := 0 healthyForType := 0 failedForType := 0 for _, u := range units { unitsForType++ totalUnits++ if u.Active == "active" { runningUnits++ healthyForType++ } if u.Active == "failed" { failedUnits++ failedForType++ } } c.promExporter.WithLabelValues(unitType, "total").Set(float64(unitsForType)) c.promExporter.WithLabelValues(unitType, "running").Set(float64(healthyForType)) c.promExporter.WithLabelValues(unitType, "failed").Set(float64(failedForType)) } c.promExporter.WithLabelValues("total", "total").Set(float64(totalUnits)) c.promExporter.WithLabelValues("total", "running").Set(float64(runningUnits)) c.promExporter.WithLabelValues("total", "failed").Set(float64(failedUnits)) return nil } func (c *SystemdCollector) Run(ctx context.Context, l *zap.SugaredLogger) { c.l = l c.l.Debug("Initializing collector") if err := c.update(); err != nil { c.l.Errorf("Failed to init collector: %s\n", err) } for { select { case <-ctx.Done(): c.l.Debug("Stopping collector") return case <-time.After(c.updateFreq): c.l.Debug("Updating collector") if err := c.update(); err != nil { c.l.Errorf("Failed to update collector: %s\n", err) } } } } func (c *SystemdCollector) PromCollector() prometheus.Collector { return c.promExporter } func New() *SystemdCollector { return &SystemdCollector{ updateFreq: 30 * time.Second, promExporter: prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "services", Subsystem: "systemd", Name: "running_units", Help: "Count of running services for systemd"}, []string{"type", "status"}), } } func init() { registry.R.MustRegisterCollector("services.systemd", New()) }