diff --git a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx index 46ba61301..e8e77db9f 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx @@ -244,6 +244,12 @@ interface ThresholdsTableProps { ) => void; dockerIgnoredPrefixes: () => string[]; setDockerIgnoredPrefixes: (value: string[] | ((prev: string[]) => string[])) => void; + ignoredGuestPrefixes: () => string[]; + setIgnoredGuestPrefixes: (value: string[] | ((prev: string[]) => string[])) => void; + guestTagWhitelist: () => string[]; + setGuestTagWhitelist: (value: string[] | ((prev: string[]) => string[])) => void; + guestTagBlacklist: () => string[]; + setGuestTagBlacklist: (value: string[] | ((prev: string[]) => string[])) => void; storageDefault: () => number; setStorageDefault: (value: number) => void; resetGuestDefaults?: () => void; @@ -432,6 +438,67 @@ export function ThresholdsTable(props: ThresholdsTableProps) { props.setHasUnsavedChanges(true); }; + const [ignoredGuestInput, setIgnoredGuestInput] = createSignal( + props.ignoredGuestPrefixes().join('\n'), + ); + const [guestTagWhitelistInput, setGuestTagWhitelistInput] = createSignal( + props.guestTagWhitelist().join('\n'), + ); + const [guestTagBlacklistInput, setGuestTagBlacklistInput] = createSignal( + props.guestTagBlacklist().join('\n'), + ); + + createEffect(() => { + const remote = props.ignoredGuestPrefixes(); + const local = ignoredGuestInput(); + const normalizedLocal = normalizeDockerIgnoredInput(local); + const isSynced = + remote.length === normalizedLocal.length && + remote.every((val, i) => val === normalizedLocal[i]); + if (!isSynced) setIgnoredGuestInput(remote.join('\n')); + }); + + createEffect(() => { + const remote = props.guestTagWhitelist(); + const local = guestTagWhitelistInput(); + const normalizedLocal = normalizeDockerIgnoredInput(local); + const isSynced = + remote.length === normalizedLocal.length && + remote.every((val, i) => val === normalizedLocal[i]); + if (!isSynced) setGuestTagWhitelistInput(remote.join('\n')); + }); + + createEffect(() => { + const remote = props.guestTagBlacklist(); + const local = guestTagBlacklistInput(); + const normalizedLocal = normalizeDockerIgnoredInput(local); + const isSynced = + remote.length === normalizedLocal.length && + remote.every((val, i) => val === normalizedLocal[i]); + if (!isSynced) setGuestTagBlacklistInput(remote.join('\n')); + }); + + const handleIgnoredGuestChange = (value: string) => { + setIgnoredGuestInput(value); + const normalized = normalizeDockerIgnoredInput(value); + props.setIgnoredGuestPrefixes(normalized); + props.setHasUnsavedChanges(true); + }; + + const handleGuestTagWhitelistChange = (value: string) => { + setGuestTagWhitelistInput(value); + const normalized = normalizeDockerIgnoredInput(value); + props.setGuestTagWhitelist(normalized); + props.setHasUnsavedChanges(true); + }; + + const handleGuestTagBlacklistChange = (value: string) => { + setGuestTagBlacklistInput(value); + const normalized = normalizeDockerIgnoredInput(value); + props.setGuestTagBlacklist(normalized); + props.setHasUnsavedChanges(true); + }; + // Set up keyboard shortcuts onMount(() => { const isEditableElement = (el: HTMLElement | null | undefined): boolean => { @@ -2552,6 +2619,59 @@ export function ThresholdsTable(props: ThresholdsTableProps) { + + toggleSection('guest-filtering')} + icon={} + emptyMessage="Configure guest filtering rules." + > + + + + Ignored Prefixes + Skip metrics for guests starting with: + + handleIgnoredGuestChange(e.currentTarget.value)} + rows={6} + class="w-full rounded-md border border-gray-300 bg-white p-2 text-sm text-gray-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" + placeholder="dev-" + /> + + + + Tag Whitelist + Only monitor guests with at least one of these tags (leave empty to disable whitelist): + + handleGuestTagWhitelistChange(e.currentTarget.value)} + rows={6} + class="w-full rounded-md border border-gray-300 bg-white p-2 text-sm text-gray-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" + placeholder="production" + /> + + + + Tag Blacklist + Ignore guests with any of these tags: + + handleGuestTagBlacklistChange(e.currentTarget.value)} + rows={6} + class="w-full rounded-md border border-gray-300 bg-white p-2 text-sm text-gray-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" + placeholder="maintenance" + /> + + + + + ([]); + const [ignoredGuestPrefixes, setIgnoredGuestPrefixes] = createSignal([]); + const [guestTagWhitelist, setGuestTagWhitelist] = createSignal([]); + const [guestTagBlacklist, setGuestTagBlacklist] = createSignal([]); const [storageDefault, setStorageDefault] = createSignal(FACTORY_STORAGE_DEFAULT); const [backupDefaults, setBackupDefaults] = createSignal({ @@ -1748,6 +1754,15 @@ export function Alerts() { dockerIgnoredContainerPrefixes: dockerIgnoredPrefixes() .map((prefix) => prefix.trim()) .filter((prefix) => prefix.length > 0), + ignoredGuestPrefixes: ignoredGuestPrefixes() + .map((prefix) => prefix.trim()) + .filter((prefix) => prefix.length > 0), + guestTagWhitelist: guestTagWhitelist() + .map((tag) => tag.trim()) + .filter((tag) => tag.length > 0), + guestTagBlacklist: guestTagBlacklist() + .map((tag) => tag.trim()) + .filter((tag) => tag.length > 0), storageDefault: createHysteresisThreshold(storageDefault()), minimumDelta: 2.0, suppressionWindow: 5, @@ -2034,6 +2049,12 @@ export function Alerts() { setDockerDefaults={setDockerDefaults} dockerIgnoredPrefixes={dockerIgnoredPrefixes} setDockerIgnoredPrefixes={setDockerIgnoredPrefixes} + ignoredGuestPrefixes={ignoredGuestPrefixes} + setIgnoredGuestPrefixes={setIgnoredGuestPrefixes} + guestTagWhitelist={guestTagWhitelist} + setGuestTagWhitelist={setGuestTagWhitelist} + guestTagBlacklist={guestTagBlacklist} + setGuestTagBlacklist={setGuestTagBlacklist} storageDefault={storageDefault} setStorageDefault={setStorageDefault} resetGuestDefaults={resetGuestDefaults} @@ -2192,7 +2213,7 @@ function OverviewTab(props: { if (!status) return false; return !status.features?.['ai_alerts']; }); - const aiAlertsUpgradeURL = createMemo(() => licenseFeatures()?.upgrade_url || 'https://pulsemonitor.app/pro'); + const aiAlertsUpgradeURL = createMemo(() => licenseFeatures()?.upgrade_url || 'https://pulserelay.pro'); // Live streaming state for running patrol const [expandedLiveStream, setExpandedLiveStream] = createSignal(false); // Track streaming blocks for sequential display (like AI chat) @@ -2205,7 +2226,7 @@ function OverviewTab(props: { const [currentThinking, setCurrentThinking] = createSignal(''); let liveStreamUnsubscribe: (() => void) | null = null; const patrolRequiresLicense = createMemo(() => patrolStatus()?.license_required === true); - const patrolUpgradeURL = createMemo(() => patrolStatus()?.upgrade_url || 'https://pulsemonitor.app/pro'); + const patrolUpgradeURL = createMemo(() => patrolStatus()?.upgrade_url || 'https://pulserelay.pro'); const patrolLicenseNote = createMemo(() => { if (!patrolRequiresLicense()) return ''; const status = patrolStatus()?.license_status; @@ -4078,6 +4099,9 @@ interface ThresholdsTabProps { dockerDisableConnectivity: () => boolean; dockerPoweredOffSeverity: () => 'warning' | 'critical'; dockerIgnoredPrefixes: () => string[]; + ignoredGuestPrefixes: () => string[]; + guestTagWhitelist: () => string[]; + guestTagBlacklist: () => string[]; storageDefault: () => number; timeThresholds: () => { guest: number; node: number; storage: number; pbs: number }; metricTimeThresholds: () => Record>; @@ -4146,6 +4170,9 @@ interface ThresholdsTabProps { setDockerDisableConnectivity: (value: boolean) => void; setDockerPoweredOffSeverity: (value: 'warning' | 'critical') => void; setDockerIgnoredPrefixes: (value: string[] | ((prev: string[]) => string[])) => void; + setIgnoredGuestPrefixes: (value: string[] | ((prev: string[]) => string[])) => void; + setGuestTagWhitelist: (value: string[] | ((prev: string[]) => string[])) => void; + setGuestTagBlacklist: (value: string[] | ((prev: string[]) => string[])) => void; setStorageDefault: (value: number) => void; setMetricTimeThresholds: ( value: @@ -4258,6 +4285,12 @@ function ThresholdsTab(props: ThresholdsTabProps) { setDockerPoweredOffSeverity={props.setDockerPoweredOffSeverity} dockerIgnoredPrefixes={props.dockerIgnoredPrefixes} setDockerIgnoredPrefixes={props.setDockerIgnoredPrefixes} + ignoredGuestPrefixes={props.ignoredGuestPrefixes} + setIgnoredGuestPrefixes={props.setIgnoredGuestPrefixes} + guestTagWhitelist={props.guestTagWhitelist} + setGuestTagWhitelist={props.setGuestTagWhitelist} + guestTagBlacklist={props.guestTagBlacklist} + setGuestTagBlacklist={props.setGuestTagBlacklist} storageDefault={props.storageDefault} setStorageDefault={props.setStorageDefault} timeThresholds={props.timeThresholds} diff --git a/frontend-modern/src/types/alerts.ts b/frontend-modern/src/types/alerts.ts index eba2e758a..fd9cfb4a9 100644 --- a/frontend-modern/src/types/alerts.ts +++ b/frontend-modern/src/types/alerts.ts @@ -121,6 +121,9 @@ export interface AlertConfig { storageDefault: HysteresisThreshold; dockerDefaults?: DockerThresholdConfig; dockerIgnoredContainerPrefixes?: string[]; + ignoredGuestPrefixes?: string[]; + guestTagWhitelist?: string[]; + guestTagBlacklist?: string[]; pmgDefaults?: PMGThresholdDefaults; snapshotDefaults?: SnapshotAlertConfig; backupDefaults?: BackupAlertConfig; diff --git a/internal/alerts/alerts.go b/internal/alerts/alerts.go index 37fdd9b20..58db7e82f 100644 --- a/internal/alerts/alerts.go +++ b/internal/alerts/alerts.go @@ -377,6 +377,9 @@ type AlertConfig struct { StorageDefault HysteresisThreshold `json:"storageDefault"` DockerDefaults DockerThresholdConfig `json:"dockerDefaults"` DockerIgnoredContainerPrefixes []string `json:"dockerIgnoredContainerPrefixes,omitempty"` + IgnoredGuestPrefixes []string `json:"ignoredGuestPrefixes,omitempty"` + GuestTagWhitelist []string `json:"guestTagWhitelist,omitempty"` + GuestTagBlacklist []string `json:"guestTagBlacklist,omitempty"` PMGDefaults PMGThresholdConfig `json:"pmgDefaults"` SnapshotDefaults SnapshotAlertConfig `json:"snapshotDefaults"` BackupDefaults BackupAlertConfig `json:"backupDefaults"` @@ -2097,6 +2100,9 @@ func (m *Manager) CheckGuest(guest interface{}, instanceName string) { enabled := m.config.Enabled disableAllGuests := m.config.DisableAllGuests disableAllGuestsOffline := m.config.DisableAllGuestsOffline + ignoredGuestPrefixes := m.config.IgnoredGuestPrefixes + guestTagWhitelist := m.config.GuestTagWhitelist + guestTagBlacklist := m.config.GuestTagBlacklist m.mu.RUnlock() if !enabled { @@ -2166,6 +2172,18 @@ func (m *Manager) CheckGuest(guest interface{}, instanceName string) { return } + + + // Check ignored prefixes + for _, prefix := range ignoredGuestPrefixes { + if prefix != "" && strings.HasPrefix(name, prefix) { + if cleared := m.suppressGuestAlerts(guestID); cleared { + m.saveActiveAlertsAsync("ignored-prefix") + } + return + } + } + settings := parsePulseTags(tags) if settings.Suppress { if cleared := m.suppressGuestAlerts(guestID); cleared { @@ -2177,6 +2195,44 @@ func (m *Manager) CheckGuest(guest interface{}, instanceName string) { return } + // Custom Tag Filtering + if len(guestTagBlacklist) > 0 || len(guestTagWhitelist) > 0 { + // Normalize tags once for checking + normalizedTags := make(map[string]bool) + for _, tag := range tags { + normalizedTags[strings.ToLower(strings.TrimSpace(tag))] = true + } + + // Check Blacklist + for _, block := range guestTagBlacklist { + if normalizedTags[strings.ToLower(strings.TrimSpace(block))] { + if cleared := m.suppressGuestAlerts(guestID); cleared { + m.saveActiveAlertsAsync("tag-blacklist") + } + log.Debug().Str("guestID", guestID).Msg("Guest suppressed by tag blacklist") + return + } + } + + // Check Whitelist + if len(guestTagWhitelist) > 0 { + found := false + for _, allow := range guestTagWhitelist { + if normalizedTags[strings.ToLower(strings.TrimSpace(allow))] { + found = true + break + } + } + if !found { + if cleared := m.suppressGuestAlerts(guestID); cleared { + m.saveActiveAlertsAsync("tag-whitelist") + } + log.Debug().Str("guestID", guestID).Msg("Guest suppressed by tag whitelist (required tag not found)") + return + } + } + } + monitorOnly := settings.MonitorOnly if monitorOnly || m.guestHasMonitorOnlyAlerts(guestID) { log.Debug().
Skip metrics for guests starting with:
Only monitor guests with at least one of these tags (leave empty to disable whitelist):
Ignore guests with any of these tags: