package apt import ( "context" "errors" "fmt" "io/fs" "os" "os/exec" "strings" "time" "git.faercol.me/monitoring/sys-exporter/registry" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" ) const rebootRequiredPath = "/var/run/reboot-required" type AptCollector struct { installedPackages int pendingUpdates int rebootRequired int rebootRequiredPath string promExporter *prometheus.GaugeVec updateFreq time.Duration l *zap.SugaredLogger } func (c *AptCollector) Collect() interface{} { return nil } func (c *AptCollector) updateInstalledPackages() error { cmd := exec.Command("/usr/bin/apt", "list", "--installed") out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to run apt list: %w (%s)", err, string(out)) } // remove the unstable interface warning c.installedPackages = len(strings.Split(strings.TrimSpace(string(out)), "\n")) - 3 return nil } func (c *AptCollector) updatePendingUpdates() error { updateCmd := exec.Command("/usr/bin/apt", "update") out, err := updateCmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to run apt update: %w (%s)", err, string(out)) } listCmd := exec.Command("/usr/bin/apt", "list", "--upgradable") out, err = listCmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to run apt list: %w (%s)", err, string(out)) } // remove the unstable interface warning c.pendingUpdates = len(strings.Split(strings.TrimSpace(string(out)), "\n")) - 3 return nil } func (c *AptCollector) updateRebootRequired() error { _, err := os.Stat(c.rebootRequiredPath) if err != nil { if errors.Is(err, fs.ErrNotExist) { c.rebootRequired = 0 return nil } return fmt.Errorf("failed to check if reboot-required exists: %w", err) } c.rebootRequired = 1 return nil } func (c *AptCollector) update() error { if err := c.updateInstalledPackages(); err != nil { return fmt.Errorf("failed to update count of installed packages: %w", err) } if err := c.updatePendingUpdates(); err != nil { return fmt.Errorf("failed to update count of pending updates: %w", err) } if err := c.updateRebootRequired(); err != nil { return fmt.Errorf("failed to update reboot status: %w", err) } c.promExporter.WithLabelValues("total").Set(float64(c.installedPackages)) c.promExporter.WithLabelValues("updates").Set(float64(c.pendingUpdates)) c.promExporter.WithLabelValues("reboot_required").Set(float64(c.rebootRequired)) return nil } func (c *AptCollector) 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 *AptCollector) PromCollector() prometheus.Collector { return c.promExporter } func New() *AptCollector { return &AptCollector{ rebootRequiredPath: rebootRequiredPath, updateFreq: 30 * time.Minute, promExporter: prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "packages", Subsystem: "apt", Name: "packages_count", Help: "Count of apt packages"}, []string{"status"}), } } func init() { registry.R.MustRegisterCollector("packages.apt", New()) }