package pacman import ( "context" "fmt" "os/exec" "strings" "time" "git.faercol.me/monitoring/sys-exporter/registry" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" ) 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 { 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)) } return len(strings.Split(string(out), "\n")), nil } type PacmanCollector struct { installedPackages int promPackages *prometheus.GaugeVec pendingUpdates int updateFreq time.Duration l *zap.SugaredLogger } 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, l *zap.SugaredLogger) { c.l = l c.l.Debug("Initializing collector") if err := c.updateInstalledPackages(); err != nil { c.l.Errorf("Failed to init count of installed packages: %s\n", err) } if err := c.updatePendingUpdates(); err != nil { c.l.Errorf("Failed to init count of updates: %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.updateInstalledPackages(); err != nil { c.l.Errorf("Failed to update count of installed packages: %s\n", err) } if err := c.updatePendingUpdates(); err != nil { c.l.Errorf("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()) }