mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
fix(alerts): preserve actionable TrueNAS NOTICE incidents
The INFO noise repair also suppressed NOTICE because both native levels map to monitor risk. TrueNAS documents NOTICE as notification-worthy. Retain native severity so NOTICE stays actionable without inflating its canonical severity; INFO suppression and confirmed recovery remain intact. A native-projection regression fails with the blanket filter and passes with the correction. Affected TrueNAS, unified resources, alerts and incident-memory package tests pass. Change-source: pulse-maintainer
This commit is contained in:
@@ -15,13 +15,19 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
TrueNAS provider incidents mapped to informational alert level remain on the
|
||||
canonical resource but do not enter the actionable active-alert lifecycle.
|
||||
TrueNAS INFO provider incidents remain on the canonical resource but do not
|
||||
enter the actionable active-alert lifecycle. Native NOTICE remains actionable
|
||||
at informational canonical severity; it must not be discarded merely because
|
||||
INFO and NOTICE share the monitor risk level. Native severity is preserved by
|
||||
the provider projection.
|
||||
Warning and critical incidents retain normal routing; downgrade to information
|
||||
clears an existing actionable incident through normal synchronisation.
|
||||
`TestSyncUnifiedResourceIncidentsTrueNASInformation` in
|
||||
`internal/alerts/unified_incidents_test.go` pins information, warning, recovery,
|
||||
and preservation of provider resource evidence.
|
||||
`TestSyncUnifiedResourceIncidentsTrueNASNativeNotice` additionally exercises
|
||||
native projection, NOTICE activation without severity inflation, and confirmed
|
||||
recovery to INFO.
|
||||
|
||||
Own alert identity, alert specs, evaluation, persistence semantics, and
|
||||
operator-facing alert routing behavior for live runtime alerts.
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
TrueNAS native alert projection preserves the trimmed, uppercase provider level in ResourceIncident.NativeSeverity. INFO and NOTICE retain the same canonical monitor risk; consumers must not lose their distinct actionability when projecting provider evidence.
|
||||
|
||||
Unraid host ingestion and canonical read-state reconstruction preserve the optional array disk count, distinguishing explicit zero from unknown. Storage assessment suppresses only the no-parity warning for an explicit zero-disk array; unknown counts retain the prior warning and disabled, invalid, or missing member evidence remains effective.
|
||||
|
||||
Direct PBS backup polling correlates manifestless snapshots with current
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
Provider incident nativeSeverity is evidence, not a storage recovery result or identity component. TrueNAS NOTICE remains actionable while INFO remains resource evidence; both retain canonical monitor risk. This distinction must not alter pool health assessment, recovery confirmation counts, or storage action authority.
|
||||
|
||||
TrueNAS replication state `FINISHED` is successful completion evidence when a
|
||||
last-run timestamp exists, matching the provider success event. Explicit error
|
||||
text still takes precedence; a missing last-run timestamp remains unknown.
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
ResourceIncident carries optional nativeSeverity JSON evidence independently of canonical Severity and identity. Missing nativeSeverity remains compatible with older payloads. TrueNAS INFO and NOTICE may share canonical monitor risk without becoming indistinguishable to alert consumers; native severity does not change resource or incident identity.
|
||||
|
||||
Unraid adapters preserve optional `numDisks` in both host and storage metadata, including explicit zero in JSON and absence for unknown counts. Disk count is topology evidence only: changing it must not change canonical host/storage identity. The storage projection uses the monitoring-owned assessment so an explicit pool-only array does not acquire a no-parity warning.
|
||||
|
||||
Own canonical resource identity, type normalization, typed views, and
|
||||
|
||||
@@ -83,8 +83,10 @@ func (m *Manager) SyncUnifiedResourceIncidents(resources []unifiedresources.Reso
|
||||
continue
|
||||
}
|
||||
// TrueNAS informational conditions remain visible on the resource,
|
||||
// but do not require acknowledgement as actionable incidents.
|
||||
if strings.EqualFold(strings.TrimSpace(incident.Provider), "truenas") && level == AlertLevelInfo {
|
||||
// but do not require acknowledgement as actionable incidents. NOTICE
|
||||
// shares the monitor risk level but remains notification-worthy.
|
||||
if strings.EqualFold(strings.TrimSpace(incident.Provider), "truenas") && level == AlertLevelInfo &&
|
||||
!strings.EqualFold(strings.TrimSpace(incident.NativeSeverity), "NOTICE") {
|
||||
continue
|
||||
}
|
||||
alert := unifiedIncidentAlert(resource, incident, level, now)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/operationaltrust"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/storagehealth"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/truenas"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
||||
)
|
||||
|
||||
@@ -1166,3 +1167,41 @@ func TestSyncUnifiedResourceIncidentsTrueNASInformation(t *testing.T) {
|
||||
t.Fatal("provider information removed")
|
||||
}
|
||||
}
|
||||
|
||||
// Exercise native projection as well as alert synchronisation: INFO and NOTICE
|
||||
// share a canonical risk level but have different notification semantics.
|
||||
func TestSyncUnifiedResourceIncidentsTrueNASNativeNotice(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
configureUnifiedEvalManager(t, m, unifiedEvalBaseConfig())
|
||||
for _, tc := range []struct {
|
||||
level string
|
||||
want int
|
||||
}{
|
||||
{"NOTICE", 1}, {"INFO", 0}, {"WARNING", 1}, {"INFO", 0}, {" notice ", 1}, {" info ", 0},
|
||||
} {
|
||||
records := truenas.FixtureRecords(truenas.FixtureSnapshot{
|
||||
CollectedAt: time.Now(),
|
||||
System: truenas.SystemInfo{Hostname: "native-notice", Healthy: true},
|
||||
Alerts: []truenas.Alert{{ID: "native-1", Level: tc.level, Message: "Provider condition"}},
|
||||
})
|
||||
resources := make([]unifiedresources.Resource, 0, len(records))
|
||||
for _, record := range records {
|
||||
// Ingestion assigns the canonical identity before evaluation.
|
||||
record.Resource.ID = "host:" + record.SourceID
|
||||
resources = append(resources, record.Resource)
|
||||
}
|
||||
// Native alerts require two recovery observations.
|
||||
m.SyncUnifiedResourceIncidents(resources)
|
||||
m.SyncUnifiedResourceIncidents(resources)
|
||||
if got := len(m.GetActiveAlerts()); got != tc.want {
|
||||
t.Fatalf("native %s: active alerts = %d, want %d", tc.level, got, tc.want)
|
||||
}
|
||||
if tc.level == "NOTICE" {
|
||||
for _, alert := range m.GetActiveAlerts() {
|
||||
if alert.Level != AlertLevelInfo {
|
||||
t.Fatalf("NOTICE changed canonical severity to %s", alert.Level)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1330,6 +1330,7 @@ func incidentFromAlert(alert Alert) (unifiedresources.ResourceIncident, bool) {
|
||||
}
|
||||
return unifiedresources.ResourceIncident{
|
||||
Provider: "truenas",
|
||||
NativeSeverity: strings.ToUpper(strings.TrimSpace(alert.Level)),
|
||||
NativeID: strings.TrimSpace(alert.ID),
|
||||
Code: incidentCodeFromAlert(alert),
|
||||
Severity: severity,
|
||||
|
||||
@@ -252,3 +252,23 @@ func TestPoolTopologyStaysDiscriminatorAcrossVDevLayouts(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentProjectionPreservesNativeSeverity(t *testing.T) {
|
||||
for _, level := range []string{"INFO", "NOTICE"} {
|
||||
t.Run(level, func(t *testing.T) {
|
||||
incident, ok := incidentFromAlert(Alert{ID: "condition-1", Level: " " + strings.ToLower(level) + " ", Message: "Provider condition"})
|
||||
if !ok {
|
||||
t.Fatal("native condition omitted")
|
||||
}
|
||||
if incident.NativeSeverity != level {
|
||||
t.Fatalf("native severity = %q, want %q", incident.NativeSeverity, level)
|
||||
}
|
||||
if incident.Severity != storagehealth.RiskMonitor {
|
||||
t.Fatalf("canonical severity inflated: %v", incident.Severity)
|
||||
}
|
||||
if incident.NativeID != "condition-1" {
|
||||
t.Fatalf("native identity changed: %q", incident.NativeID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,3 +588,23 @@ func TestUnraidDiskCountJSONAndIdentity(t *testing.T) {
|
||||
t.Fatalf("unknown count became known: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceIncidentNativeSeverityJSONContract(t *testing.T) {
|
||||
for _, native := range []string{"", "INFO", "NOTICE"} {
|
||||
incident := ResourceIncident{Provider: "truenas", NativeID: "condition-1", Code: "provider_condition", NativeSeverity: native}
|
||||
data, err := json.Marshal(incident)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(data), "\"nativeSeverity\"") != (native != "") {
|
||||
t.Fatalf("optional native severity encoding: %s", data)
|
||||
}
|
||||
var decoded ResourceIncident
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decoded != incident {
|
||||
t.Fatalf("incident evidence or identity changed: %+v", decoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,6 +469,8 @@ type StorageConsumerMeta struct {
|
||||
}
|
||||
|
||||
type ResourceIncident struct {
|
||||
// NativeSeverity preserves provider levels that share a canonical risk (e.g. INFO and NOTICE).
|
||||
NativeSeverity string `json:"nativeSeverity,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
NativeID string `json:"nativeId,omitempty"`
|
||||
Code string `json:"code"`
|
||||
|
||||
Reference in New Issue
Block a user