mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-29 01:27:34 +00:00
Surface alert override identities in diagnostics
Refs #1341 buildAlertsDiagnostic previously emitted only boolean flags (legacy thresholds, missing cooldown/grouping window). Override keys and their trigger values were absent, so triaging a support case where the user suspects an override mismatch required asking them to paste data/alerts.json from inside their container. Add an Overrides slice that names each persisted key with its thresholds and disabled flags. Sanitize mode in the frontend redacts the keys to override-N while keeping thresholds visible, so a public export still shows the alert shape without leaking instance names that may be hostnames.
This commit is contained in:
parent
a70f1d9676
commit
9ac7df976f
3 changed files with 139 additions and 8 deletions
|
|
@ -345,6 +345,18 @@ export const DiagnosticsPanel: Component = () => {
|
|||
data.aiChat.url = '[REDACTED]';
|
||||
}
|
||||
|
||||
// Redact alert override keys: they encode instance names that may
|
||||
// themselves be hostnames or IPs (some users name instances after
|
||||
// their Proxmox URL). Thresholds and disabled flags stay visible so
|
||||
// the override shape is still triageable.
|
||||
const alertsAny = data.alerts as Record<string, unknown> | undefined;
|
||||
if (alertsAny && Array.isArray(alertsAny.overrides)) {
|
||||
alertsAny.overrides = alertsAny.overrides.map((o: Record<string, unknown>, i: number) => ({
|
||||
...o,
|
||||
key: `override-${i + 1}`,
|
||||
}));
|
||||
}
|
||||
|
||||
// Redact IPs in error messages
|
||||
data.errors = errors.map(redactString);
|
||||
|
||||
|
|
|
|||
|
|
@ -376,12 +376,25 @@ type DockerAgentAttention struct {
|
|||
|
||||
// AlertsDiagnostic summarises alert configuration migration state.
|
||||
type AlertsDiagnostic struct {
|
||||
LegacyThresholdsDetected bool `json:"legacyThresholdsDetected"`
|
||||
LegacyThresholdSources []string `json:"legacyThresholdSources,omitempty"`
|
||||
LegacyScheduleSettings []string `json:"legacyScheduleSettings,omitempty"`
|
||||
MissingCooldown bool `json:"missingCooldown"`
|
||||
MissingGroupingWindow bool `json:"missingGroupingWindow"`
|
||||
Notes []string `json:"notes,omitempty"`
|
||||
LegacyThresholdsDetected bool `json:"legacyThresholdsDetected"`
|
||||
LegacyThresholdSources []string `json:"legacyThresholdSources,omitempty"`
|
||||
LegacyScheduleSettings []string `json:"legacyScheduleSettings,omitempty"`
|
||||
MissingCooldown bool `json:"missingCooldown"`
|
||||
MissingGroupingWindow bool `json:"missingGroupingWindow"`
|
||||
Notes []string `json:"notes,omitempty"`
|
||||
Overrides []AlertsOverrideDiagnostic `json:"overrides,omitempty"`
|
||||
}
|
||||
|
||||
// AlertsOverrideDiagnostic captures one threshold override entry so support
|
||||
// triage can verify that the persisted key matches the runtime resource ID
|
||||
// (e.g. that a Ceph pool override under `pve5-ceph-pool-data_replication` is
|
||||
// actually keyed the same way the alert manager looks it up).
|
||||
type AlertsOverrideDiagnostic struct {
|
||||
Key string `json:"key"`
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
DisableConnectivity bool `json:"disableConnectivity,omitempty"`
|
||||
Thresholds map[string]float64 `json:"thresholds,omitempty"`
|
||||
LegacyThresholds bool `json:"legacyThresholds,omitempty"`
|
||||
}
|
||||
|
||||
// AIChatDiagnostic reports on the AI chat service status.
|
||||
|
|
@ -1049,13 +1062,22 @@ func buildAlertsDiagnostic(m *monitoring.Monitor) *AlertsDiagnostic {
|
|||
legacySources = append(legacySources, "node-defaults")
|
||||
}
|
||||
|
||||
overrideKeys := make([]string, 0, len(config.Overrides))
|
||||
for key := range config.Overrides {
|
||||
overrideKeys = append(overrideKeys, key)
|
||||
}
|
||||
sort.Strings(overrideKeys)
|
||||
|
||||
overrideIndex := 0
|
||||
for _, override := range config.Overrides {
|
||||
for _, key := range overrideKeys {
|
||||
override := config.Overrides[key]
|
||||
overrideIndex++
|
||||
if hasLegacyThresholds(override) {
|
||||
isLegacy := hasLegacyThresholds(override)
|
||||
if isLegacy {
|
||||
diag.LegacyThresholdsDetected = true
|
||||
legacySources = append(legacySources, fmt.Sprintf("override-%d", overrideIndex))
|
||||
}
|
||||
diag.Overrides = append(diag.Overrides, summarizeOverride(key, override, isLegacy))
|
||||
}
|
||||
|
||||
for idx, rule := range config.CustomRules {
|
||||
|
|
@ -1156,6 +1178,38 @@ func hasLegacyThresholds(th alerts.ThresholdConfig) bool {
|
|||
th.NetworkOutLegacy != nil
|
||||
}
|
||||
|
||||
func summarizeOverride(key string, th alerts.ThresholdConfig, legacy bool) AlertsOverrideDiagnostic {
|
||||
out := AlertsOverrideDiagnostic{
|
||||
Key: key,
|
||||
Disabled: th.Disabled,
|
||||
DisableConnectivity: th.DisableConnectivity,
|
||||
LegacyThresholds: legacy,
|
||||
}
|
||||
|
||||
thresholds := map[string]float64{}
|
||||
addThreshold := func(name string, t *alerts.HysteresisThreshold) {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
thresholds[name] = t.Trigger
|
||||
}
|
||||
addThreshold("cpu", th.CPU)
|
||||
addThreshold("memory", th.Memory)
|
||||
addThreshold("disk", th.Disk)
|
||||
addThreshold("diskRead", th.DiskRead)
|
||||
addThreshold("diskWrite", th.DiskWrite)
|
||||
addThreshold("networkIn", th.NetworkIn)
|
||||
addThreshold("networkOut", th.NetworkOut)
|
||||
addThreshold("usage", th.Usage)
|
||||
addThreshold("temperature", th.Temperature)
|
||||
addThreshold("diskTemperature", th.DiskTemperature)
|
||||
|
||||
if len(thresholds) > 0 {
|
||||
out.Thresholds = thresholds
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func preferredDockerHostName(host models.DockerHost) string {
|
||||
if name := strings.TrimSpace(host.DisplayName); name != "" {
|
||||
return name
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
|
||||
|
|
@ -415,6 +416,70 @@ func TestBuildAlertsDiagnostic_LegacySettings(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Triage cases like #1341 hinge on whether the persisted override key
|
||||
// matches the runtime storage ID. Surface override keys + thresholds in the
|
||||
// diagnostics export so we don't have to ask users to cat `data/alerts.json`.
|
||||
func TestBuildAlertsDiagnostic_OverridesEmittedForTriage(t *testing.T) {
|
||||
cfg := &config.Config{DataPath: t.TempDir()}
|
||||
monitor := newMonitorForDiagnostics(t, cfg)
|
||||
|
||||
manager := monitor.GetAlertManager()
|
||||
alertCfg := manager.GetConfig()
|
||||
alertCfg.Schedule.Cooldown = 600
|
||||
alertCfg.Schedule.Grouping.Window = 30
|
||||
|
||||
usage := alerts.HysteresisThreshold{Trigger: 50, Clear: 45}
|
||||
cpu := alerts.HysteresisThreshold{Trigger: 75, Clear: 70}
|
||||
alertCfg.Overrides = map[string]alerts.ThresholdConfig{
|
||||
"pve5-ceph-pool-data_replication": {Usage: &usage},
|
||||
"pve5-101": {CPU: &cpu, Disabled: true},
|
||||
}
|
||||
manager.UpdateConfig(alertCfg)
|
||||
|
||||
diag := buildAlertsDiagnostic(monitor)
|
||||
if diag == nil {
|
||||
t.Fatalf("expected diagnostics")
|
||||
}
|
||||
if len(diag.Overrides) != 2 {
|
||||
t.Fatalf("override count = %d, want 2", len(diag.Overrides))
|
||||
}
|
||||
|
||||
byKey := map[string]AlertsOverrideDiagnostic{}
|
||||
for _, o := range diag.Overrides {
|
||||
byKey[o.Key] = o
|
||||
}
|
||||
|
||||
ceph, ok := byKey["pve5-ceph-pool-data_replication"]
|
||||
if !ok {
|
||||
t.Fatalf("expected ceph pool override key in diagnostics, got keys %v", keysOf(byKey))
|
||||
}
|
||||
if got := ceph.Thresholds["usage"]; got != 50 {
|
||||
t.Fatalf("ceph usage threshold = %.1f, want 50", got)
|
||||
}
|
||||
if ceph.Disabled {
|
||||
t.Fatalf("ceph override should not be disabled")
|
||||
}
|
||||
|
||||
guest, ok := byKey["pve5-101"]
|
||||
if !ok {
|
||||
t.Fatalf("expected guest override key in diagnostics, got keys %v", keysOf(byKey))
|
||||
}
|
||||
if got := guest.Thresholds["cpu"]; got != 75 {
|
||||
t.Fatalf("guest cpu threshold = %.1f, want 75", got)
|
||||
}
|
||||
if !guest.Disabled {
|
||||
t.Fatalf("guest override Disabled flag lost in summary")
|
||||
}
|
||||
}
|
||||
|
||||
func keysOf(m map[string]AlertsOverrideDiagnostic) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestBuildDiscoveryDiagnostic_ConfigOnly(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
DiscoveryEnabled: true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue