package sysinfo import ( "context" "errors" "fmt" "os" "regexp" "strings" "time" "git.faercol.me/monitoring/sys-exporter/registry" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" ) var versionRegexp = regexp.MustCompile(`(?m)^(\w+) version ([^ ]+) .*$`) const ( hosnameFile = "/etc/hostname" osRealeaseFile = "/etc/os-release" versionFile = "/proc/version" machineIDFile = "/etc/machine-id" ) var labels = []string{ "hostname", "kernel", "os", "distro", "distro_version", "machine_id", } type SysinfoCollector struct { hostname string kernelVersion string os string distro string distroVersion string machineID string hostnameFile string osReleaseFile string versionFile string machineIDFile string promExporter *prometheus.GaugeVec updateFreq time.Duration l *zap.SugaredLogger } func (c *SysinfoCollector) Collect() interface{} { return nil } func (c *SysinfoCollector) labels() prometheus.Labels { return prometheus.Labels{ "hostname": c.hostname, "kernel": c.kernelVersion, "os": c.os, "distro": c.distro, "distro_version": c.distroVersion, "machine_id": c.machineID, } } func (c *SysinfoCollector) updateHostname() error { rawHostname, err := os.ReadFile(c.hostnameFile) if err != nil { return err } c.hostname = strings.TrimSpace(string(rawHostname)) return nil } func (c *SysinfoCollector) updateKernelVersion() error { rawVersion, err := os.ReadFile(c.versionFile) if err != nil { return err } if matches := versionRegexp.FindAllStringSubmatch(string(rawVersion), -1); matches == nil { return errors.New("failed to match /proc/version") } else { c.os = matches[0][1] c.kernelVersion = matches[0][2] } return nil } func (c *SysinfoCollector) updateDistro() error { rawContent, err := os.ReadFile(c.osReleaseFile) if err != nil { return err } releaseData, err := parseOSRelease(rawContent) if err != nil { return fmt.Errorf("failed to parse /etc/os-release file: %w", err) } switch releaseData["ID"] { case "arch": c.distro = "Arch Linux" c.distroVersion = releaseData["BUILD_ID"] case "debian": c.distro = "Debian GNU/Linux" c.distroVersion = strings.ReplaceAll(releaseData["VERSION"], `"`, "") default: c.distro = releaseData["NAME"] } return nil } func (c *SysinfoCollector) updateMachineID() error { machineIDContent, err := os.ReadFile(c.machineIDFile) if err != nil { return err } c.machineID = strings.TrimSpace(string(machineIDContent)) return nil } func (c *SysinfoCollector) update() error { if err := c.updateKernelVersion(); err != nil { return fmt.Errorf("failed to get kernel version: %w", err) } if err := c.updateHostname(); err != nil { return fmt.Errorf("failed to get hostname: %w", err) } if err := c.updateDistro(); err != nil { return fmt.Errorf("failed to get distro info: %w", err) } if err := c.updateMachineID(); err != nil { return fmt.Errorf("failed to get machine-id: %w", err) } c.promExporter.With(c.labels()).Set(1) return nil } func (c *SysinfoCollector) 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 *SysinfoCollector) PromCollector() prometheus.Collector { return c.promExporter } func New() *SysinfoCollector { return &SysinfoCollector{ updateFreq: 30 * time.Minute, hostnameFile: hosnameFile, osReleaseFile: osRealeaseFile, versionFile: versionFile, machineIDFile: machineIDFile, promExporter: prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "system", Name: "info", Help: "System global information"}, labels), } } func init() { registry.R.MustRegisterCollector("system.info", New()) }