Merge core runtime TrueNAS NOTICE correction

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-05 01:45:34 +01:00
10 changed files with 100 additions and 4 deletions
@@ -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
+4 -2
View File
@@ -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)
+39
View File
@@ -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)
}
}
}
}
}
+1
View File
@@ -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)
}
}
}
+2
View File
@@ -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"`