Add some new collectors

This commit is contained in:
Melora Hugues 2024-12-01 18:12:19 +01:00
parent c48f1e0f66
commit 9e5450475e
6 changed files with 409 additions and 0 deletions

View file

@ -8,7 +8,10 @@ import (
"git.faercol.me/monitoring/sys-exporter/registry"
"github.com/prometheus/client_golang/prometheus/promhttp"
_ "git.faercol.me/monitoring/sys-exporter/collector/loadavg"
_ "git.faercol.me/monitoring/sys-exporter/collector/meminfo"
_ "git.faercol.me/monitoring/sys-exporter/collector/pacman"
_ "git.faercol.me/monitoring/sys-exporter/collector/systemd"
_ "git.faercol.me/monitoring/sys-exporter/collector/uptime"
)

View file

@ -0,0 +1,86 @@
package loadavg
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"time"
"git.faercol.me/monitoring/sys-exporter/registry"
"github.com/prometheus/client_golang/prometheus"
)
const loadAvgFileLocation = "/proc/loadavg"
type LoadAvgCollector struct {
loadAvgFileLocation string
promExporter *prometheus.SummaryVec
updateFreq time.Duration
}
func (c *LoadAvgCollector) Collect() interface{} {
return nil
}
func (c *LoadAvgCollector) update() error {
fileContent, err := os.ReadFile(c.loadAvgFileLocation)
if err != nil {
return fmt.Errorf("failed to read loadavg file: %w", err)
}
vals := strings.Split(string(fileContent), " ")
if len(vals) != 5 {
return fmt.Errorf("invalid format %q for loadavg file", string(fileContent))
}
var load1Min, load5Min, load15Min float64
if load1Min, err = strconv.ParseFloat(vals[0], 64); err != nil {
return fmt.Errorf("invalid value %q for load 1min: %w", vals[0], err)
}
if load5Min, err = strconv.ParseFloat(vals[1], 64); err != nil {
return fmt.Errorf("invalid value %q for load 5mins: %w", vals[1], err)
}
if load15Min, err = strconv.ParseFloat(vals[2], 64); err != nil {
return fmt.Errorf("invalid value %q for load 15min: %w", vals[2], err)
}
c.promExporter.WithLabelValues("1min").Observe(load1Min)
c.promExporter.WithLabelValues("5mins").Observe(load5Min)
c.promExporter.WithLabelValues("15mins").Observe(load15Min)
return nil
}
func (c *LoadAvgCollector) Run(ctx context.Context) {
if err := c.update(); err != nil {
fmt.Printf("Failed to init loadavg collector: %s\n", err)
}
for {
select {
case <-ctx.Done():
return
case <-time.After(c.updateFreq):
if err := c.update(); err != nil {
fmt.Printf("Failed to update loadavg collector: %s\n", err)
}
}
}
}
func (c *LoadAvgCollector) PromCollector() prometheus.Collector {
return c.promExporter
}
func New() *LoadAvgCollector {
return &LoadAvgCollector{
updateFreq: 1 * time.Second,
loadAvgFileLocation: loadAvgFileLocation,
promExporter: prometheus.NewSummaryVec(prometheus.SummaryOpts{Namespace: "system", Name: "loadavg", Help: "Load average of the system"}, []string{"period"}),
}
}
func init() {
registry.R.MustRegisterCollector("system.loadavg", New())
}

View file

