2024-12-01 17:12:19 +00:00
package systemd
import (
"context"
"encoding/json"
"fmt"
"os/exec"
"strings"
"time"
"git.faercol.me/monitoring/sys-exporter/registry"
"github.com/prometheus/client_golang/prometheus"
2024-12-02 17:37:19 +00:00
"go.uber.org/zap"
2024-12-01 17:12:19 +00:00
)
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
2024-12-02 17:37:19 +00:00
l * zap . SugaredLogger
2024-12-01 17:12:19 +00:00
}
func ( c * SystemdCollector ) Collect ( ) interface { } {
return nil
}
func ( c * SystemdCollector ) getServicesStatus ( ) ( [ ] serviceStatus , error ) {
2024-12-07 17:13:41 +00:00
out , err := exec . Command ( "/usr/bin/systemctl" , "list-units" , "--output" , "json" ) . CombinedOutput ( )
2024-12-01 17:12:19 +00:00
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
}
2024-12-02 17:37:19 +00:00
func ( c * SystemdCollector ) Run ( ctx context . Context , l * zap . SugaredLogger ) {
c . l = l
c . l . Debug ( "Initializing collector" )
2024-12-01 17:12:19 +00:00
if err := c . update ( ) ; err != nil {
2024-12-02 17:37:19 +00:00
c . l . Errorf ( "Failed to init collector: %s\n" , err )
2024-12-01 17:12:19 +00:00
}
for {
select {
case <- ctx . Done ( ) :
2024-12-02 17:37:19 +00:00
c . l . Debug ( "Stopping collector" )
2024-12-01 17:12:19 +00:00
return
case <- time . After ( c . updateFreq ) :
2024-12-02 17:37:19 +00:00
c . l . Debug ( "Updating collector" )
2024-12-01 17:12:19 +00:00
if err := c . update ( ) ; err != nil {
2024-12-02 17:37:19 +00:00
c . l . Errorf ( "Failed to update collector: %s\n" , err )
2024-12-01 17:12:19 +00:00
}
}
}
}
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 ( ) )
}