mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-10 03:51:54 +00:00
feat: Add guest filtering by tag and name prefix via Alert Configuration. Resolves #863
This commit is contained in:
parent
c9fc827f4c
commit
71d0401c80
4 changed files with 214 additions and 2 deletions
|
|
@ -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) {
|
|||
</CollapsibleSection>
|
||||
</Show>
|
||||
|
||||
<Show when={activeTab() === 'proxmox'}>
|
||||
<CollapsibleSection
|
||||
id="guest-filtering"
|
||||
title="Guest Filtering"
|
||||
collapsed={isCollapsed('guest-filtering')}
|
||||
onToggle={() => toggleSection('guest-filtering')}
|
||||
icon={<Monitor class="w-5 h-5" />}
|
||||
emptyMessage="Configure guest filtering rules."
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-6 p-4 xl:grid-cols-3">
|
||||
<Card padding="md" tone="glass">
|
||||
<div class="mb-2">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Ignored Prefixes</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Skip metrics for guests starting with:</p>
|
||||
</div>
|
||||
<textarea
|
||||
value={ignoredGuestInput()}
|
||||
onInput={(e) => 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-"
|
||||
/>
|
||||
</Card>
|
||||
<Card padding="md" tone="glass">
|
||||
<div class="mb-2">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Tag Whitelist</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Only monitor guests with at least one of these tags (leave empty to disable whitelist):</p>
|
||||
</div>
|
||||
<textarea
|
||||
value={guestTagWhitelistInput()}
|
||||
onInput={(e) => 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"
|
||||
/>
|
||||
</Card>
|
||||
<Card padding="md" tone="glass">
|
||||
<div class="mb-2">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Tag Blacklist</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Ignore guests with any of these tags:</p>
|
||||
</div>
|
||||
<textarea
|
||||
value={guestTagBlacklistInput()}
|
||||
onInput={(e) => 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"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
</Show>
|
||||
|
||||
<Show when={hasSection('backups')}>
|
||||
<CollapsibleSection
|
||||
id="backups"
|
||||
|
|
|
|||
|
|
@ -1033,6 +1033,9 @@ export function Alerts() {
|
|||
setDockerPoweredOffSeverity(FACTORY_DOCKER_STATE_SEVERITY);
|
||||
}
|
||||
setDockerIgnoredPrefixes(config.dockerIgnoredContainerPrefixes ?? []);
|
||||
setIgnoredGuestPrefixes(config.ignoredGuestPrefixes ?? []);
|
||||
setGuestTagWhitelist(config.guestTagWhitelist ?? []);
|
||||
setGuestTagBlacklist(config.guestTagBlacklist ?? []);
|
||||
|
||||
if (config.storageDefault) {
|
||||
setStorageDefault(getTriggerValue(config.storageDefault) ?? 85);
|
||||
|
|
@ -1434,6 +1437,9 @@ export function Alerts() {
|
|||
FACTORY_DOCKER_STATE_SEVERITY,
|
||||
);
|
||||
const [dockerIgnoredPrefixes, setDockerIgnoredPrefixes] = createSignal<string[]>([]);
|
||||
const [ignoredGuestPrefixes, setIgnoredGuestPrefixes] = createSignal<string[]>([]);
|
||||
const [guestTagWhitelist, setGuestTagWhitelist] = createSignal<string[]>([]);
|
||||
const [guestTagBlacklist, setGuestTagBlacklist] = createSignal<string[]>([]);
|
||||
|
||||
const [storageDefault, setStorageDefault] = createSignal(FACTORY_STORAGE_DEFAULT);
|
||||
const [backupDefaults, setBackupDefaults] = createSignal<BackupAlertConfig>({
|
||||
|
|
@ -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<string, Record<string, number>>;
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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().
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue