diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md index 3b83b4cccf..b77ee0219e 100644 --- a/docs/release-control/v6/internal/subsystems/monitoring.md +++ b/docs/release-control/v6/internal/subsystems/monitoring.md @@ -32,6 +32,11 @@ monitor restart. Monitor construction also applies the persisted grouping enabled flag, window, and node/guest keys as one notification-manager policy, so restart behavior is identical to a live alert-configuration save. +Monitor construction enables the alerts-owned persistent event log once for +each tenant alert manager. This is bootstrap wiring only: monitoring does not +own event types, retention, query semantics, or lifecycle/notification truth, +and an event-store startup or append failure must not interrupt polling, state +assembly, alert evaluation, or notification delivery. Monitoring also owns the distinction between Proxmox VM power state and QEMU guest-agent reachability: fresh or never-healthy VMs with an enabled but unavailable guest agent stay `not-running`, while only VMs with recent healthy diff --git a/internal/alerts/alerts_test.go b/internal/alerts/alerts_test.go index 46a6bfa27d..d7ab30da69 100644 --- a/internal/alerts/alerts_test.go +++ b/internal/alerts/alerts_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/rcourtman/pulse-go-rewrite/internal/alerts/eventlog" alertspecs "github.com/rcourtman/pulse-go-rewrite/internal/alerts/specs" "github.com/rcourtman/pulse-go-rewrite/internal/models" "github.com/rcourtman/pulse-go-rewrite/internal/proxmoxidentity" @@ -31,6 +32,23 @@ func ptrTime(t time.Time) *time.Time { return &tt } +func TestAlertEventLogDoesNotInventFiringFromActiveStorage(t *testing.T) { + manager := newEventLogManager(t) + addEventLogAlert(manager, &Alert{ + ID: "restored-alert", + Type: "cpu", + Level: AlertLevelWarning, + ResourceID: "node-1", + ResourceName: "node-1", + StartTime: time.Now().Add(-10 * time.Minute), + }) + + events := queryAlertEvents(t, manager, eventlog.Filter{AlertID: "restored-alert"}) + if len(events) != 0 { + t.Fatalf("active-storage insertion recorded events = %+v, want none", events) + } +} + func TestGuestAlertIncludesTagsForNotificationRouting(t *testing.T) { m := newTestManager(t) m.ClearActiveAlerts() diff --git a/internal/api/alerts_endpoints_test.go b/internal/api/alerts_endpoints_test.go index 1492d5392f..e76d5a3403 100644 --- a/internal/api/alerts_endpoints_test.go +++ b/internal/api/alerts_endpoints_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/rcourtman/pulse-go-rewrite/internal/alerts" + "github.com/rcourtman/pulse-go-rewrite/internal/alerts/eventlog" ) func TestAlertsEndpoints(t *testing.T) { @@ -285,6 +286,26 @@ func TestAlertsEndpoints(t *testing.T) { } }) + t.Run("GetAlertEvents", func(t *testing.T) { + res, err := http.Get(srv.server.URL + "/api/alerts/events") + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + t.Fatalf("status code = %d, want %d", res.StatusCode, http.StatusOK) + } + + var events []eventlog.Event + if err := json.NewDecoder(res.Body).Decode(&events); err != nil { + t.Fatalf("decode alert events: %v", err) + } + if events == nil { + t.Fatal("alert events decoded as nil, want an empty JSON array") + } + }) + // 5. Get alert history t.Run("GetAlertHistory", func(t *testing.T) { res, err := http.Get(srv.server.URL + "/api/alerts/history") diff --git a/internal/monitoring/monitor_host_agent_removal_lifecycle_test.go b/internal/monitoring/monitor_host_agent_removal_lifecycle_test.go index f9ba67a7bf..ec02382e72 100644 --- a/internal/monitoring/monitor_host_agent_removal_lifecycle_test.go +++ b/internal/monitoring/monitor_host_agent_removal_lifecycle_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/rcourtman/pulse-go-rewrite/internal/alerts" + "github.com/rcourtman/pulse-go-rewrite/internal/alerts/eventlog" "github.com/rcourtman/pulse-go-rewrite/internal/config" "github.com/rcourtman/pulse-go-rewrite/internal/models" "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources" @@ -16,6 +17,10 @@ import ( func newHostRemovalLifecycleMonitor(t *testing.T, dataPath string) *Monitor { t.Helper() + events, err := eventlog.OpenInMemory() + if err != nil { + t.Fatalf("open alert event log: %v", err) + } monitor := &Monitor{ state: models.NewState(), alertManager: alerts.NewManagerWithDataDir(dataPath), @@ -25,6 +30,10 @@ func newHostRemovalLifecycleMonitor(t *testing.T, dataPath string) *Monitor { config: &config.Config{DataPath: dataPath}, hostContinuityStore: config.NewHostContinuityStore(dataPath, nil), } + // Production monitor construction enables the alerts-owned event log. + // Exercise agent removal and re-enrollment with the same adjacent runtime + // enabled so it cannot accidentally become lifecycle authority. + monitor.alertManager.SetEventLog(events) t.Cleanup(func() { monitor.alertManager.Stop() }) return monitor }