@ -0,0 +1,114 @@
package meminfo
import (
"context"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"
"git.faercol.me/monitoring/sys-exporter/registry"
"git.faercol.me/monitoring/sys-exporter/utils"
"github.com/prometheus/client_golang/prometheus"
)
const meminfoFileLocation = "/proc/meminfo"
var meminfoRegex = regexp.MustCompile(`(?m)^(?P<key>\S+):\s+(?P<value>\d+(?:\s\S+$|$))`)
func valueInBytes(rawVal string) (int, error) {
split := strings.Split(rawVal, " ")
baseVal, err := strconv.ParseInt(split[0], 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid base value %q: %w", split[0], err)
}
if len(split) == 1 { // no unit present in the value
return int(baseVal), nil
}
switch split[1] {
case "B":
return int(baseVal), nil
case "kB":
return 1000 * int(baseVal), nil
case "MB":
return 1_000_000 * int(baseVal), nil
case "GB":
return 1_000_000_000 * int(baseVal), nil
case "TB":
return 1_000_000_000_000 * int(baseVal), nil
default:
return 0, fmt.Errorf("invalid unit %q", split[1])
}
}
type MeminfoCollector struct {
meminfoFileLocation string
promExporter *prometheus.SummaryVec
updateFreq time.Duration
}
func (c *MeminfoCollector) Collect() interface{} {
return nil
}
func (c *MeminfoCollector) update() error {
fileContent, err := os.ReadFile(c.meminfoFileLocation)
if err != nil {
return fmt.Errorf("failed to read meminfo file: %w", err)
}
rawMatches, err := utils.HandleRegexp(string(fileContent), *meminfoRegex)
if err != nil {
return fmt.Errorf("failed to parse meminfo file: %w", err)
}
mappedVals := utils.HandleKeyValRegexRes(rawMatches)
for valueType, rawVal := range mappedVals {
val, err := valueInBytes(rawVal)
if err != nil {
return fmt.Errorf("invalid value in meminfo file: %w", err)
}
c.promExporter.WithLabelValues(valueType).Observe(float64(val))
}
return nil
}
func (c *MeminfoCollector) Run(ctx context.Context) {
if err := c.update(); err != nil {
fmt.Printf("Failed to init meminfo collector: %s\n", err)
}
for {
select {
case <-ctx.Done():
return
case <-time.After(c.updateFreq):
if err := c.update(); err != nil {
fmt.Printf("Failed to update meminfo collector: %s\n", err)
}
}
}
}
func (c *MeminfoCollector) PromCollector() prometheus.Collector {
return c.promExporter
}
func New() *MeminfoCollector {
return &MeminfoCollector{
updateFreq: 1 * time.Second,
meminfoFileLocation: meminfoFileLocation,
promExporter: prometheus.NewSummaryVec(prometheus.SummaryOpts{Namespace: "system", Name: "meminfo", Help: "System memory info"}, []string{"type"}),
}
}
func init() {
registry.R.MustRegisterCollector("system.meminfo", New())
}

View file

@ -20,6 +20,9 @@ func listCommandLines(command string, args ...string) (int, error) {
cmd := exec.Command(command, args...)
out, err := cmd.CombinedOutput()
if err != nil {
if string(out) == "" { // pacman can return a returncode 1 when the list is empty
return 0, nil
}
return 0, fmt.Errorf("failed to run command: %w (%s)", err, string(out))
}

View file

@ -0,0 +1,130 @@
package systemd
import (
"context"
"encoding/json"
"fmt"
"os/exec"
"strings"
"time"
"git.faercol.me/monitoring/sys-exporter/registry"
"github.com/prometheus/client_golang/prometheus"
)
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
}
func (c *SystemdCollector) Collect() interface{} {
return nil
}
func (c *SystemdCollector) getServicesStatus() ([]serviceStatus, error) {
out, err := exec.Command("/sbin/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) {
if err := c.update(); err != nil {
fmt.Printf("Failed to init systemd status: %s\n", err)
}
for {
select {
case <-ctx.Done():
return
case <-time.After(c.updateFreq):
if err := c.update(); err != nil {
fmt.Printf("Failed to update systemd status: %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())
}

73
utils/utils.go Normal file
View file

@ -0,0 +1,73 @@
package utils
import (
"errors"
"regexp"
)
// HandleRegexp handles a regex with named capture groups.
// If the given regexp does not have any named capture group, or if no match is found, an error is returned
// Matches are returned as a map which keys are the capture groups names
func HandleRegexp(dat string, pattern regexp.Regexp) (res []map[string]string, err error) {
namedGroups := 0
for _, n := range pattern.SubexpNames() {
if n != "" {
namedGroups++
}
}
if namedGroups == 0 {
return nil, errors.New("no named subgroup in pattern")
}
matches := pattern.FindAllStringSubmatch(dat, -1)
if matches == nil {
return nil, errors.New("no match found")
}
res = make([]map[string]string, 0)
for _, line := range matches {
subMatchMap := make(map[string]string)
for i, name := range pattern.SubexpNames() {
if i != 0 {
subMatchMap[name] = string(line[i])
}
}
res = append(res, subMatchMap)
}
return
}
// HandleKeyValRegexRes merges a list of maps containing result as a key/value format
//
// Input maps all have the following format:
//
// {
// "key": "some_key",
// "value": "some_val",
// }
//
// Final result will have the format
//
// {
// "some_key": "some_val",
// }
func HandleKeyValRegexRes(rawMatches []map[string]string) map[string]string {
matchedMap := make(map[string]string)
for _, m := range rawMatches {
currKey := ""
currVal := ""
for k, v := range m {
if k == "key" {
currKey = v
} else if k == "value" {
currVal = v
}
// All other keys are ignored
// Should not happen given the given regexp
}
if currKey != "" && currVal != "" {
matchedMap[currKey] = currVal
}
}
return matchedMap
}