diff --git a/cmd/server/main.go b/cmd/server/main.go index 98c2eaf..7a4da62 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -14,6 +14,7 @@ import ( _ "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/sysinfo" _ "git.faercol.me/monitoring/sys-exporter/collector/systemd" _ "git.faercol.me/monitoring/sys-exporter/collector/uptime" ) diff --git a/collector/sysinfo/sysinfo.go b/collector/sysinfo/sysinfo.go new file mode 100644 index 0000000..3836c47 --- /dev/null +++ b/collector/sysinfo/sysinfo.go @@ -0,0 +1,167 @@ +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" +) + +var labels = []string{ + "hostname", + "kernel", + "os", + "distro", + "distro_version", +} + +type SysinfoCollector struct { + hostname string + kernelVersion string + os string + distro string + distroVersion string + + hostnameFile string + osReleaseFile string + versionFile 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, + } +} + +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 = releaseData["VERSION"] + default: + c.distro = releaseData["NAME"] + } + + 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) + } + + 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, + promExporter: prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: "system", Name: "info", Help: "System global information"}, labels), + } +} + +func init() { + registry.R.MustRegisterCollector("system.info", New()) +} diff --git a/collector/sysinfo/utils.go b/collector/sysinfo/utils.go new file mode 100644 index 0000000..a31a4f8 --- /dev/null +++ b/collector/sysinfo/utils.go @@ -0,0 +1,29 @@ +package sysinfo + +import ( + "fmt" + "strings" +) + +func parseOSRelease(content []byte) (map[string]string, error) { + res := make(map[string]string) + + for _, line := range strings.Split(string(content), "\n") { + if line == "" { + continue + } + + lineVals := strings.SplitN(line, "=", 2) + if len(lineVals) != 2 { + return nil, fmt.Errorf("impossible to split line %q", line) + } + + if _, ok := res[lineVals[0]]; ok { + return nil, fmt.Errorf("duplicate key %q", lineVals[0]) + } + + res[lineVals[0]] = lineVals[1] + } + + return res, nil +} diff --git a/debian/sys-exporter/etc/sys-exporter/config.yml b/debian/sys-exporter/etc/sys-exporter/config.yml index 724e37d..d96019e 100644 --- a/debian/sys-exporter/etc/sys-exporter/config.yml +++ b/debian/sys-exporter/etc/sys-exporter/config.yml @@ -5,3 +5,4 @@ collectors: - system.meminfo - services.systemd - system.uptime + - system.info