diff --git a/cmd/server/main.go b/cmd/server/main.go index 1a3c4c6..6b8ff0b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,7 +1,30 @@ package main -import "fmt" +import ( + "context" + "fmt" + "net/http" + + "git.faercol.me/monitoring/sys-exporter/registry" + "github.com/prometheus/client_golang/prometheus/promhttp" + + _ "git.faercol.me/monitoring/sys-exporter/collector/pacman" + _ "git.faercol.me/monitoring/sys-exporter/collector/uptime" +) + +// type gatherer struct { +// reg *prometheus.Registry +// } + +// func (g *gatherer) Gather() (pcm.MetricFamily, error) { +// return g.reg. +// } func main() { fmt.Println("starting server") + + registry.R.Run(context.Background()) + + http.Handle("/metrics", promhttp.HandlerFor(registry.R.PromRegistry(), promhttp.HandlerOpts{})) + http.ListenAndServe(":2112", nil) } diff --git a/collector/collector.go b/collector/collector.go new file mode 100644 index 0000000..0df1698 --- /dev/null +++ b/collector/collector.go @@ -0,0 +1,13 @@ +package collector + +import ( + "context" + + "github.com/prometheus/client_golang/prometheus" +) + +type Collector interface { + Collect() interface{} + PromCollector() prometheus.Collector + Run(ctx context.Context) +} diff --git a/collector/pacman/pacman.go b/collector/pacman/pacman.go new file mode 100644 index 0000000..ab2b8c6 --- /dev/null +++ b/collector/pacman/pacman.go @@ -0,0 +1,108 @@ +package pacman + +import ( + "context" + "fmt" + "os/exec" + "strings" + "time" + + "git.faercol.me/monitoring/sys-exporter/registry" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + totalLabel = "total" + updatesLabel = "updates" +) + +func listCommandLines(command string, args ...string) (int, error) { + cmd := exec.Command(command, args...) + out, err := cmd.CombinedOutput() + if err != nil { + return 0, fmt.Errorf("failed to run command: %w (%s)", err, string(out)) + } + + return len(strings.Split(string(out), "\n")), nil +} + +type PacmanCollector struct { + installedPackages int + promPackages *prometheus.GaugeVec + pendingUpdates int + updateFreq time.Duration +} + +func (c *PacmanCollector) Collect() interface{} { + return map[string]int{ + "installed_packages": c.installedPackages, + "pending_updates": c.pendingUpdates, + } +} + +func (c *PacmanCollector) updateInstalledPackages() error { + count, err := listCommandLines("/sbin/pacman", "-Q") + if err != nil { + return fmt.Errorf("failed to count installed packages: %w", err) + } + + c.installedPackages = count + c.promPackages.WithLabelValues(totalLabel).Set(float64(count)) + return nil +} + +func (c *PacmanCollector) updatePendingUpdates() error { + updateCmd := exec.Command("/sbin/pacman", "-Sy") + if out, err := updateCmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to update pacman cache: %w (%s)", err, string(out)) + } + + count, err := listCommandLines("/sbin/pacman", "-Qu") + if err != nil { + return fmt.Errorf("failed to count updated packages: %w", err) + } + + c.pendingUpdates = count + c.promPackages.WithLabelValues(updatesLabel).Set(float64(count)) + return nil +} + +func (c *PacmanCollector) Run(ctx context.Context) { + if err := c.updateInstalledPackages(); err != nil { + fmt.Printf("Failed to init count of installed packages: %s\n", err) + } + if err := c.updatePendingUpdates(); err != nil { + fmt.Printf("Failed to init count of updates: %s\n", err) + } + + for { + select { + case <-ctx.Done(): + return + case <-time.After(c.updateFreq): + if err := c.updateInstalledPackages(); err != nil { + fmt.Printf("Failed to update count of installed packages: %s\n", err) + } + if err := c.updatePendingUpdates(); err != nil { + fmt.Printf("Failed to update count of updates: %s\n", err) + } + } + } +} + +func (c *PacmanCollector) PromCollector() prometheus.Collector { + return c.promPackages +} + +func New() *PacmanCollector { + c := PacmanCollector{ + updateFreq: 5 * time.Minute, + promPackages: prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "packages", Subsystem: "pacman", Name: "packages_count", Help: "Count of pacman packages"}, []string{"status"}), + } + + return &c +} + +func init() { + registry.R.MustRegisterCollector("packages.pacman", New()) +} diff --git a/collector/uptime/uptime.go b/collector/uptime/uptime.go new file mode 100644 index 0000000..967637f --- /dev/null +++ b/collector/uptime/uptime.go @@ -0,0 +1,84 @@ +package uptime + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + "time" + + "git.faercol.me/monitoring/sys-exporter/registry" + "github.com/prometheus/client_golang/prometheus" +) + +const procUptimeLocation = "/proc/uptime" + +type UptimeCollector struct { + ctx context.Context + procFileLocation string + uptimeFreq time.Duration + uptime time.Duration + uptimeProm prometheus.Gauge +} + +func (c *UptimeCollector) Collect() interface{} { + return c.uptime +} + +func (c *UptimeCollector) update() error { + fileContent, err := os.ReadFile(c.procFileLocation) + if err != nil { + return fmt.Errorf("failed to read uptime file: %w", err) + } + + uptimeVals := strings.Split(string(fileContent), " ") + if len(uptimeVals) != 2 { + return fmt.Errorf("invalid format for /proc/uptime: %q", string(fileContent)) + } + + uptimeFloat, err := strconv.ParseFloat(uptimeVals[0], 64) + if err != nil { + return fmt.Errorf("invalid uptime format for float %s: %w", uptimeVals[0], err) + } + + c.uptime = time.Duration(int(uptimeFloat)) * time.Second + c.uptimeProm.Set(uptimeFloat) + return nil +} + +func (c *UptimeCollector) PromCollector() prometheus.Collector { + return c.uptimeProm +} + +func (c *UptimeCollector) Run(ctx context.Context) { + if err := c.update(); err != nil { + fmt.Printf("Failed to init collector: %s\n", err) + } + + for { + select { + case <-ctx.Done(): + return + case <-time.After(c.uptimeFreq): + if err := c.update(); err != nil { + fmt.Printf("Failed to update collector: %s\n", err) + } + } + } +} + +func New() *UptimeCollector { + return &UptimeCollector{ + ctx: context.TODO(), + procFileLocation: procUptimeLocation, + uptimeFreq: 1 * time.Second, + uptime: 0, + uptimeProm: prometheus.NewGauge(prometheus.GaugeOpts{Namespace: "system", Name: "uptime_nanosec", Help: "System uptime in nanoseconds"}), + } + +} + +func init() { + registry.R.MustRegisterCollector("system.uptime", New()) +} diff --git a/go.mod b/go.mod index 8652463..a64dbd0 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,17 @@ module git.faercol.me/monitoring/sys-exporter go 1.23.3 + +require github.com/prometheus/client_golang v1.20.5 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + golang.org/x/sys v0.22.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d5318cf --- /dev/null +++ b/go.sum @@ -0,0 +1,24 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/registry/registry.go b/registry/registry.go new file mode 100644 index 0000000..d205444 --- /dev/null +++ b/registry/registry.go @@ -0,0 +1,42 @@ +package registry + +import ( + "context" + + "git.faercol.me/monitoring/sys-exporter/collector" + "github.com/prometheus/client_golang/prometheus" +) + +var R CollectorRegistry + +type CollectorRegistry struct { + promRegistry *prometheus.Registry + collectors map[string]collector.Collector +} + +func (r *CollectorRegistry) RegisterCollector(name string, c collector.Collector) error { + r.collectors[name] = c + return r.promRegistry.Register(c.PromCollector()) +} + +func (r *CollectorRegistry) MustRegisterCollector(name string, c collector.Collector) { + r.collectors[name] = c + r.promRegistry.MustRegister(c.PromCollector()) +} + +func (r *CollectorRegistry) PromRegistry() *prometheus.Registry { + return r.promRegistry +} + +func (r *CollectorRegistry) Run(ctx context.Context) { + for _, c := range r.collectors { + go c.Run(ctx) + } +} + +func init() { + R = CollectorRegistry{ + promRegistry: prometheus.NewRegistry(), + collectors: make(map[string]collector.Collector), + } +}