mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Surface alert override identities in diagnostics
Forward-port of the release/5.1 fix (9ac7df976). buildAlertsDiagnostic
previously emitted only cooldown/grouping flags, so triaging support
cases like #1341 where a user suspects an override key mismatch
required asking them to paste alerts config 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.
This commit is contained in:
@@ -99,6 +99,21 @@ const createDiagnosticsData = (): DiagnosticsData =>
|
||||
platforms: [{ key: 'truenas', catalog_selected: 2, credentials_opened: 1 }],
|
||||
notes: ['Some probed addresses did not match a supported API-backed platform.'],
|
||||
},
|
||||
alerts: {
|
||||
missingCooldown: false,
|
||||
missingGroupingWindow: false,
|
||||
overrides: [
|
||||
{
|
||||
key: 'pve5-ceph-pool-data_replication',
|
||||
thresholds: { usage: 50 },
|
||||
},
|
||||
{
|
||||
key: 'pve5-101',
|
||||
disabled: true,
|
||||
thresholds: { cpu: 75 },
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: ['probe failed for 10.0.0.10 after timeout'],
|
||||
}) as DiagnosticsData;
|
||||
|
||||
@@ -140,6 +155,10 @@ describe('diagnosticsModel', () => {
|
||||
}),
|
||||
);
|
||||
expect(sanitized.errors).toEqual(['probe failed for [REDACTED_IP] after timeout']);
|
||||
expect(sanitized.alerts?.overrides).toEqual([
|
||||
expect.objectContaining({ key: 'override-1', thresholds: { usage: 50 } }),
|
||||
expect.objectContaining({ key: 'override-2', disabled: true, thresholds: { cpu: 75 } }),
|
||||
]);
|
||||
expect(sanitized).not.toHaveProperty('commercialFunnel');
|
||||
expect(sanitized).not.toHaveProperty('infrastructureOnboarding');
|
||||
});
|
||||
|
||||
@@ -81,10 +81,18 @@ export interface DockerAgentDiagnostic {
|
||||
notes?: string[];
|
||||
}
|
||||
|
||||
export interface AlertsOverrideDiagnostic {
|
||||
key: string;
|
||||
disabled?: boolean;
|
||||
disableConnectivity?: boolean;
|
||||
thresholds?: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface AlertsDiagnostic {
|
||||
missingCooldown: boolean;
|
||||
missingGroupingWindow: boolean;
|
||||
notes?: string[];
|
||||
overrides?: AlertsOverrideDiagnostic[];
|
||||
}
|
||||
|
||||
export interface MetricsStoreDiagnostic {
|
||||
@@ -237,6 +245,16 @@ export function sanitizeDiagnosticsData(raw: DiagnosticsData): DiagnosticsData {
|
||||
data.aiChat.url = '[REDACTED]';
|
||||
}
|
||||
|
||||
if (data.alerts && Array.isArray(data.alerts.overrides)) {
|
||||
data.alerts = {
|
||||
...data.alerts,
|
||||
overrides: data.alerts.overrides.map((override, index) => ({
|
||||
...override,
|
||||
key: `override-${index + 1}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(data.errors)) {
|
||||
data.errors = data.errors.map(redactString);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
alertconfig "github.com/rcourtman/pulse-go-rewrite/internal/alerts/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
||||
@@ -680,9 +681,20 @@ func (a DockerAgentAttention) NormalizeCollections() DockerAgentAttention {
|
||||
|
||||
// AlertsDiagnostic summarises alert configuration migration state.
|
||||
type AlertsDiagnostic struct {
|
||||
MissingCooldown bool `json:"missingCooldown"`
|
||||
MissingGroupingWindow bool `json:"missingGroupingWindow"`
|
||||
Notes []string `json:"notes"`
|
||||
MissingCooldown bool `json:"missingCooldown"`
|
||||
MissingGroupingWindow bool `json:"missingGroupingWindow"`
|
||||
Notes []string `json:"notes"`
|
||||
Overrides []AlertsOverrideDiagnostic `json:"overrides,omitempty"`
|
||||
}
|
||||
|
||||
// AlertsOverrideDiagnostic captures one threshold override entry so support
|
||||
// triage can verify the persisted key matches the runtime resource ID
|
||||
// without asking users to cat their alerts config from inside the container.
|
||||
type AlertsOverrideDiagnostic struct {
|
||||
Key string `json:"key"`
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
DisableConnectivity bool `json:"disableConnectivity,omitempty"`
|
||||
Thresholds map[string]float64 `json:"thresholds,omitempty"`
|
||||
}
|
||||
|
||||
func (d AlertsDiagnostic) NormalizeCollections() AlertsDiagnostic {
|
||||
@@ -1321,9 +1333,49 @@ func buildAlertsDiagnostic(m *monitoring.Monitor) *AlertsDiagnostic {
|
||||
appendNote("Alert grouping window is disabled. Configure a grouping window to bundle related alerts.")
|
||||
}
|
||||
|
||||
overrideKeys := make([]string, 0, len(config.Overrides))
|
||||
for key := range config.Overrides {
|
||||
overrideKeys = append(overrideKeys, key)
|
||||
}
|
||||
sort.Strings(overrideKeys)
|
||||
for _, key := range overrideKeys {
|
||||
diag.Overrides = append(diag.Overrides, summarizeAlertOverride(key, config.Overrides[key]))
|
||||
}
|
||||
|
||||
return diag
|
||||
}
|
||||
|
||||
func summarizeAlertOverride(key string, th alertconfig.ThresholdConfig) AlertsOverrideDiagnostic {
|
||||
out := AlertsOverrideDiagnostic{
|
||||
Key: key,
|
||||
Disabled: th.Disabled,
|
||||
DisableConnectivity: th.DisableConnectivity,
|
||||
}
|
||||
|
||||
thresholds := map[string]float64{}
|
||||
addThreshold := func(name string, t *alertconfig.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 fingerprintPublicKey(pub string) (string, error) {
|
||||
pub = strings.TrimSpace(pub)
|
||||
if pub == "" {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
alertconfig "github.com/rcourtman/pulse-go-rewrite/internal/alerts/config"
|
||||
"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"
|
||||
@@ -472,6 +473,70 @@ func TestBuildAlertsDiagnostic_LegacySettings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Triage cases like #1341 hinge on whether a persisted override key matches
|
||||
// the runtime resource ID. Surface override keys + thresholds in the
|
||||
// diagnostics export so we don't have to ask users to cat the alerts config.
|
||||
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 := alertconfig.HysteresisThreshold{Trigger: 50, Clear: 45}
|
||||
cpu := alertconfig.HysteresisThreshold{Trigger: 75, Clear: 70}
|
||||
alertCfg.Overrides = map[string]alertconfig.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", keysOfOverrideDiag(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", keysOfOverrideDiag(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 keysOfOverrideDiag(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,
|
||||
|
||||
Reference in New Issue
Block a user