From e564284be85a61bd9fc69ded9ff86410ee0b5ed3 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sun, 8 Feb 2026 09:49:32 +0100 Subject: [PATCH] First working version --- .gitignore | 1 + Makefile | 7 ++ go.mod | 5 ++ go.sum | 2 + main.go | 252 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 267 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a4bb0f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/check_dmesg diff --git a/Makefile b/Makefile new file mode 100644 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 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 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 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] \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: -- 2.52.0