--- /dev/null
+// 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: