138 lines
3.3 KiB
Go
138 lines
3.3 KiB
Go
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())
|
|
}
|