From f05c4e1fcec6576d5981c3d3c9fed7fd03935157 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Mon, 17 Aug 2026 23:06:08 +0200 Subject: [PATCH] f-29: watchdog: add notify #29 Signed-off-by: GnomeZworc --- internal/watchdog/notify/notify.go | 5 + internal/watchdog/notify/stderr.go | 24 +++++ internal/watchdog/notify/stderr_test.go | 120 ++++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 internal/watchdog/notify/notify.go create mode 100644 internal/watchdog/notify/stderr.go create mode 100644 internal/watchdog/notify/stderr_test.go diff --git a/internal/watchdog/notify/notify.go b/internal/watchdog/notify/notify.go new file mode 100644 index 0000000..fbf75e2 --- /dev/null +++ b/internal/watchdog/notify/notify.go @@ -0,0 +1,5 @@ +package notify + +type Notifier interface { + Notify(kind, name, problem string) +} diff --git a/internal/watchdog/notify/stderr.go b/internal/watchdog/notify/stderr.go new file mode 100644 index 0000000..5436b2c --- /dev/null +++ b/internal/watchdog/notify/stderr.go @@ -0,0 +1,24 @@ +package notify + +import "log/slog" + +var _ Notifier = (*StderrNotifier)(nil) + +type StderrNotifier struct { + logger *slog.Logger +} + +func NewStderr(l *slog.Logger) *StderrNotifier { + if l == nil { + l = slog.Default() + } + return &StderrNotifier{logger: l} +} + +func (n *StderrNotifier) Notify(kind, name, problem string) { + n.logger.Error("watchdog: incohérence détectée", + "kind", kind, + "name", name, + "problem", problem, + ) +} diff --git a/internal/watchdog/notify/stderr_test.go b/internal/watchdog/notify/stderr_test.go new file mode 100644 index 0000000..cfc45a4 --- /dev/null +++ b/internal/watchdog/notify/stderr_test.go @@ -0,0 +1,120 @@ +package notify + +import ( + "bytes" + "encoding/json" + "log/slog" + "strings" + "testing" +) + +func newTestNotifier(t *testing.T) (*StderrNotifier, *bytes.Buffer) { + t.Helper() + var buf bytes.Buffer + l := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelError})) + return NewStderr(l), &buf +} + +func decodeLines(t *testing.T, buf *bytes.Buffer) []map[string]any { + t.Helper() + var out []map[string]any + for _, line := range strings.Split(strings.TrimSpace(buf.String()), "\n") { + if line == "" { + continue + } + var m map[string]any + if err := json.Unmarshal([]byte(line), &m); err != nil { + t.Fatalf("ligne non JSON %q: %v", line, err) + } + out = append(out, m) + } + return out +} + +func TestNotify_EmetLesTroisChampsStructures(t *testing.T) { + n, buf := newTestNotifier(t) + + n.Notify("vm", "i-test1", "tap12 absente du netns vp-admin") + + lines := decodeLines(t, buf) + if len(lines) != 1 { + t.Fatalf("attendu 1 ligne, obtenu %d", len(lines)) + } + + for field, want := range map[string]string{ + "kind": "vm", + "name": "i-test1", + "problem": "tap12 absente du netns vp-admin", + } { + got, ok := lines[0][field] + if !ok { + t.Errorf("champ %q absent de la sortie", field) + continue + } + if got != want { + t.Errorf("champ %q = %v, attendu %q", field, got, want) + } + } +} + +func TestNotify_NiveauError(t *testing.T) { + n, buf := newTestNotifier(t) + + n.Notify("vpc", "vp-admin", "netns absent") + + lines := decodeLines(t, buf) + if len(lines) != 1 { + t.Fatalf("attendu 1 ligne, obtenu %d", len(lines)) + } + if lines[0]["level"] != "ERROR" { + t.Errorf("level = %v, attendu ERROR", lines[0]["level"]) + } +} + +func TestNotify_MessageNeContientPasLesValeurs(t *testing.T) { + n, buf := newTestNotifier(t) + + n.Notify("subnet", "br-000042", "bridge down") + + lines := decodeLines(t, buf) + msg, _ := lines[0]["msg"].(string) + for _, v := range []string{"br-000042", "bridge down"} { + if strings.Contains(msg, v) { + t.Errorf("msg %q ne devrait pas contenir la valeur %q (elle doit être un attribut)", msg, v) + } + } +} + +func TestNotify_AucuneDeduplication(t *testing.T) { + n, buf := newTestNotifier(t) + + for range 3 { + n.Notify("vm", "i-test1", "qemu ne répond pas sur QMP") + } + + if lines := decodeLines(t, buf); len(lines) != 3 { + t.Errorf("attendu 3 lignes (une par appel), obtenu %d", len(lines)) + } +} + +func TestNotify_ChampsVides(t *testing.T) { + n, buf := newTestNotifier(t) + + n.Notify("", "", "") + + if lines := decodeLines(t, buf); len(lines) != 1 { + t.Errorf("attendu 1 ligne, obtenu %d", len(lines)) + } +} + +func TestNewStderr_LoggerNil(t *testing.T) { + n := NewStderr(nil) + if n.logger == nil { + t.Fatal("logger nil non remplacé par slog.Default()") + } + n.Notify("vpc", "vp-admin", "netns absent") +} + +func TestNewStderr_ImplementeNotifier(t *testing.T) { + var _ Notifier = NewStderr(nil) +}