diff --git a/Dashboard/MainWindow.xaml.cs b/Dashboard/MainWindow.xaml.cs index 00027fb..852898d 100644 --- a/Dashboard/MainWindow.xaml.cs +++ b/Dashboard/MainWindow.xaml.cs @@ -26,6 +26,7 @@ using PerformanceMonitorDashboard.Services; using System.ComponentModel; using System.Windows.Data; +using System.Xml.Linq; namespace PerformanceMonitorDashboard { @@ -1064,7 +1065,7 @@ private async Task CheckAllServerAlertsAsync() var connectionString = server.GetConnectionString(_credentialService); var databaseService = new DatabaseService(connectionString); var connStatus = _serverManager.GetConnectionStatus(server.Id); - var health = await databaseService.GetAlertHealthAsync(connStatus.SqlEngineEdition, prefs.LongRunningQueryThresholdMinutes, prefs.LongRunningJobMultiplier, prefs.LongRunningQueryMaxResults, prefs.LongRunningQueryExcludeSpServerDiagnostics, prefs.LongRunningQueryExcludeWaitFor, prefs.LongRunningQueryExcludeBackups, prefs.LongRunningQueryExcludeMiscWaits); + var health = await databaseService.GetAlertHealthAsync(connStatus.SqlEngineEdition, prefs.LongRunningQueryThresholdMinutes, prefs.LongRunningJobMultiplier, prefs.LongRunningQueryMaxResults, prefs.LongRunningQueryExcludeSpServerDiagnostics, prefs.LongRunningQueryExcludeWaitFor, prefs.LongRunningQueryExcludeBackups, prefs.LongRunningQueryExcludeMiscWaits, prefs.AlertExcludedDatabases); if (health.IsOnline) { @@ -1124,7 +1125,7 @@ private async Task EvaluateAlertConditionsAsync( $"{(int)health.TotalBlocked} session(s), longest {(int)health.LongestBlockedSeconds}s", $"{prefs.BlockingThresholdSeconds}s", true, "tray"); - var blockingContext = await BuildBlockingContextAsync(databaseService); + var blockingContext = await BuildBlockingContextAsync(databaseService, prefs.AlertExcludedDatabases); await _emailAlertService.TrySendAlertEmailAsync( "Blocking Detected", @@ -1152,8 +1153,13 @@ await _emailAlertService.TrySendAlertEmailAsync( } _previousDeadlockCounts[serverId] = health.DeadlockCount; + /* Use the database-filtered count when excluded databases are configured, + matching how blocking alerts filter before the threshold check. + Falls back to the raw delta when no databases are excluded. */ + var effectiveDeadlockDelta = health.FilteredDeadlockCount ?? deadlockDelta; + bool deadlocksExceeded = prefs.NotifyOnDeadlock - && deadlockDelta >= prefs.DeadlockThreshold; + && effectiveDeadlockDelta >= prefs.DeadlockThreshold; if (deadlocksExceeded) { @@ -1162,19 +1168,19 @@ await _emailAlertService.TrySendAlertEmailAsync( { _notificationService?.ShowDeadlockNotification( serverName, - (int)deadlockDelta); + (int)effectiveDeadlockDelta); _lastDeadlockAlert[serverId] = now; _emailAlertService.RecordAlert(serverId, serverName, "Deadlocks Detected", - deadlockDelta.ToString(), + effectiveDeadlockDelta.ToString(), prefs.DeadlockThreshold.ToString(), true, "tray"); - var deadlockContext = await BuildDeadlockContextAsync(databaseService); + var deadlockContext = await BuildDeadlockContextAsync(databaseService, prefs.AlertExcludedDatabases); await _emailAlertService.TrySendAlertEmailAsync( "Deadlocks Detected", serverName, - deadlockDelta.ToString(), + effectiveDeadlockDelta.ToString(), prefs.DeadlockThreshold.ToString(), serverId, deadlockContext); @@ -1264,15 +1270,23 @@ await _emailAlertService.TrySendAlertEmailAsync( } /* Long-running query alerts */ + var lrqList = health.LongRunningQueries; + if (prefs.AlertExcludedDatabases.Count > 0) + lrqList = lrqList + .Where(q => string.IsNullOrEmpty(q.DatabaseName) || + !prefs.AlertExcludedDatabases.Any(e => + string.Equals(e, q.DatabaseName, StringComparison.OrdinalIgnoreCase))) + .ToList(); + bool longRunningTriggered = prefs.NotifyOnLongRunningQueries - && health.LongRunningQueries.Count > 0; + && lrqList.Count > 0; if (longRunningTriggered) { _activeLongRunningQueryAlert[serverId] = true; if (!_lastLongRunningQueryAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= alertCooldown) { - var worst = health.LongRunningQueries[0]; + var worst = lrqList[0]; var elapsedMinutes = worst.ElapsedSeconds / 60; var preview = Truncate(worst.QueryText, 80); _notificationService?.ShowLongRunningQueryNotification( @@ -1283,12 +1297,12 @@ await _emailAlertService.TrySendAlertEmailAsync( $"Session #{worst.SessionId} running {elapsedMinutes}m", $"{prefs.LongRunningQueryThresholdMinutes}m", true, "tray"); - var lrqContext = BuildLongRunningQueryContext(health.LongRunningQueries); + var lrqContext = BuildLongRunningQueryContext(lrqList); await _emailAlertService.TrySendAlertEmailAsync( "Long-Running Query", serverName, - $"{health.LongRunningQueries.Count} query(s), longest {elapsedMinutes}m", + $"{lrqList.Count} query(s), longest {elapsedMinutes}m", $"{prefs.LongRunningQueryThresholdMinutes}m", serverId, lrqContext); @@ -1388,13 +1402,23 @@ private static string Truncate(string text, int maxLength = 300) return text.Length <= maxLength ? text : text.Substring(0, maxLength) + "..."; } - private static async Task BuildBlockingContextAsync(DatabaseService databaseService) + private static async Task BuildBlockingContextAsync(DatabaseService databaseService, List? excludedDatabases = null) { try { var events = await databaseService.GetBlockingEventsAsync(hoursBack: 1); if (events == null || events.Count == 0) return null; + if (excludedDatabases != null && excludedDatabases.Count > 0) + { + events = events + .Where(e => string.IsNullOrEmpty(e.DatabaseName) || + !excludedDatabases.Any(ex => + string.Equals(ex, e.DatabaseName, StringComparison.OrdinalIgnoreCase))) + .ToList(); + if (events.Count == 0) return null; + } + var context = new AlertContext(); var firstXml = (string?)null; @@ -1436,13 +1460,21 @@ private static string Truncate(string text, int maxLength = 300) } } - private static async Task BuildDeadlockContextAsync(DatabaseService databaseService) + private static async Task BuildDeadlockContextAsync(DatabaseService databaseService, List? excludedDatabases = null) { try { var deadlocks = await databaseService.GetDeadlocksAsync(hoursBack: 1); if (deadlocks == null || deadlocks.Count == 0) return null; + if (excludedDatabases != null && excludedDatabases.Count > 0) + { + deadlocks = deadlocks + .Where(d => !IsDeadlockExcluded(d, excludedDatabases)) + .ToList(); + if (deadlocks.Count == 0) return null; + } + var context = new AlertContext(); var firstGraph = (string?)null; @@ -1496,6 +1528,29 @@ private static string Truncate(string text, int maxLength = 300) } } + /// + /// Returns true if a deadlock should be excluded based on the deadlock graph XML. + /// A deadlock is only excluded when ALL process nodes have a currentdbname in the excluded list. + /// Cross-database deadlocks involving any non-excluded database will still be reported. + /// + private static bool IsDeadlockExcluded(DeadlockItem deadlock, List excludedDatabases) + { + if (string.IsNullOrEmpty(deadlock.DeadlockGraph)) return false; + try + { + var doc = XElement.Parse(deadlock.DeadlockGraph); + var dbNames = doc.Descendants("process") + .Select(p => p.Attribute("currentdbname")?.Value) + .Where(n => !string.IsNullOrEmpty(n)) + .Cast() + .ToList(); + if (dbNames.Count == 0) return false; + return dbNames.All(db => excludedDatabases.Any(e => + string.Equals(e, db, StringComparison.OrdinalIgnoreCase))); + } + catch { return false; } + } + private static AlertContext? BuildPoisonWaitContext(List triggeredWaits) { if (triggeredWaits.Count == 0) return null; diff --git a/Dashboard/Models/AlertHealthResult.cs b/Dashboard/Models/AlertHealthResult.cs index e51f232..8cb5e4c 100644 --- a/Dashboard/Models/AlertHealthResult.cs +++ b/Dashboard/Models/AlertHealthResult.cs @@ -22,6 +22,15 @@ public class AlertHealthResult public long TotalBlocked { get; set; } public decimal LongestBlockedSeconds { get; set; } public long DeadlockCount { get; set; } + + /// + /// Deadlock count for the alert window filtered by excluded databases. + /// Sourced from collect.blocking_deadlock_stats when excluded databases are configured. + /// When set, EvaluateAlertConditionsAsync uses this instead of the raw delta + /// from the server-wide performance counter, matching how blocking alerts filter. + /// Null when no databases are excluded (fall back to raw delta). + /// + public long? FilteredDeadlockCount { get; set; } public List PoisonWaits { get; set; } = new(); public List LongRunningQueries { get; set; } = new(); public TempDbSpaceInfo? TempDbSpace { get; set; } diff --git a/Dashboard/Models/UserPreferences.cs b/Dashboard/Models/UserPreferences.cs index a28a56c..aa2a3cd 100644 --- a/Dashboard/Models/UserPreferences.cs +++ b/Dashboard/Models/UserPreferences.cs @@ -138,6 +138,9 @@ private static string GetDefaultCsvSeparator() // Update check settings public bool CheckForUpdatesOnStartup { get; set; } = true; + // Alert database exclusions + public List AlertExcludedDatabases { get; set; } = new(); + // Alert suppression (persisted) public List SilencedServers { get; set; } = new(); public List SilencedServerTabs { get; set; } = new(); diff --git a/Dashboard/Services/DatabaseService.NocHealth.cs b/Dashboard/Services/DatabaseService.NocHealth.cs index 48435a1..e67133a 100644 --- a/Dashboard/Services/DatabaseService.NocHealth.cs +++ b/Dashboard/Services/DatabaseService.NocHealth.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Data; +using System.Linq; using System.Threading.Tasks; using Microsoft.Data.SqlClient; using PerformanceMonitorDashboard.Helpers; @@ -129,7 +130,8 @@ public async Task GetAlertHealthAsync( bool excludeSpServerDiagnostics = true, bool excludeWaitFor = true, bool excludeBackups = true, - bool excludeMiscWaits = true) + bool excludeMiscWaits = true, + IReadOnlyList? excludedDatabases = null) { var result = new AlertHealthResult(); @@ -141,14 +143,20 @@ public async Task GetAlertHealthAsync( result.IsOnline = true; var cpuTask = GetCpuPercentAsync(connection, engineEdition); - var blockingTask = GetBlockingValuesAsync(connection); + var blockingTask = GetBlockingValuesAsync(connection, excludedDatabases ?? Array.Empty()); var deadlockTask = GetDeadlockCountAsync(connection); + var filteredDeadlockTask = excludedDatabases?.Count > 0 + ? GetFilteredDeadlockCountAsync(connection, excludedDatabases) + : null; var poisonWaitTask = GetPoisonWaitDeltasAsync(connection); var longRunningTask = GetLongRunningQueriesAsync(connection, longRunningQueryThresholdMinutes, longRunningQueryMaxResults, excludeSpServerDiagnostics, excludeWaitFor, excludeBackups, excludeMiscWaits); var tempDbTask = GetTempDbSpaceAsync(connection); var anomalousJobTask = GetAnomalousJobsAsync(connection, longRunningJobMultiplier); - await Task.WhenAll(cpuTask, blockingTask, deadlockTask, poisonWaitTask, longRunningTask, tempDbTask, anomalousJobTask); + var allTasks = filteredDeadlockTask != null + ? new Task[] { cpuTask, blockingTask, deadlockTask, filteredDeadlockTask, poisonWaitTask, longRunningTask, tempDbTask, anomalousJobTask } + : new Task[] { cpuTask, blockingTask, deadlockTask, poisonWaitTask, longRunningTask, tempDbTask, anomalousJobTask }; + await Task.WhenAll(allTasks); var cpuResult = await cpuTask; result.CpuPercent = cpuResult.SqlCpu; @@ -159,6 +167,8 @@ public async Task GetAlertHealthAsync( result.LongestBlockedSeconds = blockingResult.LongestBlockedSeconds; result.DeadlockCount = await deadlockTask; + if (filteredDeadlockTask != null) + result.FilteredDeadlockCount = await filteredDeadlockTask; result.PoisonWaits = await poisonWaitTask; result.LongRunningQueries = await longRunningTask; result.TempDbSpace = await tempDbTask; @@ -177,9 +187,16 @@ public async Task GetAlertHealthAsync( /// Returns blocking values directly (without writing to a ServerHealthStatus). /// Used by GetAlertHealthAsync for lightweight alert checks. /// - private async Task<(long TotalBlocked, decimal LongestBlockedSeconds)> GetBlockingValuesAsync(SqlConnection connection) + private async Task<(long TotalBlocked, decimal LongestBlockedSeconds)> GetBlockingValuesAsync(SqlConnection connection, IReadOnlyList excludedDatabases) { - const string query = @"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + var dbFilter = ""; + var dbParams = new List(); + for (int i = 0; i < excludedDatabases.Count; i++) + dbParams.Add($"@exdb{i}"); + if (dbParams.Count > 0) + dbFilter = $"AND DB_NAME(s.dbid) NOT IN ({string.Join(", ", dbParams)})"; + + var query = $@"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT total_blocked = COUNT_BIG(*), @@ -187,12 +204,15 @@ public async Task GetAlertHealthAsync( FROM sys.sysprocesses AS s WHERE s.blocked <> 0 AND s.lastwaittype LIKE N'LCK%' + {dbFilter} OPTION(MAXDOP 1, RECOMPILE);"; try { using var cmd = new SqlCommand(query, connection); cmd.CommandTimeout = 10; + for (int i = 0; i < excludedDatabases.Count; i++) + cmd.Parameters.AddWithValue($"@exdb{i}", excludedDatabases[i]); using var reader = await cmd.ExecuteReaderAsync(); if (await reader.ReadAsync()) @@ -431,6 +451,49 @@ WHERE pc.counter_name LIKE N'Number of Deadlocks/sec%' } } + /// + /// Counts recent deadlocks from collect.blocking_deadlock_stats, excluding the specified databases. + /// Uses a 5-minute window matching the alert cooldown so each cooldown period + /// reflects only deadlocks from non-excluded databases. + /// This is the filtered equivalent of GetDeadlockCountAsync, which reads from + /// sys.dm_os_performance_counters and cannot be filtered by database. + /// + private async Task GetFilteredDeadlockCountAsync(SqlConnection connection, IReadOnlyList excludedDatabases) + { + var dbFilter = ""; + var dbParams = new List(); + for (int i = 0; i < excludedDatabases.Count; i++) + dbParams.Add($"@exdb{i}"); + if (dbParams.Count > 0) + dbFilter = $"AND bds.database_name NOT IN ({string.Join(", ", dbParams)})"; + + var query = $@"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + SELECT + filtered_deadlock_count = + COALESCE(SUM(bds.deadlock_count_delta), 0) + FROM collect.blocking_deadlock_stats AS bds + WHERE bds.collection_time >= DATEADD(MINUTE, -5, SYSUTCDATETIME()) + AND bds.deadlock_count_delta IS NOT NULL + {dbFilter} + OPTION(MAXDOP 1, RECOMPILE);"; + + try + { + using var cmd = new SqlCommand(query, connection); + cmd.CommandTimeout = 10; + for (int i = 0; i < excludedDatabases.Count; i++) + cmd.Parameters.AddWithValue($"@exdb{i}", excludedDatabases[i]); + var result = await cmd.ExecuteScalarAsync(); + return result is long l ? l : (result is int i2 ? (long)i2 : 0); + } + catch (Exception ex) + { + Logger.Warning($"Failed to get filtered deadlock count: {ex.Message}"); + return null; // Fall back to raw delta + } + } + private async Task GetCollectorStatusAsync(SqlConnection connection, ServerHealthStatus status) { const string query = @"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; diff --git a/Dashboard/SettingsWindow.xaml b/Dashboard/SettingsWindow.xaml index 1b5266a..9157522 100644 --- a/Dashboard/SettingsWindow.xaml +++ b/Dashboard/SettingsWindow.xaml @@ -277,6 +277,12 @@ + + + + + diff --git a/Dashboard/SettingsWindow.xaml.cs b/Dashboard/SettingsWindow.xaml.cs index 07364a8..fe67c18 100644 --- a/Dashboard/SettingsWindow.xaml.cs +++ b/Dashboard/SettingsWindow.xaml.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; using System.Windows; using System.Windows.Controls; using PerformanceMonitorDashboard.Helpers; @@ -167,6 +168,7 @@ private void LoadSettings() LrqExcludeWaitForCheckBox.IsChecked = prefs.LongRunningQueryExcludeWaitFor; LrqExcludeBackupsCheckBox.IsChecked = prefs.LongRunningQueryExcludeBackups; LrqExcludeMiscWaitsCheckBox.IsChecked = prefs.LongRunningQueryExcludeMiscWaits; + AlertExcludedDatabasesTextBox.Text = string.Join(", ", prefs.AlertExcludedDatabases); NotifyOnTempDbSpaceCheckBox.IsChecked = prefs.NotifyOnTempDbSpace; TempDbSpaceThresholdTextBox.Text = prefs.TempDbSpaceThresholdPercent.ToString(CultureInfo.InvariantCulture); NotifyOnLongRunningJobsCheckBox.IsChecked = prefs.NotifyOnLongRunningJobs; @@ -315,6 +317,7 @@ private void RestoreAlertDefaultsButton_Click(object sender, RoutedEventArgs e) LongRunningJobMultiplierTextBox.Text = "3"; AlertCooldownTextBox.Text = "5"; EmailCooldownTextBox.Text = "15"; + AlertExcludedDatabasesTextBox.Text = ""; UpdateAlertPreviewText(); } @@ -603,6 +606,11 @@ private void OkButton_Click(object sender, RoutedEventArgs e) prefs.LongRunningQueryExcludeWaitFor = LrqExcludeWaitForCheckBox.IsChecked == true; prefs.LongRunningQueryExcludeBackups = LrqExcludeBackupsCheckBox.IsChecked == true; prefs.LongRunningQueryExcludeMiscWaits = LrqExcludeMiscWaitsCheckBox.IsChecked == true; + prefs.AlertExcludedDatabases = AlertExcludedDatabasesTextBox.Text + .Split(',') + .Select(s => s.Trim()) + .Where(s => s.Length > 0) + .ToList(); prefs.NotifyOnTempDbSpace = NotifyOnTempDbSpaceCheckBox.IsChecked == true; if (int.TryParse(TempDbSpaceThresholdTextBox.Text, out int tempDbThreshold) && tempDbThreshold > 0 && tempDbThreshold <= 100) diff --git a/Lite/App.xaml.cs b/Lite/App.xaml.cs index 66a5f48..97d84a4 100644 --- a/Lite/App.xaml.cs +++ b/Lite/App.xaml.cs @@ -69,6 +69,7 @@ public partial class App : Application public static bool AlertLongRunningQueryExcludeWaitFor { get; set; } = true; public static bool AlertLongRunningQueryExcludeBackups { get; set; } = true; public static bool AlertLongRunningQueryExcludeMiscWaits { get; set; } = true; + public static List AlertExcludedDatabases { get; set; } = new(); public static bool AlertTempDbSpaceEnabled { get; set; } = true; public static int AlertTempDbSpaceThresholdPercent { get; set; } = 80; public static bool AlertLongRunningJobEnabled { get; set; } = true; @@ -254,6 +255,15 @@ public static void LoadAlertSettings() if (root.TryGetProperty("alert_long_running_query_exclude_waitfor", out v)) AlertLongRunningQueryExcludeWaitFor = v.GetBoolean(); if (root.TryGetProperty("alert_long_running_query_exclude_backups", out v)) AlertLongRunningQueryExcludeBackups = v.GetBoolean(); if (root.TryGetProperty("alert_long_running_query_exclude_misc_waits", out v)) AlertLongRunningQueryExcludeMiscWaits = v.GetBoolean(); + if (root.TryGetProperty("alert_excluded_databases", out v) && v.ValueKind == System.Text.Json.JsonValueKind.Array) + { + AlertExcludedDatabases = new List(); + foreach (var elem in v.EnumerateArray()) + { + var db = elem.GetString(); + if (!string.IsNullOrWhiteSpace(db)) AlertExcludedDatabases.Add(db); + } + } if (root.TryGetProperty("alert_tempdb_space_enabled", out v)) AlertTempDbSpaceEnabled = v.GetBoolean(); if (root.TryGetProperty("alert_tempdb_space_threshold_percent", out v)) AlertTempDbSpaceThresholdPercent = v.GetInt32(); if (root.TryGetProperty("alert_long_running_job_enabled", out v)) AlertLongRunningJobEnabled = v.GetBoolean(); diff --git a/Lite/MainWindow.xaml.cs b/Lite/MainWindow.xaml.cs index eef0dbb..8c6e4ec 100644 --- a/Lite/MainWindow.xaml.cs +++ b/Lite/MainWindow.xaml.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using System.Linq; +using System.Xml.Linq; using System.Threading; using System.Windows; using System.Windows.Controls; @@ -1001,8 +1002,26 @@ await _emailAlertService.TrySendAlertEmailAsync( } /* Blocking alerts */ + var effectiveBlockingCount = summary.BlockingCount; + if (App.AlertBlockingEnabled && App.AlertExcludedDatabases.Count > 0 + && summary.BlockingCount >= App.AlertBlockingThreshold && _dataService != null) + { + try + { + var blockingRows = await _dataService.GetRecentBlockedProcessReportsAsync(summary.ServerId, hoursBack: 1); + effectiveBlockingCount = blockingRows + .Count(r => string.IsNullOrEmpty(r.DatabaseName) || + !App.AlertExcludedDatabases.Any(e => + string.Equals(e, r.DatabaseName, StringComparison.OrdinalIgnoreCase))); + } + catch (Exception ex) + { + AppLogger.Error("Alerts", $"Failed to filter blocking count for {summary.DisplayName}: {ex.Message}"); + } + } + bool blockingExceeded = App.AlertBlockingEnabled - && summary.BlockingCount >= App.AlertBlockingThreshold; + && effectiveBlockingCount >= App.AlertBlockingThreshold; if (blockingExceeded) { @@ -1011,7 +1030,7 @@ await _emailAlertService.TrySendAlertEmailAsync( { _trayService.ShowNotification( "Blocking Detected", - $"{summary.DisplayName}: {summary.BlockingCount} blocking session(s)", + $"{summary.DisplayName}: {effectiveBlockingCount} blocking session(s)", Hardcodet.Wpf.TaskbarNotification.BalloonIcon.Warning); _lastBlockingAlert[key] = now; @@ -1020,7 +1039,7 @@ await _emailAlertService.TrySendAlertEmailAsync( await _emailAlertService.TrySendAlertEmailAsync( "Blocking Detected", summary.DisplayName, - summary.BlockingCount.ToString(), + effectiveBlockingCount.ToString(), App.AlertBlockingThreshold.ToString(), summary.ServerId, blockingContext); @@ -1036,8 +1055,24 @@ await _emailAlertService.TrySendAlertEmailAsync( } /* Deadlock alerts */ + var effectiveDeadlockCount = summary.DeadlockCount; + if (App.AlertDeadlockEnabled && App.AlertExcludedDatabases.Count > 0 + && summary.DeadlockCount >= App.AlertDeadlockThreshold && _dataService != null) + { + try + { + var deadlockRows = await _dataService.GetRecentDeadlocksAsync(summary.ServerId, hoursBack: 1); + effectiveDeadlockCount = deadlockRows + .Count(r => !IsDeadlockExcluded(r, App.AlertExcludedDatabases)); + } + catch (Exception ex) + { + AppLogger.Error("Alerts", $"Failed to filter deadlock count for {summary.DisplayName}: {ex.Message}"); + } + } + bool deadlocksExceeded = App.AlertDeadlockEnabled - && summary.DeadlockCount >= App.AlertDeadlockThreshold; + && effectiveDeadlockCount >= App.AlertDeadlockThreshold; if (deadlocksExceeded) { @@ -1046,7 +1081,7 @@ await _emailAlertService.TrySendAlertEmailAsync( { _trayService.ShowNotification( "Deadlocks Detected", - $"{summary.DisplayName}: {summary.DeadlockCount} deadlock(s) in the last hour", + $"{summary.DisplayName}: {effectiveDeadlockCount} deadlock(s) in the last hour", Hardcodet.Wpf.TaskbarNotification.BalloonIcon.Error); _lastDeadlockAlert[key] = now; @@ -1055,7 +1090,7 @@ await _emailAlertService.TrySendAlertEmailAsync( await _emailAlertService.TrySendAlertEmailAsync( "Deadlocks Detected", summary.DisplayName, - summary.DeadlockCount.ToString(), + effectiveDeadlockCount.ToString(), App.AlertDeadlockThreshold.ToString(), summary.ServerId, deadlockContext); @@ -1126,6 +1161,15 @@ await _emailAlertService.TrySendAlertEmailAsync( { var longRunning = await _dataService.GetLongRunningQueriesAsync(summary.ServerId, App.AlertLongRunningQueryThresholdMinutes, App.AlertLongRunningQueryMaxResults, App.AlertLongRunningQueryExcludeSpServerDiagnostics, App.AlertLongRunningQueryExcludeWaitFor, App.AlertLongRunningQueryExcludeBackups, App.AlertLongRunningQueryExcludeMiscWaits); + if (App.AlertExcludedDatabases.Count > 0) + { + longRunning = longRunning + .Where(q => string.IsNullOrEmpty(q.DatabaseName) || + !App.AlertExcludedDatabases.Any(e => + string.Equals(e, q.DatabaseName, StringComparison.OrdinalIgnoreCase))) + .ToList(); + } + if (longRunning.Count > 0) { _activeLongRunningQueryAlert[key] = true; @@ -1283,6 +1327,16 @@ private static string TruncateText(string text, int maxLength = 300) var events = await _dataService.GetRecentBlockedProcessReportsAsync(serverId, hoursBack: 1); if (events == null || events.Count == 0) return null; + if (App.AlertExcludedDatabases.Count > 0) + { + events = events + .Where(e => string.IsNullOrEmpty(e.DatabaseName) || + !App.AlertExcludedDatabases.Any(ex => + string.Equals(ex, e.DatabaseName, StringComparison.OrdinalIgnoreCase))) + .ToList(); + if (events.Count == 0) return null; + } + var context = new AlertContext(); var firstXml = (string?)null; @@ -1333,6 +1387,14 @@ private static string TruncateText(string text, int maxLength = 300) var deadlocks = await _dataService.GetRecentDeadlocksAsync(serverId, hoursBack: 1); if (deadlocks == null || deadlocks.Count == 0) return null; + if (App.AlertExcludedDatabases.Count > 0) + { + deadlocks = deadlocks + .Where(d => !IsDeadlockExcluded(d, App.AlertExcludedDatabases)) + .ToList(); + if (deadlocks.Count == 0) return null; + } + var context = new AlertContext(); var firstGraph = (string?)null; @@ -1369,6 +1431,24 @@ private static string TruncateText(string text, int maxLength = 300) } } + private static bool IsDeadlockExcluded(DeadlockRow row, List excludedDatabases) + { + if (string.IsNullOrEmpty(row.DeadlockGraphXml)) return false; + try + { + var doc = XElement.Parse(row.DeadlockGraphXml); + var dbNames = doc.Descendants("process") + .Select(p => p.Attribute("currentdbname")?.Value) + .Where(n => !string.IsNullOrEmpty(n)) + .Cast() + .ToList(); + if (dbNames.Count == 0) return false; + return dbNames.All(db => excludedDatabases.Any(e => + string.Equals(e, db, StringComparison.OrdinalIgnoreCase))); + } + catch { return false; } + } + private static AlertContext? BuildPoisonWaitContext(List triggeredWaits) { if (triggeredWaits.Count == 0) return null; diff --git a/Lite/Windows/SettingsWindow.xaml b/Lite/Windows/SettingsWindow.xaml index 284b5c2..ba43707 100644 --- a/Lite/Windows/SettingsWindow.xaml +++ b/Lite/Windows/SettingsWindow.xaml @@ -234,6 +234,15 @@ + + + + + diff --git a/Lite/Windows/SettingsWindow.xaml.cs b/Lite/Windows/SettingsWindow.xaml.cs index 5408d6b..61296c6 100644 --- a/Lite/Windows/SettingsWindow.xaml.cs +++ b/Lite/Windows/SettingsWindow.xaml.cs @@ -8,6 +8,7 @@ using System; using System.IO; +using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; using System.Windows; @@ -455,6 +456,7 @@ private void LoadAlertSettings() LrqExcludeWaitForCheckBox.IsChecked = App.AlertLongRunningQueryExcludeWaitFor; LrqExcludeBackupsCheckBox.IsChecked = App.AlertLongRunningQueryExcludeBackups; LrqExcludeMiscWaitsCheckBox.IsChecked = App.AlertLongRunningQueryExcludeMiscWaits; + AlertExcludedDatabasesBox.Text = string.Join(", ", App.AlertExcludedDatabases); AlertTempDbSpaceCheckBox.IsChecked = App.AlertTempDbSpaceEnabled; AlertTempDbSpaceThresholdBox.Text = App.AlertTempDbSpaceThresholdPercent.ToString(); AlertLongRunningJobCheckBox.IsChecked = App.AlertLongRunningJobEnabled; @@ -490,6 +492,11 @@ private bool SaveAlertSettings() App.AlertLongRunningQueryExcludeWaitFor = LrqExcludeWaitForCheckBox.IsChecked == true; App.AlertLongRunningQueryExcludeBackups = LrqExcludeBackupsCheckBox.IsChecked == true; App.AlertLongRunningQueryExcludeMiscWaits = LrqExcludeMiscWaitsCheckBox.IsChecked == true; + App.AlertExcludedDatabases = AlertExcludedDatabasesBox.Text + .Split(',') + .Select(s => s.Trim()) + .Where(s => s.Length > 0) + .ToList(); App.AlertTempDbSpaceEnabled = AlertTempDbSpaceCheckBox.IsChecked == true; if (int.TryParse(AlertTempDbSpaceThresholdBox.Text, out var tempDb) && tempDb > 0 && tempDb <= 100) App.AlertTempDbSpaceThresholdPercent = tempDb; @@ -538,6 +545,9 @@ private bool SaveAlertSettings() root["alert_long_running_query_exclude_waitfor"] = App.AlertLongRunningQueryExcludeWaitFor; root["alert_long_running_query_exclude_backups"] = App.AlertLongRunningQueryExcludeBackups; root["alert_long_running_query_exclude_misc_waits"] = App.AlertLongRunningQueryExcludeMiscWaits; + var dbArray = new System.Text.Json.Nodes.JsonArray(); + foreach (var db in App.AlertExcludedDatabases) dbArray.Add(db); + root["alert_excluded_databases"] = dbArray; root["alert_tempdb_space_enabled"] = App.AlertTempDbSpaceEnabled; root["alert_tempdb_space_threshold_percent"] = App.AlertTempDbSpaceThresholdPercent; root["alert_long_running_job_enabled"] = App.AlertLongRunningJobEnabled; @@ -581,6 +591,7 @@ private void RestoreAlertDefaultsButton_Click(object sender, RoutedEventArgs e) AlertLongRunningJobMultiplierBox.Text = "3"; AlertCooldownBox.Text = "5"; EmailCooldownBox.Text = "15"; + AlertExcludedDatabasesBox.Text = ""; UpdateAlertPreviewText(); }