]> ruderich.org/simon Gitweb - monitoring/check_dmesg.git/commitdiff
First working version
authorSimon Ruderich <simon@ruderich.org>
Sun, 8 Feb 2026 08:49:32 +0000 (09:49 +0100)
committerSimon Ruderich <simon@ruderich.org>
Sun, 8 Feb 2026 08:49:32 +0000 (09:49 +0100)
.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
go.mod [new file with mode: 0644]
go.sum [new file with mode: 0644]
main.go [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..9a4bb0f
--- /dev/null
@@ -0,0 +1 @@
+/check_dmesg
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..d6c8acf
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,7 @@
+all:
+       go build
+       go test -v
+       go fmt
+       go vet
+
+.PHONY: all
diff --git a/go.mod b/go.mod
new file mode 100644 (file)
index 0000000..9e4fee4
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module check_dmesg
+
+go 1.24.0
+
+require github.com/google/renameio/v2 v2.0.0
diff --git a/go.sum b/go.sum
new file mode 100644 (file)
index 0000000..0699119
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,2 @@
+github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
+github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
diff --git a/main.go b/main.go
new file mode 100644 (file)
index 0000000..697d2d8
--- /dev/null
+++ b/main.go
@@ -0,0 +1,252 @@
+// check_dmesg: monitor dmesg for error messages
+
+// SPDX-License-Identifier: GPL-3.0-or-later
+// Copyright (C) 2026  Simon Ruderich
+
+package main
+
+import (
+       "bufio"
+       "encoding/json"
+       "flag"
+       "fmt"
+       "io"
+       "log"
+       "os"
+       "os/exec"
+       "path/filepath"
+       "regexp"
+       "time"
+
+       "github.com/google/renameio/v2"
+)
+
+// Exit code convention for Nagios
+const (
+       OK   = 0
+       WARN = 1
+       CRIT = 2
+)
+
+type State struct {
+       Cursor string `json:"cursor"`
+       // Must store messages as journalctl might rotate them
+       Messages []Message `json:"messages"`
+}
+
+type Message struct {
+       Time     time.Time
+       Priority int
+       Message  string
+}
+
+func main() {
+       statePath := flag.String("state", "", "path to state file")
+       prio := flag.Int("prio", 3, "show messages with equal or higher priority")
+       clear := flag.Bool("clear", false, "clear known error messages in state")
+
+       flag.Usage = func() {
+               fmt.Fprintf(os.Stderr, "usage: %s [options] <exclude-regexp..>\n\n",
+                       os.Args[0])
+               flag.PrintDefaults()
+       }
+       flag.Parse()
+
+       var excludes []*regexp.Regexp
+       for _, x := range flag.Args() {
+               r, err := regexp.Compile(x)
+               if err != nil {
+                       log.Print(err)
+                       os.Exit(CRIT)
+               }
+               excludes = append(excludes, r)
+       }
+
+       var state *State
+       if *statePath == "" {
+               state = &State{}
+       } else {
+               x, err := LoadState(*statePath)
+               if err != nil {
+                       log.Print(err)
+                       os.Exit(CRIT)
+               }
+               state = x
+       }
+
+       // Forget all known messages
+       if *clear {
+               state.Messages = nil
+       }
+
+       xs, cursor, err := ReadDmesg(state.Cursor)
+       if err != nil {
+               log.Print(err)
+               os.Exit(CRIT)
+       }
+       xs = append(state.Messages, xs...)
+       state.Cursor = cursor
+
+       exit := OK
+       state.Messages = nil
+msg:
+       for _, x := range xs {
+               if x.Priority > *prio {
+                       continue
+               }
+               for _, r := range excludes {
+                       if r.MatchString(x.Message) {
+                               continue msg
+                       }
+               }
+
+               fmt.Printf("%s/%s: %s\n",
+                       FmtPriority(x.Priority), x.Time.Format(time.RFC3339),
+                       x.Message)
+               exit = CRIT
+               state.Messages = append(state.Messages, x)
+       }
+
+       if *statePath != "" {
+               err = SaveState(*statePath, state)
+               if err != nil {
+                       log.Print(err)
+                       os.Exit(CRIT)
+               }
+       }
+
+       os.Exit(exit)
+}
+
+func ReadDmesg(afterCursor string) ([]Message, string, error) {
+       cmd := exec.Command("journalctl",
+               "--dmesg",
+               "--output=json",
+       )
+       if afterCursor == "" {
+               // Initial run: only read messages since last boot
+               cmd.Args = append(cmd.Args, "--boot")
+       } else {
+               // Later run: read messages also over boots so no messages are lost
+               cmd.Args = append(cmd.Args, "--after-cursor="+afterCursor)
+       }
+       out, err := cmd.StdoutPipe()
+       if err != nil {
+               return nil, "", err
+       }
+       err = cmd.Start()
+       if err != nil {
+               return nil, "", err
+       }
+
+       var res []Message
+       var cursor string
+
+       r := bufio.NewReader(out)
+       for {
+               line, err := r.ReadBytes('\n')
+               if err != nil {
+                       if err == io.EOF {
+                               break
+                       }
+                       return nil, "", err
+               }
+
+               var x struct {
+                       Cursor    string `json:"__CURSOR"`
+                       Timestamp int64  `json:"__REALTIME_TIMESTAMP,string"`
+                       Priority  int    `json:"PRIORITY,string"`
+                       Message   string `json:"MESSAGE"`
+               }
+               err = json.Unmarshal(line, &x)
+               if err != nil {
+                       return nil, "", err
+               }
+
+               cursor = x.Cursor
+               res = append(res, Message{
+                       Time:     time.UnixMicro(x.Timestamp),
+                       Priority: x.Priority,
+                       Message:  x.Message,
+               })
+       }
+
+       err = cmd.Wait()
+       if err != nil {
+               return nil, "", err
+       }
+
+       // No new (unfiltered) messages since last run, re-use cursor
+       if cursor == "" {
+               cursor = afterCursor
+       }
+       return res, cursor, nil
+}
+
+func FmtPriority(priority int) string {
+       switch priority {
+       case 0:
+               return "emerg"
+       case 1:
+               return "alert"
+       case 2:
+               return "crit"
+       case 3:
+               return "err"
+       case 4:
+               return "warning"
+       case 5:
+               return "notice"
+       case 6:
+               return "info"
+       case 7:
+               return "debug"
+       default:
+               return ""
+       }
+}
+
+func LoadState(path string) (*State, error) {
+       x, err := os.ReadFile(path)
+       if err != nil {
+               if os.IsNotExist(err) {
+                       return &State{}, nil
+               }
+               return nil, err
+       }
+       var res State
+       err = json.Unmarshal(x, &res)
+       if err != nil {
+               return nil, err
+       }
+       return &res, nil
+}
+
+func SaveState(path string, state *State) error {
+       x, err := json.Marshal(state)
+       if err != nil {
+               return err
+       }
+
+       // Atomically replace the state file
+       f, err := renameio.TempFile(filepath.Dir(path), path)
+       if err != nil {
+               return err
+       }
+       defer f.Cleanup()
+       _, err = f.Write(x)
+       if err != nil {
+               return err
+       }
+       err = f.CloseAtomicallyReplace()
+       if err != nil {
+               return err
+       }
+
+       // NOTE: Durabilty requires an fsync on the directory. Not important here
+       // (next call will just refetch data) so skip it.
+
+       return nil
+}
+
+// vi: set noet ts=4 sw=4 sts=4: