From 2213d868c8029ddea1970986f11c13a38b1207d8 Mon Sep 17 00:00:00 2001 From: Hannah Vernon Date: Fri, 27 Feb 2026 14:26:39 -0600 Subject: [PATCH 01/27] Added exclusions in GetLongRunningQueriesAsync() method for SP_SERVER_DIAGNOSTIC wait types, and WAITFOR wait types. --- Dashboard/Services/DatabaseService.NocHealth.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Dashboard/Services/DatabaseService.NocHealth.cs b/Dashboard/Services/DatabaseService.NocHealth.cs index 7a98b65..5f3e5d5 100644 --- a/Dashboard/Services/DatabaseService.NocHealth.cs +++ b/Dashboard/Services/DatabaseService.NocHealth.cs @@ -603,9 +603,11 @@ ORDER BY collection_time DESC /// Gets currently running queries that exceed the duration threshold. /// Uses live DMV data (sys.dm_exec_requests) for immediate detection. /// - private async Task> GetLongRunningQueriesAsync(SqlConnection connection, int thresholdMinutes) + private async Task> GetLongRunningQueriesAsync(SqlConnection connection, int thresholdMinutes, bool excludeSpServerDiagnostics = true) { - const string query = @"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + string spServerDiagnosticsFilter = excludeSpServerDiagnostics ? "AND r.wait_type NOT LIKE N'%SP_SERVER_DIAGNOSTICS%'" : ""; + + string query = @$"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT TOP (5) r.session_id, @@ -621,10 +623,11 @@ SELECT TOP (5) FROM sys.dm_exec_requests AS r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t JOIN sys.dm_exec_sessions AS s ON s.session_id = r.session_id - WHERE r.session_id > 50 - AND r.total_elapsed_time >= @thresholdMs - AND t.text NOT LIKE N'%waitfor delay%' - AND t.text NOT LIKE N'%waitfor receive%' + WHERE + r.session_id > 50 + {spServerDiagnosticsFilter} + AND r.total_elapsed_time >= @thresholdMs + AND r.wait_type <> N'WAITFOR' ORDER BY r.total_elapsed_time DESC OPTION(MAXDOP 1, RECOMPILE);"; From 9b249fcfd5243448b1dd54bb6928f75b7aa0f034 Mon Sep 17 00:00:00 2001 From: Hannah Vernon Date: Fri, 27 Feb 2026 14:35:43 -0600 Subject: [PATCH 02/27] Added TOP parameter for query in GetLongRunningQueriesAsync() method to allow future configurability on the number of long-running queries returned. Added optional parameter to control display of WAITFOR types in future. --- .../Services/DatabaseService.NocHealth.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Dashboard/Services/DatabaseService.NocHealth.cs b/Dashboard/Services/DatabaseService.NocHealth.cs index 5f3e5d5..fa1e4fe 100644 --- a/Dashboard/Services/DatabaseService.NocHealth.cs +++ b/Dashboard/Services/DatabaseService.NocHealth.cs @@ -603,13 +603,26 @@ ORDER BY collection_time DESC /// Gets currently running queries that exceed the duration threshold. /// Uses live DMV data (sys.dm_exec_requests) for immediate detection. /// - private async Task> GetLongRunningQueriesAsync(SqlConnection connection, int thresholdMinutes, bool excludeSpServerDiagnostics = true) + private async Task> GetLongRunningQueriesAsync(SqlConnection connection, int thresholdMinutes, bool excludeSpServerDiagnostics = true, bool excludeWaitFor = true, int maxLongRunningQueryCount = 5) { + // Exclude internal SP_SERVER_DIAGNOSTICS queries by default, as they often run long and aren't actionable. string spServerDiagnosticsFilter = excludeSpServerDiagnostics ? "AND r.wait_type NOT LIKE N'%SP_SERVER_DIAGNOSTICS%'" : ""; + // Exclude WAITFOR queries by default, as they can run indefinitely and may not indicate a problem. + string waitForFilter = excludeWaitFor ? "AND r.wait_type <> N'WAITFOR'" : ""; + + // Sanity check to prevent SQL syntax errors + if (maxLongRunningQueryCount <= 5) + { + maxLongRunningQueryCount = 5; + }; + + // Use TOP to limit the number of long-running queries returned, with a reasonable default of 5. + string LongRunningQueryCount = "TOP (" + maxLongRunningQueryCount + ")"; + string query = @$"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT TOP (5) + SELECT {LongRunningQueryCount} r.session_id, DB_NAME(r.database_id) AS database_name, SUBSTRING(t.text, 1, 300) AS query_text, @@ -625,9 +638,9 @@ CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t JOIN sys.dm_exec_sessions AS s ON s.session_id = r.session_id WHERE r.session_id > 50 - {spServerDiagnosticsFilter} AND r.total_elapsed_time >= @thresholdMs - AND r.wait_type <> N'WAITFOR' + {spServerDiagnosticsFilter} + {waitForFilter} ORDER BY r.total_elapsed_time DESC OPTION(MAXDOP 1, RECOMPILE);"; From adefd2527d3dc98e34430b6fd9dc39a1a2f7318a Mon Sep 17 00:00:00 2001 From: Hannah Vernon Date: Fri, 27 Feb 2026 14:43:11 -0600 Subject: [PATCH 03/27] Replaced waitForFilter string constructor with C# string interpolation instead of janky string addition. --- Dashboard/Services/DatabaseService.NocHealth.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dashboard/Services/DatabaseService.NocHealth.cs b/Dashboard/Services/DatabaseService.NocHealth.cs index fa1e4fe..9f34d3c 100644 --- a/Dashboard/Services/DatabaseService.NocHealth.cs +++ b/Dashboard/Services/DatabaseService.NocHealth.cs @@ -612,13 +612,13 @@ private async Task> GetLongRunningQueriesAsync(SqlCon string waitForFilter = excludeWaitFor ? "AND r.wait_type <> N'WAITFOR'" : ""; // Sanity check to prevent SQL syntax errors - if (maxLongRunningQueryCount <= 5) + if (maxLongRunningQueryCount <= 5) { maxLongRunningQueryCount = 5; }; // Use TOP to limit the number of long-running queries returned, with a reasonable default of 5. - string LongRunningQueryCount = "TOP (" + maxLongRunningQueryCount + ")"; + string LongRunningQueryCount = @$"TOP ({maxLongRunningQueryCount})"; string query = @$"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; From f75bb2da2cbaddb418a4a146791d88e6cb39b869 Mon Sep 17 00:00:00 2001 From: Hannah Vernon Date: Fri, 27 Feb 2026 15:38:57 -0600 Subject: [PATCH 04/27] Added exclusion for backup-related waits to GetLongRunningQueriesAsync() method. Removed System.Collections.Generic using statement as it is unnecessary. --- Dashboard/Controls/LandingPage.xaml.cs | 2 +- Dashboard/Services/DatabaseService.NocHealth.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Dashboard/Controls/LandingPage.xaml.cs b/Dashboard/Controls/LandingPage.xaml.cs index b0816de..7978aff 100644 --- a/Dashboard/Controls/LandingPage.xaml.cs +++ b/Dashboard/Controls/LandingPage.xaml.cs @@ -7,7 +7,7 @@ */ using System; -using System.Collections.Generic; +//using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; diff --git a/Dashboard/Services/DatabaseService.NocHealth.cs b/Dashboard/Services/DatabaseService.NocHealth.cs index 9f34d3c..a90b8c5 100644 --- a/Dashboard/Services/DatabaseService.NocHealth.cs +++ b/Dashboard/Services/DatabaseService.NocHealth.cs @@ -603,7 +603,7 @@ ORDER BY collection_time DESC /// Gets currently running queries that exceed the duration threshold. /// Uses live DMV data (sys.dm_exec_requests) for immediate detection. /// - private async Task> GetLongRunningQueriesAsync(SqlConnection connection, int thresholdMinutes, bool excludeSpServerDiagnostics = true, bool excludeWaitFor = true, int maxLongRunningQueryCount = 5) + private async Task> GetLongRunningQueriesAsync(SqlConnection connection, int thresholdMinutes, bool excludeSpServerDiagnostics = true, bool excludeWaitFor = true, int maxLongRunningQueryCount = 5, bool excludeBackupWaits = true) { // Exclude internal SP_SERVER_DIAGNOSTICS queries by default, as they often run long and aren't actionable. string spServerDiagnosticsFilter = excludeSpServerDiagnostics ? "AND r.wait_type NOT LIKE N'%SP_SERVER_DIAGNOSTICS%'" : ""; @@ -611,6 +611,9 @@ private async Task> GetLongRunningQueriesAsync(SqlCon // Exclude WAITFOR queries by default, as they can run indefinitely and may not indicate a problem. string waitForFilter = excludeWaitFor ? "AND r.wait_type <> N'WAITFOR'" : ""; + // Exclude backup waits if specified, as they can run long and aren't typically actionable in this context. + string backupsFilter = excludeBackupWaits ? "AND r.wait_type NOT IN (N'BACKUPTHREAD', N'BACKUPIO')" : ""; + // Sanity check to prevent SQL syntax errors if (maxLongRunningQueryCount <= 5) { @@ -641,6 +644,7 @@ CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t AND r.total_elapsed_time >= @thresholdMs {spServerDiagnosticsFilter} {waitForFilter} + {backupsFilter} ORDER BY r.total_elapsed_time DESC OPTION(MAXDOP 1, RECOMPILE);"; From fd9b497878fbbc39bd43564801e5fe6721e32d88 Mon Sep 17 00:00:00 2001 From: Hannah Vernon Date: Mon, 2 Mar 2026 10:54:24 -0600 Subject: [PATCH 05/27] Reverted Controls/LandingPage.xaml.cs --- Dashboard/Controls/LandingPage.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dashboard/Controls/LandingPage.xaml.cs b/Dashboard/Controls/LandingPage.xaml.cs index 7978aff..b0816de 100644 --- a/Dashboard/Controls/LandingPage.xaml.cs +++ b/Dashboard/Controls/LandingPage.xaml.cs @@ -7,7 +7,7 @@ */ using System; -//using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; From a98ef7bb6cc8ecfe369081b801e82c9a1a6e77e0 Mon Sep 17 00:00:00 2001 From: Hannah Vernon Date: Mon, 2 Mar 2026 10:57:48 -0600 Subject: [PATCH 06/27] Added BROKER_RECEIVE_WAITFOR wait type to waitforFilter exclusions. Added miscWaitsFilter to exclude XE_LIVE_TARGET_TVF waits. Removed unused parameters. Corrected minimum value for maxLongRunningQueryCount (minimum 1 instead of 5). --- .../Services/DatabaseService.NocHealth.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/Dashboard/Services/DatabaseService.NocHealth.cs b/Dashboard/Services/DatabaseService.NocHealth.cs index a90b8c5..26cb36f 100644 --- a/Dashboard/Services/DatabaseService.NocHealth.cs +++ b/Dashboard/Services/DatabaseService.NocHealth.cs @@ -603,29 +603,24 @@ ORDER BY collection_time DESC /// Gets currently running queries that exceed the duration threshold. /// Uses live DMV data (sys.dm_exec_requests) for immediate detection. /// - private async Task> GetLongRunningQueriesAsync(SqlConnection connection, int thresholdMinutes, bool excludeSpServerDiagnostics = true, bool excludeWaitFor = true, int maxLongRunningQueryCount = 5, bool excludeBackupWaits = true) + private async Task> GetLongRunningQueriesAsync(SqlConnection connection, int thresholdMinutes) { + // Exclude internal SP_SERVER_DIAGNOSTICS queries by default, as they often run long and aren't actionable. - string spServerDiagnosticsFilter = excludeSpServerDiagnostics ? "AND r.wait_type NOT LIKE N'%SP_SERVER_DIAGNOSTICS%'" : ""; + string spServerDiagnosticsFilter = "AND r.wait_type NOT LIKE N'%SP_SERVER_DIAGNOSTICS%'"; // Exclude WAITFOR queries by default, as they can run indefinitely and may not indicate a problem. - string waitForFilter = excludeWaitFor ? "AND r.wait_type <> N'WAITFOR'" : ""; + string waitForFilter = "AND r.wait_type NOT IN (N'WAITFOR', N'BROKER_RECEIVE_WAITFOR')"; // Exclude backup waits if specified, as they can run long and aren't typically actionable in this context. - string backupsFilter = excludeBackupWaits ? "AND r.wait_type NOT IN (N'BACKUPTHREAD', N'BACKUPIO')" : ""; - - // Sanity check to prevent SQL syntax errors - if (maxLongRunningQueryCount <= 5) - { - maxLongRunningQueryCount = 5; - }; + string backupsFilter = "AND r.wait_type NOT IN (N'BACKUPTHREAD', N'BACKUPIO')"; - // Use TOP to limit the number of long-running queries returned, with a reasonable default of 5. - string LongRunningQueryCount = @$"TOP ({maxLongRunningQueryCount})"; + // Exclude miscellaneous wait type that aren't typically actionable + string miscWaitsFilter = "AND r.wait_type NOT IN (N'XE_LIVE_TARGET_TVF')"; string query = @$"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT {LongRunningQueryCount} + SELECT TOP(5) r.session_id, DB_NAME(r.database_id) AS database_name, SUBSTRING(t.text, 1, 300) AS query_text, @@ -645,6 +640,7 @@ CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t {spServerDiagnosticsFilter} {waitForFilter} {backupsFilter} + {miscWaitsFilter} ORDER BY r.total_elapsed_time DESC OPTION(MAXDOP 1, RECOMPILE);"; From ab1c47df04c979dfa49b7de08a22fc16c386c9c9 Mon Sep 17 00:00:00 2001 From: Hannah Vernon Date: Mon, 2 Mar 2026 11:21:59 -0600 Subject: [PATCH 07/27] Added filtering for SP_SERVER_DIAGNOSTICS, WAITFOR, BROKER_RECEIVE_WAITFOR, BACKUPTHREAD, BACKUPIO, and XE_LIVE_TARGET_TVF wait types in Lite/Services/LocalDataService.WaitStats.cs --- Lite/Services/LocalDataService.WaitStats.cs | 54 +++++++++++++-------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/Lite/Services/LocalDataService.WaitStats.cs b/Lite/Services/LocalDataService.WaitStats.cs index f7f1a31..b330243 100644 --- a/Lite/Services/LocalDataService.WaitStats.cs +++ b/Lite/Services/LocalDataService.WaitStats.cs @@ -202,26 +202,40 @@ public async Task> GetLongRunningQueriesAsync(int ser var thresholdMs = (long)thresholdMinutes * 60 * 1000; - command.CommandText = @" -SELECT - session_id, - database_name, - SUBSTRING(query_text, 1, 300) AS query_text, - total_elapsed_time_ms / 1000 AS elapsed_seconds, - cpu_time_ms, - reads, - writes, - wait_type, - blocking_session_id -FROM v_query_snapshots -WHERE server_id = $1 -AND collection_time = (SELECT MAX(collection_time) FROM v_query_snapshots WHERE server_id = $1) -AND session_id > 50 -AND total_elapsed_time_ms >= $2 -AND query_text NOT LIKE '%waitfor delay%' -AND query_text NOT LIKE '%waitfor receive%' -ORDER BY total_elapsed_time_ms DESC -LIMIT 5"; + // Exclude internal SP_SERVER_DIAGNOSTICS queries by default, as they often run long and aren't actionable. + string spServerDiagnosticsFilter = "AND r.wait_type NOT LIKE N'%SP_SERVER_DIAGNOSTICS%'"; + + // Exclude WAITFOR queries by default, as they can run indefinitely and may not indicate a problem. + string waitForFilter = "AND r.wait_type NOT IN (N'WAITFOR', N'BROKER_RECEIVE_WAITFOR')"; + + // Exclude backup waits if specified, as they can run long and aren't typically actionable in this context. + string backupsFilter = "AND r.wait_type NOT IN (N'BACKUPTHREAD', N'BACKUPIO')"; + + // Exclude miscellaneous wait type that aren't typically actionable + string miscWaitsFilter = "AND r.wait_type NOT IN (N'XE_LIVE_TARGET_TVF')"; + + command.CommandText = @$" + SELECT + session_id, + database_name, + SUBSTRING(query_text, 1, 300) AS query_text, + total_elapsed_time_ms / 1000 AS elapsed_seconds, + cpu_time_ms, + reads, + writes, + wait_type, + blocking_session_id + FROM v_query_snapshots + WHERE server_id = $1 + AND collection_time = (SELECT MAX(collection_time) FROM v_query_snapshots WHERE server_id = $1) + AND session_id > 50 + {spServerDiagnosticsFilter} + {waitForFilter} + {backupsFilter} + {miscWaitsFilter} + AND total_elapsed_time_ms >= $2 + ORDER BY total_elapsed_time_ms DESC + LIMIT 5;"; command.Parameters.Add(new DuckDBParameter { Value = serverId }); command.Parameters.Add(new DuckDBParameter { Value = thresholdMs }); From 8ac49037edccaff0fe3cd2d1e2a2a7d2e287691e Mon Sep 17 00:00:00 2001 From: Hannah Vernon Date: Tue, 3 Mar 2026 15:02:20 -0600 Subject: [PATCH 08/27] Add configurable max results setting for long-running queries Adds LongRunningQueryMaxResults to UserPreferences (default 5) and exposes it in the Settings UI alongside the existing duration threshold. Threads the value through GetAlertHealthAsync and GetLongRunningQueriesAsync to replace the hardcoded TOP(5) in the DMV query. Co-Authored-By: Claude Sonnet 4.6 --- Dashboard/MainWindow.xaml.cs | 2 +- Dashboard/Models/UserPreferences.cs | 1 + Dashboard/Services/DatabaseService.NocHealth.cs | 8 ++++---- Dashboard/SettingsWindow.xaml | 8 +++++++- Dashboard/SettingsWindow.xaml.cs | 10 ++++++++++ 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/Dashboard/MainWindow.xaml.cs b/Dashboard/MainWindow.xaml.cs index 573ac82..ba43d99 100644 --- a/Dashboard/MainWindow.xaml.cs +++ b/Dashboard/MainWindow.xaml.cs @@ -1051,7 +1051,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); + var health = await databaseService.GetAlertHealthAsync(connStatus.SqlEngineEdition, prefs.LongRunningQueryThresholdMinutes, prefs.LongRunningJobMultiplier, prefs.LongRunningQueryMaxResults); if (health.IsOnline) { diff --git a/Dashboard/Models/UserPreferences.cs b/Dashboard/Models/UserPreferences.cs index d48b553..7a498e0 100644 --- a/Dashboard/Models/UserPreferences.cs +++ b/Dashboard/Models/UserPreferences.cs @@ -82,6 +82,7 @@ public class UserPreferences public int PoisonWaitThresholdMs { get; set; } = 500; // Alert when avg ms per wait > X public bool NotifyOnLongRunningQueries { get; set; } = true; public int LongRunningQueryThresholdMinutes { get; set; } = 30; // Alert when query runs > X minutes + public int LongRunningQueryMaxResults { get; set; } = 5; // Max number of long-running queries returned per check public bool NotifyOnTempDbSpace { get; set; } = true; public int TempDbSpaceThresholdPercent { get; set; } = 80; // Alert when TempDB used > X% public bool NotifyOnLongRunningJobs { get; set; } = true; diff --git a/Dashboard/Services/DatabaseService.NocHealth.cs b/Dashboard/Services/DatabaseService.NocHealth.cs index 26cb36f..4e2b126 100644 --- a/Dashboard/Services/DatabaseService.NocHealth.cs +++ b/Dashboard/Services/DatabaseService.NocHealth.cs @@ -121,7 +121,7 @@ public async Task RefreshNocHealthStatusAsync(ServerHealthStatus status, int eng /// Lightweight alert-only health check. Runs 3 queries instead of 9. /// Used by MainWindow's independent alert timer. /// - public async Task GetAlertHealthAsync(int engineEdition = 0, int longRunningQueryThresholdMinutes = 30, int longRunningJobMultiplier = 3) + public async Task GetAlertHealthAsync(int engineEdition = 0, int longRunningQueryThresholdMinutes = 30, int longRunningJobMultiplier = 3, int longRunningQueryMaxResults = 5) { var result = new AlertHealthResult(); @@ -136,7 +136,7 @@ public async Task GetAlertHealthAsync(int engineEdition = 0, var blockingTask = GetBlockingValuesAsync(connection); var deadlockTask = GetDeadlockCountAsync(connection); var poisonWaitTask = GetPoisonWaitDeltasAsync(connection); - var longRunningTask = GetLongRunningQueriesAsync(connection, longRunningQueryThresholdMinutes); + var longRunningTask = GetLongRunningQueriesAsync(connection, longRunningQueryThresholdMinutes, longRunningQueryMaxResults); var tempDbTask = GetTempDbSpaceAsync(connection); var anomalousJobTask = GetAnomalousJobsAsync(connection, longRunningJobMultiplier); @@ -603,7 +603,7 @@ ORDER BY collection_time DESC /// Gets currently running queries that exceed the duration threshold. /// Uses live DMV data (sys.dm_exec_requests) for immediate detection. /// - private async Task> GetLongRunningQueriesAsync(SqlConnection connection, int thresholdMinutes) + private async Task> GetLongRunningQueriesAsync(SqlConnection connection, int thresholdMinutes, int maxResults = 5) { // Exclude internal SP_SERVER_DIAGNOSTICS queries by default, as they often run long and aren't actionable. @@ -620,7 +620,7 @@ private async Task> GetLongRunningQueriesAsync(SqlCon string query = @$"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT TOP(5) + SELECT TOP({maxResults}) r.session_id, DB_NAME(r.database_id) AS database_name, SUBSTRING(t.text, 1, 300) AS query_text, diff --git a/Dashboard/SettingsWindow.xaml b/Dashboard/SettingsWindow.xaml index 9e4a3a9..39aa691 100644 --- a/Dashboard/SettingsWindow.xaml +++ b/Dashboard/SettingsWindow.xaml @@ -206,7 +206,13 @@ Margin="8,0,8,0" VerticalAlignment="Center" TextAlignment="Center"/> - + + + + 0) + { + prefs.LongRunningQueryMaxResults = lrqMaxResults; + } + else + { + validationErrors.Add("Long-running query max results must be a positive number"); + } + prefs.NotifyOnTempDbSpace = NotifyOnTempDbSpaceCheckBox.IsChecked == true; if (int.TryParse(TempDbSpaceThresholdTextBox.Text, out int tempDbThreshold) && tempDbThreshold > 0 && tempDbThreshold <= 100) prefs.TempDbSpaceThresholdPercent = tempDbThreshold; From c5476dd3f6214774334b52576f50eca9be764c1e Mon Sep 17 00:00:00 2001 From: Hannah Vernon Date: Tue, 3 Mar 2026 15:13:46 -0600 Subject: [PATCH 09/27] Apply max results validation and Lite parity for long-running queries Adds Math.Clamp(1, int.MaxValue) guard to GetLongRunningQueriesAsync in both Dashboard and Lite, and updates the Dashboard settings validation to use an explicit range check with a descriptive error message. Mirrors the LongRunningQueryMaxResults setting across the Lite project: adds the App property, loads/saves it to settings.json, exposes it in the Lite Settings UI, and passes it through to GetLongRunningQueriesAsync to replace the hardcoded LIMIT 5. Co-Authored-By: Claude Sonnet 4.6 --- Dashboard/Services/DatabaseService.NocHealth.cs | 1 + Dashboard/SettingsWindow.xaml.cs | 4 ++-- Lite/App.xaml.cs | 2 ++ Lite/MainWindow.xaml.cs | 2 +- Lite/Services/LocalDataService.WaitStats.cs | 6 ++++-- Lite/Windows/SettingsWindow.xaml | 7 ++++++- Lite/Windows/SettingsWindow.xaml.cs | 4 ++++ 7 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Dashboard/Services/DatabaseService.NocHealth.cs b/Dashboard/Services/DatabaseService.NocHealth.cs index 4e2b126..a17b809 100644 --- a/Dashboard/Services/DatabaseService.NocHealth.cs +++ b/Dashboard/Services/DatabaseService.NocHealth.cs @@ -605,6 +605,7 @@ ORDER BY collection_time DESC /// private async Task> GetLongRunningQueriesAsync(SqlConnection connection, int thresholdMinutes, int maxResults = 5) { + maxResults = Math.Clamp(maxResults, 1, int.MaxValue); // Exclude internal SP_SERVER_DIAGNOSTICS queries by default, as they often run long and aren't actionable. string spServerDiagnosticsFilter = "AND r.wait_type NOT LIKE N'%SP_SERVER_DIAGNOSTICS%'"; diff --git a/Dashboard/SettingsWindow.xaml.cs b/Dashboard/SettingsWindow.xaml.cs index 12296c6..7c44cb7 100644 --- a/Dashboard/SettingsWindow.xaml.cs +++ b/Dashboard/SettingsWindow.xaml.cs @@ -562,13 +562,13 @@ private void OkButton_Click(object sender, RoutedEventArgs e) else if (prefs.NotifyOnLongRunningQueries) validationErrors.Add("Long-running query threshold must be a positive number"); - if (int.TryParse(LongRunningQueryMaxResultsTextBox.Text, out int lrqMaxResults) && lrqMaxResults > 0) + if (int.TryParse(LongRunningQueryMaxResultsTextBox.Text, out int lrqMaxResults) && lrqMaxResults >= 1 && lrqMaxResults <= int.MaxValue) { prefs.LongRunningQueryMaxResults = lrqMaxResults; } else { - validationErrors.Add("Long-running query max results must be a positive number"); + validationErrors.Add($"Long-running query max results must be between 1 and {int.MaxValue}"); } prefs.NotifyOnTempDbSpace = NotifyOnTempDbSpaceCheckBox.IsChecked == true; diff --git a/Lite/App.xaml.cs b/Lite/App.xaml.cs index 146aee4..49dc1ac 100644 --- a/Lite/App.xaml.cs +++ b/Lite/App.xaml.cs @@ -64,6 +64,7 @@ public partial class App : Application public static int AlertPoisonWaitThresholdMs { get; set; } = 500; public static bool AlertLongRunningQueryEnabled { get; set; } = true; public static int AlertLongRunningQueryThresholdMinutes { get; set; } = 30; + public static int AlertLongRunningQueryMaxResults { get; set; } = 5; public static bool AlertTempDbSpaceEnabled { get; set; } = true; public static int AlertTempDbSpaceThresholdPercent { get; set; } = 80; public static bool AlertLongRunningJobEnabled { get; set; } = true; @@ -239,6 +240,7 @@ public static void LoadAlertSettings() if (root.TryGetProperty("alert_poison_wait_threshold_ms", out v)) AlertPoisonWaitThresholdMs = v.GetInt32(); if (root.TryGetProperty("alert_long_running_query_enabled", out v)) AlertLongRunningQueryEnabled = v.GetBoolean(); if (root.TryGetProperty("alert_long_running_query_threshold_minutes", out v)) AlertLongRunningQueryThresholdMinutes = v.GetInt32(); + if (root.TryGetProperty("alert_long_running_query_max_results", out v)) AlertLongRunningQueryMaxResults = Math.Clamp(v.GetInt32(), 1, int.MaxValue); 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 b247b24..c5ca9d4 100644 --- a/Lite/MainWindow.xaml.cs +++ b/Lite/MainWindow.xaml.cs @@ -1122,7 +1122,7 @@ await _emailAlertService.TrySendAlertEmailAsync( { try { - var longRunning = await _dataService.GetLongRunningQueriesAsync(summary.ServerId, App.AlertLongRunningQueryThresholdMinutes); + var longRunning = await _dataService.GetLongRunningQueriesAsync(summary.ServerId, App.AlertLongRunningQueryThresholdMinutes, App.AlertLongRunningQueryMaxResults); if (longRunning.Count > 0) { diff --git a/Lite/Services/LocalDataService.WaitStats.cs b/Lite/Services/LocalDataService.WaitStats.cs index b330243..496e1d7 100644 --- a/Lite/Services/LocalDataService.WaitStats.cs +++ b/Lite/Services/LocalDataService.WaitStats.cs @@ -195,8 +195,10 @@ ORDER BY collection_time DESC /// Gets long-running queries from the latest collection snapshot. /// Returns sessions whose total elapsed time exceeds the given threshold. /// - public async Task> GetLongRunningQueriesAsync(int serverId, int thresholdMinutes) + public async Task> GetLongRunningQueriesAsync(int serverId, int thresholdMinutes, int maxResults = 5) { + maxResults = Math.Clamp(maxResults, 1, int.MaxValue); + using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); @@ -235,7 +237,7 @@ AND session_id > 50 {miscWaitsFilter} AND total_elapsed_time_ms >= $2 ORDER BY total_elapsed_time_ms DESC - LIMIT 5;"; + LIMIT {maxResults};"; command.Parameters.Add(new DuckDBParameter { Value = serverId }); command.Parameters.Add(new DuckDBParameter { Value = thresholdMs }); diff --git a/Lite/Windows/SettingsWindow.xaml b/Lite/Windows/SettingsWindow.xaml index af3ba5c..3c70345 100644 --- a/Lite/Windows/SettingsWindow.xaml +++ b/Lite/Windows/SettingsWindow.xaml @@ -172,7 +172,12 @@ - + + + diff --git a/Lite/Windows/SettingsWindow.xaml.cs b/Lite/Windows/SettingsWindow.xaml.cs index 8dcfc6a..176cd70 100644 --- a/Lite/Windows/SettingsWindow.xaml.cs +++ b/Lite/Windows/SettingsWindow.xaml.cs @@ -386,6 +386,7 @@ private void LoadAlertSettings() AlertPoisonWaitThresholdBox.Text = App.AlertPoisonWaitThresholdMs.ToString(); AlertLongRunningQueryCheckBox.IsChecked = App.AlertLongRunningQueryEnabled; AlertLongRunningQueryThresholdBox.Text = App.AlertLongRunningQueryThresholdMinutes.ToString(); + AlertLongRunningQueryMaxResultsBox.Text = App.AlertLongRunningQueryMaxResults.ToString(); AlertTempDbSpaceCheckBox.IsChecked = App.AlertTempDbSpaceEnabled; AlertTempDbSpaceThresholdBox.Text = App.AlertTempDbSpaceThresholdPercent.ToString(); AlertLongRunningJobCheckBox.IsChecked = App.AlertLongRunningJobEnabled; @@ -413,6 +414,8 @@ private void SaveAlertSettings() App.AlertLongRunningQueryEnabled = AlertLongRunningQueryCheckBox.IsChecked == true; if (int.TryParse(AlertLongRunningQueryThresholdBox.Text, out var lrq) && lrq > 0) App.AlertLongRunningQueryThresholdMinutes = lrq; + if (int.TryParse(AlertLongRunningQueryMaxResultsBox.Text, out var lrqMax) && lrqMax >= 1 && lrqMax <= int.MaxValue) + App.AlertLongRunningQueryMaxResults = lrqMax; App.AlertTempDbSpaceEnabled = AlertTempDbSpaceCheckBox.IsChecked == true; if (int.TryParse(AlertTempDbSpaceThresholdBox.Text, out var tempDb) && tempDb > 0 && tempDb <= 100) App.AlertTempDbSpaceThresholdPercent = tempDb; @@ -447,6 +450,7 @@ private void SaveAlertSettings() root["alert_poison_wait_threshold_ms"] = App.AlertPoisonWaitThresholdMs; root["alert_long_running_query_enabled"] = App.AlertLongRunningQueryEnabled; root["alert_long_running_query_threshold_minutes"] = App.AlertLongRunningQueryThresholdMinutes; + root["alert_long_running_query_max_results"] = App.AlertLongRunningQueryMaxResults; root["alert_tempdb_space_enabled"] = App.AlertTempDbSpaceEnabled; root["alert_tempdb_space_threshold_percent"] = App.AlertTempDbSpaceThresholdPercent; root["alert_long_running_job_enabled"] = App.AlertLongRunningJobEnabled; From b121b4a012b5f6909f9e23fb3bcf1dbc427c3acf Mon Sep 17 00:00:00 2001 From: Hannah Vernon Date: Tue, 3 Mar 2026 16:18:55 -0600 Subject: [PATCH 10/27] Use GetInt64() when loading long-running query max results from JSON Prevents an OverflowException if the value in settings.json is outside the int32 range. The value is read as long, clamped to [1, int.MaxValue], then cast back to int. Co-Authored-By: Claude Sonnet 4.6 --- Lite/App.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lite/App.xaml.cs b/Lite/App.xaml.cs index 49dc1ac..d90caad 100644 --- a/Lite/App.xaml.cs +++ b/Lite/App.xaml.cs @@ -240,7 +240,7 @@ public static void LoadAlertSettings() if (root.TryGetProperty("alert_poison_wait_threshold_ms", out v)) AlertPoisonWaitThresholdMs = v.GetInt32(); if (root.TryGetProperty("alert_long_running_query_enabled", out v)) AlertLongRunningQueryEnabled = v.GetBoolean(); if (root.TryGetProperty("alert_long_running_query_threshold_minutes", out v)) AlertLongRunningQueryThresholdMinutes = v.GetInt32(); - if (root.TryGetProperty("alert_long_running_query_max_results", out v)) AlertLongRunningQueryMaxResults = Math.Clamp(v.GetInt32(), 1, int.MaxValue); + if (root.TryGetProperty("alert_long_running_query_max_results", out v)) AlertLongRunningQueryMaxResults = (int)Math.Clamp(v.GetInt64(), 1, int.MaxValue); 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(); From 1ceb06563ae4d97dde8ec29f4f6ac1843a1222cf Mon Sep 17 00:00:00 2001 From: Hannah Vernon Date: Tue, 3 Mar 2026 16:33:31 -0600 Subject: [PATCH 11/27] Add configurable long-running query filter toggles Replaces hardcoded wait type exclusions in GetLongRunningQueriesAsync with user-configurable booleans for SP_SERVER_DIAGNOSTICS, WAITFOR / BROKER_RECEIVE_WAITFOR, backup waits, and miscellaneous waits. All four filters default to true (existing behavior preserved). Settings are exposed in the Notifications section of both Dashboard and Lite Settings UIs and persisted to UserPreferences / settings.json. Co-Authored-By: Claude Sonnet 4.6 --- Dashboard/MainWindow.xaml.cs | 2 +- Dashboard/Models/UserPreferences.cs | 4 ++ .../Services/DatabaseService.NocHealth.cs | 40 ++++++++++++------- Dashboard/SettingsWindow.xaml | 14 +++++++ Dashboard/SettingsWindow.xaml.cs | 9 +++++ Lite/App.xaml.cs | 8 ++++ Lite/MainWindow.xaml.cs | 2 +- Lite/Services/LocalDataService.WaitStats.cs | 28 +++++++------ Lite/Windows/SettingsWindow.xaml | 15 +++++++ Lite/Windows/SettingsWindow.xaml.cs | 12 ++++++ 10 files changed, 106 insertions(+), 28 deletions(-) diff --git a/Dashboard/MainWindow.xaml.cs b/Dashboard/MainWindow.xaml.cs index ba43d99..ecf1ca1 100644 --- a/Dashboard/MainWindow.xaml.cs +++ b/Dashboard/MainWindow.xaml.cs @@ -1051,7 +1051,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); + var health = await databaseService.GetAlertHealthAsync(connStatus.SqlEngineEdition, prefs.LongRunningQueryThresholdMinutes, prefs.LongRunningJobMultiplier, prefs.LongRunningQueryMaxResults, prefs.LongRunningQueryExcludeSpServerDiagnostics, prefs.LongRunningQueryExcludeWaitFor, prefs.LongRunningQueryExcludeBackups, prefs.LongRunningQueryExcludeMiscWaits); if (health.IsOnline) { diff --git a/Dashboard/Models/UserPreferences.cs b/Dashboard/Models/UserPreferences.cs index 7a498e0..23b3e6a 100644 --- a/Dashboard/Models/UserPreferences.cs +++ b/Dashboard/Models/UserPreferences.cs @@ -83,6 +83,10 @@ public class UserPreferences public bool NotifyOnLongRunningQueries { get; set; } = true; public int LongRunningQueryThresholdMinutes { get; set; } = 30; // Alert when query runs > X minutes public int LongRunningQueryMaxResults { get; set; } = 5; // Max number of long-running queries returned per check + public bool LongRunningQueryExcludeSpServerDiagnostics { get; set; } = true; + public bool LongRunningQueryExcludeWaitFor { get; set; } = true; + public bool LongRunningQueryExcludeBackups { get; set; } = true; + public bool LongRunningQueryExcludeMiscWaits { get; set; } = true; public bool NotifyOnTempDbSpace { get; set; } = true; public int TempDbSpaceThresholdPercent { get; set; } = 80; // Alert when TempDB used > X% public bool NotifyOnLongRunningJobs { get; set; } = true; diff --git a/Dashboard/Services/DatabaseService.NocHealth.cs b/Dashboard/Services/DatabaseService.NocHealth.cs index a17b809..dd2b3c1 100644 --- a/Dashboard/Services/DatabaseService.NocHealth.cs +++ b/Dashboard/Services/DatabaseService.NocHealth.cs @@ -121,7 +121,15 @@ public async Task RefreshNocHealthStatusAsync(ServerHealthStatus status, int eng /// Lightweight alert-only health check. Runs 3 queries instead of 9. /// Used by MainWindow's independent alert timer. /// - public async Task GetAlertHealthAsync(int engineEdition = 0, int longRunningQueryThresholdMinutes = 30, int longRunningJobMultiplier = 3, int longRunningQueryMaxResults = 5) + public async Task GetAlertHealthAsync( + int engineEdition = 0, + int longRunningQueryThresholdMinutes = 30, + int longRunningJobMultiplier = 3, + int longRunningQueryMaxResults = 5, + bool excludeSpServerDiagnostics = true, + bool excludeWaitFor = true, + bool excludeBackups = true, + bool excludeMiscWaits = true) { var result = new AlertHealthResult(); @@ -136,7 +144,7 @@ public async Task GetAlertHealthAsync(int engineEdition = 0, var blockingTask = GetBlockingValuesAsync(connection); var deadlockTask = GetDeadlockCountAsync(connection); var poisonWaitTask = GetPoisonWaitDeltasAsync(connection); - var longRunningTask = GetLongRunningQueriesAsync(connection, longRunningQueryThresholdMinutes, longRunningQueryMaxResults); + var longRunningTask = GetLongRunningQueriesAsync(connection, longRunningQueryThresholdMinutes, longRunningQueryMaxResults, excludeSpServerDiagnostics, excludeWaitFor, excludeBackups, excludeMiscWaits); var tempDbTask = GetTempDbSpaceAsync(connection); var anomalousJobTask = GetAnomalousJobsAsync(connection, longRunningJobMultiplier); @@ -603,21 +611,25 @@ ORDER BY collection_time DESC /// Gets currently running queries that exceed the duration threshold. /// Uses live DMV data (sys.dm_exec_requests) for immediate detection. /// - private async Task> GetLongRunningQueriesAsync(SqlConnection connection, int thresholdMinutes, int maxResults = 5) + private async Task> GetLongRunningQueriesAsync( + SqlConnection connection, + int thresholdMinutes, + int maxResults = 5, + bool excludeSpServerDiagnostics = true, + bool excludeWaitFor = true, + bool excludeBackups = true, + bool excludeMiscWaits = true) { maxResults = Math.Clamp(maxResults, 1, int.MaxValue); - // Exclude internal SP_SERVER_DIAGNOSTICS queries by default, as they often run long and aren't actionable. - string spServerDiagnosticsFilter = "AND r.wait_type NOT LIKE N'%SP_SERVER_DIAGNOSTICS%'"; - - // Exclude WAITFOR queries by default, as they can run indefinitely and may not indicate a problem. - string waitForFilter = "AND r.wait_type NOT IN (N'WAITFOR', N'BROKER_RECEIVE_WAITFOR')"; - - // Exclude backup waits if specified, as they can run long and aren't typically actionable in this context. - string backupsFilter = "AND r.wait_type NOT IN (N'BACKUPTHREAD', N'BACKUPIO')"; - - // Exclude miscellaneous wait type that aren't typically actionable - string miscWaitsFilter = "AND r.wait_type NOT IN (N'XE_LIVE_TARGET_TVF')"; + string spServerDiagnosticsFilter = excludeSpServerDiagnostics + ? "AND r.wait_type NOT LIKE N'%SP_SERVER_DIAGNOSTICS%'" : ""; + string waitForFilter = excludeWaitFor + ? "AND r.wait_type NOT IN (N'WAITFOR', N'BROKER_RECEIVE_WAITFOR')" : ""; + string backupsFilter = excludeBackups + ? "AND r.wait_type NOT IN (N'BACKUPTHREAD', N'BACKUPIO')" : ""; + string miscWaitsFilter = excludeMiscWaits + ? "AND r.wait_type NOT IN (N'XE_LIVE_TARGET_TVF')" : ""; string query = @$"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; diff --git a/Dashboard/SettingsWindow.xaml b/Dashboard/SettingsWindow.xaml index 39aa691..41d2699 100644 --- a/Dashboard/SettingsWindow.xaml +++ b/Dashboard/SettingsWindow.xaml @@ -214,6 +214,20 @@ TextAlignment="Center"/> + + + + + + 0 && tempDbThreshold <= 100) prefs.TempDbSpaceThresholdPercent = tempDbThreshold; diff --git a/Lite/App.xaml.cs b/Lite/App.xaml.cs index d90caad..0b791de 100644 --- a/Lite/App.xaml.cs +++ b/Lite/App.xaml.cs @@ -65,6 +65,10 @@ public partial class App : Application public static bool AlertLongRunningQueryEnabled { get; set; } = true; public static int AlertLongRunningQueryThresholdMinutes { get; set; } = 30; public static int AlertLongRunningQueryMaxResults { get; set; } = 5; + public static bool AlertLongRunningQueryExcludeSpServerDiagnostics { get; set; } = true; + public static bool AlertLongRunningQueryExcludeWaitFor { get; set; } = true; + public static bool AlertLongRunningQueryExcludeBackups { get; set; } = true; + public static bool AlertLongRunningQueryExcludeMiscWaits { get; set; } = true; public static bool AlertTempDbSpaceEnabled { get; set; } = true; public static int AlertTempDbSpaceThresholdPercent { get; set; } = 80; public static bool AlertLongRunningJobEnabled { get; set; } = true; @@ -241,6 +245,10 @@ public static void LoadAlertSettings() if (root.TryGetProperty("alert_long_running_query_enabled", out v)) AlertLongRunningQueryEnabled = v.GetBoolean(); if (root.TryGetProperty("alert_long_running_query_threshold_minutes", out v)) AlertLongRunningQueryThresholdMinutes = v.GetInt32(); if (root.TryGetProperty("alert_long_running_query_max_results", out v)) AlertLongRunningQueryMaxResults = (int)Math.Clamp(v.GetInt64(), 1, int.MaxValue); + if (root.TryGetProperty("alert_long_running_query_exclude_sp_server_diagnostics", out v)) AlertLongRunningQueryExcludeSpServerDiagnostics = v.GetBoolean(); + 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_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 c5ca9d4..93e3944 100644 --- a/Lite/MainWindow.xaml.cs +++ b/Lite/MainWindow.xaml.cs @@ -1122,7 +1122,7 @@ await _emailAlertService.TrySendAlertEmailAsync( { try { - var longRunning = await _dataService.GetLongRunningQueriesAsync(summary.ServerId, App.AlertLongRunningQueryThresholdMinutes, App.AlertLongRunningQueryMaxResults); + var longRunning = await _dataService.GetLongRunningQueriesAsync(summary.ServerId, App.AlertLongRunningQueryThresholdMinutes, App.AlertLongRunningQueryMaxResults, App.AlertLongRunningQueryExcludeSpServerDiagnostics, App.AlertLongRunningQueryExcludeWaitFor, App.AlertLongRunningQueryExcludeBackups, App.AlertLongRunningQueryExcludeMiscWaits); if (longRunning.Count > 0) { diff --git a/Lite/Services/LocalDataService.WaitStats.cs b/Lite/Services/LocalDataService.WaitStats.cs index 496e1d7..b997b20 100644 --- a/Lite/Services/LocalDataService.WaitStats.cs +++ b/Lite/Services/LocalDataService.WaitStats.cs @@ -195,7 +195,14 @@ ORDER BY collection_time DESC /// Gets long-running queries from the latest collection snapshot. /// Returns sessions whose total elapsed time exceeds the given threshold. /// - public async Task> GetLongRunningQueriesAsync(int serverId, int thresholdMinutes, int maxResults = 5) + public async Task> GetLongRunningQueriesAsync( + int serverId, + int thresholdMinutes, + int maxResults = 5, + bool excludeSpServerDiagnostics = true, + bool excludeWaitFor = true, + bool excludeBackups = true, + bool excludeMiscWaits = true) { maxResults = Math.Clamp(maxResults, 1, int.MaxValue); @@ -204,17 +211,14 @@ public async Task> GetLongRunningQueriesAsync(int ser var thresholdMs = (long)thresholdMinutes * 60 * 1000; - // Exclude internal SP_SERVER_DIAGNOSTICS queries by default, as they often run long and aren't actionable. - string spServerDiagnosticsFilter = "AND r.wait_type NOT LIKE N'%SP_SERVER_DIAGNOSTICS%'"; - - // Exclude WAITFOR queries by default, as they can run indefinitely and may not indicate a problem. - string waitForFilter = "AND r.wait_type NOT IN (N'WAITFOR', N'BROKER_RECEIVE_WAITFOR')"; - - // Exclude backup waits if specified, as they can run long and aren't typically actionable in this context. - string backupsFilter = "AND r.wait_type NOT IN (N'BACKUPTHREAD', N'BACKUPIO')"; - - // Exclude miscellaneous wait type that aren't typically actionable - string miscWaitsFilter = "AND r.wait_type NOT IN (N'XE_LIVE_TARGET_TVF')"; + string spServerDiagnosticsFilter = excludeSpServerDiagnostics + ? "AND r.wait_type NOT LIKE N'%SP_SERVER_DIAGNOSTICS%'" : ""; + string waitForFilter = excludeWaitFor + ? "AND r.wait_type NOT IN (N'WAITFOR', N'BROKER_RECEIVE_WAITFOR')" : ""; + string backupsFilter = excludeBackups + ? "AND r.wait_type NOT IN (N'BACKUPTHREAD', N'BACKUPIO')" : ""; + string miscWaitsFilter = excludeMiscWaits + ? "AND r.wait_type NOT IN (N'XE_LIVE_TARGET_TVF')" : ""; command.CommandText = @$" SELECT diff --git a/Lite/Windows/SettingsWindow.xaml b/Lite/Windows/SettingsWindow.xaml index 3c70345..8fdc37c 100644 --- a/Lite/Windows/SettingsWindow.xaml +++ b/Lite/Windows/SettingsWindow.xaml @@ -180,6 +180,21 @@ + + + + + + diff --git a/Lite/Windows/SettingsWindow.xaml.cs b/Lite/Windows/SettingsWindow.xaml.cs index 176cd70..687c4b6 100644 --- a/Lite/Windows/SettingsWindow.xaml.cs +++ b/Lite/Windows/SettingsWindow.xaml.cs @@ -387,6 +387,10 @@ private void LoadAlertSettings() AlertLongRunningQueryCheckBox.IsChecked = App.AlertLongRunningQueryEnabled; AlertLongRunningQueryThresholdBox.Text = App.AlertLongRunningQueryThresholdMinutes.ToString(); AlertLongRunningQueryMaxResultsBox.Text = App.AlertLongRunningQueryMaxResults.ToString(); + LrqExcludeSpServerDiagnosticsCheckBox.IsChecked = App.AlertLongRunningQueryExcludeSpServerDiagnostics; + LrqExcludeWaitForCheckBox.IsChecked = App.AlertLongRunningQueryExcludeWaitFor; + LrqExcludeBackupsCheckBox.IsChecked = App.AlertLongRunningQueryExcludeBackups; + LrqExcludeMiscWaitsCheckBox.IsChecked = App.AlertLongRunningQueryExcludeMiscWaits; AlertTempDbSpaceCheckBox.IsChecked = App.AlertTempDbSpaceEnabled; AlertTempDbSpaceThresholdBox.Text = App.AlertTempDbSpaceThresholdPercent.ToString(); AlertLongRunningJobCheckBox.IsChecked = App.AlertLongRunningJobEnabled; @@ -416,6 +420,10 @@ private void SaveAlertSettings() App.AlertLongRunningQueryThresholdMinutes = lrq; if (int.TryParse(AlertLongRunningQueryMaxResultsBox.Text, out var lrqMax) && lrqMax >= 1 && lrqMax <= int.MaxValue) App.AlertLongRunningQueryMaxResults = lrqMax; + App.AlertLongRunningQueryExcludeSpServerDiagnostics = LrqExcludeSpServerDiagnosticsCheckBox.IsChecked == true; + App.AlertLongRunningQueryExcludeWaitFor = LrqExcludeWaitForCheckBox.IsChecked == true; + App.AlertLongRunningQueryExcludeBackups = LrqExcludeBackupsCheckBox.IsChecked == true; + App.AlertLongRunningQueryExcludeMiscWaits = LrqExcludeMiscWaitsCheckBox.IsChecked == true; App.AlertTempDbSpaceEnabled = AlertTempDbSpaceCheckBox.IsChecked == true; if (int.TryParse(AlertTempDbSpaceThresholdBox.Text, out var tempDb) && tempDb > 0 && tempDb <= 100) App.AlertTempDbSpaceThresholdPercent = tempDb; @@ -451,6 +459,10 @@ private void SaveAlertSettings() root["alert_long_running_query_enabled"] = App.AlertLongRunningQueryEnabled; root["alert_long_running_query_threshold_minutes"] = App.AlertLongRunningQueryThresholdMinutes; root["alert_long_running_query_max_results"] = App.AlertLongRunningQueryMaxResults; + root["alert_long_running_query_exclude_sp_server_diagnostics"] = App.AlertLongRunningQueryExcludeSpServerDiagnostics; + 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; root["alert_tempdb_space_enabled"] = App.AlertTempDbSpaceEnabled; root["alert_tempdb_space_threshold_percent"] = App.AlertTempDbSpaceThresholdPercent; root["alert_long_running_job_enabled"] = App.AlertLongRunningJobEnabled; From 2f54765cf34618cafcbd02952575da58b4cd06ae Mon Sep 17 00:00:00 2001 From: Hannah Vernon Date: Tue, 3 Mar 2026 17:18:12 -0600 Subject: [PATCH 12/27] merged with incoming dev branch --- CHANGELOG.md | 97 ++++ Dashboard/AddServerDialog.xaml | 172 ++++--- Dashboard/App.xaml | 2 + Dashboard/CollectionLogWindow.xaml | 2 +- Dashboard/Controls/ConfigChangesContent.xaml | 6 +- Dashboard/Controls/CriticalIssuesContent.xaml | 2 +- Dashboard/Controls/CurrentConfigContent.xaml | 6 +- Dashboard/Controls/DefaultTraceContent.xaml | 2 +- Dashboard/Controls/PlanViewerControl.xaml | 5 +- Dashboard/Controls/PlanViewerControl.xaml.cs | 32 ++ .../Controls/QueryPerformanceContent.xaml | 36 +- Dashboard/Controls/SystemEventsContent.xaml | 4 +- Dashboard/Dashboard.csproj | 3 + Dashboard/Helpers/ChartHoverHelper.cs | 7 +- Dashboard/Helpers/ServerTimeConverter.cs | 37 ++ Dashboard/Helpers/ServerTimeHelper.cs | 26 ++ Dashboard/Helpers/TimeDisplayMode.cs | 17 + Dashboard/MainWindow.xaml | 10 + Dashboard/MainWindow.xaml.cs | 427 +++++++++++++++++- Dashboard/Models/UserPreferences.cs | 3 + Dashboard/ProcedureHistoryWindow.xaml | 6 +- Dashboard/QueryExecutionHistoryWindow.xaml | 2 +- Dashboard/QueryStatsHistoryWindow.xaml | 6 +- Dashboard/ServerTab.xaml | 43 +- Dashboard/ServerTab.xaml.cs | 47 +- .../Services/DatabaseService.NocHealth.cs | 2 +- .../DatabaseService.QueryPerformance.cs | 8 +- .../DatabaseService.ResourceMetrics.cs | 32 +- Dashboard/Services/PlanAnalyzer.cs | 162 +++++-- Dashboard/SettingsWindow.xaml | 11 + Dashboard/SettingsWindow.xaml.cs | 20 + Dashboard/Themes/CoolBreezeTheme.xaml | 35 ++ Dashboard/Themes/DarkTheme.xaml | 35 ++ Dashboard/Themes/LightTheme.xaml | 35 ++ Dashboard/TracePatternHistoryWindow.xaml | 4 +- Installer/Program.cs | 60 +-- InstallerGui/InstallerGui.csproj | 3 + InstallerGui/MainWindow.xaml | 6 - InstallerGui/MainWindow.xaml.cs | 2 - InstallerGui/Services/InstallationService.cs | 28 +- Lite/App.xaml.cs | 15 + Lite/Controls/PlanViewerControl.xaml | 5 +- Lite/Controls/PlanViewerControl.xaml.cs | 32 ++ Lite/Controls/ServerTab.xaml | 31 +- Lite/Controls/ServerTab.xaml.cs | 37 +- Lite/Helpers/ChartHoverHelper.cs | 8 +- Lite/Helpers/TimeDisplayMode.cs | 17 + Lite/MainWindow.xaml | 28 +- Lite/MainWindow.xaml.cs | 386 ++++++++++++++++ Lite/PerformanceMonitorLite.csproj | 3 + Lite/Services/LocalDataService.Blocking.cs | 29 +- Lite/Services/LocalDataService.WaitStats.cs | 33 +- Lite/Services/PlanAnalyzer.cs | 162 +++++-- Lite/Themes/CoolBreezeTheme.xaml | 35 ++ Lite/Themes/DarkTheme.xaml | 35 ++ Lite/Themes/LightTheme.xaml | 35 ++ Lite/Windows/AddServerDialog.xaml | 43 +- Lite/Windows/AddServerDialog.xaml.cs | 4 +- Lite/Windows/SettingsWindow.xaml | 9 + Lite/Windows/SettingsWindow.xaml.cs | 50 ++ README.md | 24 +- install/01_install_database.sql | 4 - install/08_collect_query_stats.sql | 28 +- install/10_collect_procedure_stats.sql | 38 +- install/11_collect_query_snapshots.sql | 83 +++- install/18_collect_cpu_utilization_stats.sql | 4 +- install/22_collect_blocked_processes.sql | 6 +- install/24_collect_deadlock_xml.sql | 6 +- install/28_collect_system_health_wrapper.sql | 6 +- install/29_collect_default_trace.sql | 6 +- install/45_create_agent_jobs.sql | 247 +++++----- .../02_default_trace_events_new_columns.sql | 40 ++ upgrades/2.0.0-to-2.1.0/upgrade.txt | 1 + 73 files changed, 2362 insertions(+), 571 deletions(-) create mode 100644 Dashboard/Helpers/ServerTimeConverter.cs create mode 100644 Dashboard/Helpers/TimeDisplayMode.cs create mode 100644 Lite/Helpers/TimeDisplayMode.cs create mode 100644 upgrades/2.0.0-to-2.1.0/02_default_trace_events_new_columns.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index f10f250..a6d5cc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,60 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.1.0] - 2026-03-04 + +### Important + +- **Schema upgrade**: The `config.collection_schedule` table gains two new columns (`collect_query`, `collect_plan`) for optional query text and execution plan collection. Both default to enabled to preserve existing behavior. Upgrade scripts run automatically via the CLI/GUI installer and use idempotent checks. + +### Added + +- **Light theme and "Cool Breeze" theme** — full light mode support for both Dashboard and Lite with live preview in settings ([#347]) +- **Standalone Plan Viewer** — open, paste (Ctrl+V), or drag & drop `.sqlplan` files independent of any server connection, with tabbed multi-plan support ([#359]) +- **Time display mode toggle** — show timestamps in Server Time, Local Time, or UTC with timezone labels across all grids and tooltips ([#17]) +- **30 PlanAnalyzer rules** — expanded from 12 to 30 rules covering implicit conversions, GetRangeThroughConvert, lazy spools, OR expansion, exchange spills, RID lookups, and more ([#327], [#349], [#356], [#379]) +- **Wait stats banner** in plan viewer showing top waits for the query ([#373]) +- **UDF runtime details** — CPU and elapsed time shown in Runtime Summary pane when UDFs are present ([#382]) +- **Sortable statement grid** and canvas panning in plan viewer ([#331]) +- **Comma-separated column filters** — enter multiple values separated by commas in text filters ([#348]) +- **Optional query text and plan collection** — per-collector flags in `config.collection_schedule` to disable query text or plan capture ([#337]) +- **`--preserve-jobs` installer flag** — keep existing SQL Agent job schedules during upgrade ([#326]) +- **Copy Query Text** context menu on Dashboard statements grid ([#367]) +- **Server list sorting** by display name in both Dashboard and Lite ([#30]) +- **Warning status icon** in server health indicators ([#355]) +- Reserved threads and 10 missing ShowPlan XML attributes in plan viewer ([#378]) +- Nightly build workflow for CI ([#332]) + +### Changed + +- PlanAnalyzer warning messages rewritten to be actionable with expert-guided per-rule advice ([#370], [#371]) +- PlanAnalyzer rule tuning: time-based spill analysis (Rule 7), lowered parallel skew thresholds (Rule 8), memory grant floor raised to 1GB/4GB (Rule 9), skip PROBE-only bitmap predicates (Rule 11) ([#341], [#342], [#343], [#358]) +- First-run collector lookback reduced from 3-7 days to 1 hour for faster initial data ([#335]) +- Plan canvas aligns top-left and resets scroll on statement switch ([#366]) +- Plan viewer polish: index suggestions, property panel improvements, muted brush audit ([#365]) +- Add Server dialog visual parity between Dashboard and Lite with theme-driven PasswordBox styling ([#289]) + +### Fixed + +- **OverflowException** on wait stats page with large decimal values — SQL Server `decimal(38,24)` exceeding .NET precision ([#395]) +- **SQL dumps** on mirroring passive servers with RESTORING databases ([#384]) +- **UI hang** when adding first server to Dashboard ([#387]) +- **UTC/local timezone mismatch** in blocked process XML processor ([#383]) +- **AG secondary filter** skipping all inaccessible databases in cross-database collectors ([#325]) +- DuckDB column aliases in long-running queries ([#391]) +- sp_server_diagnostics and WAITFOR excluded from long-running query alerts ([#362]) +- UDF timing units corrected: microseconds to milliseconds ([#338]) +- DuckDB migration ordering after archive-and-reset ([#314]) +- Int16 cast error in long-running query alerts ([#313]) +- Missing dark mode on 19 SystemEventsContent charts ([#321]) +- Missing tooltips on charts after theme changes ([#319]) +- Operator time per-thread calculation synced across all plan viewers ([#392]) +- Theme StaticResource/DynamicResource binding fix for runtime theme switching +- Memory grant MB display, missing index quality scoring, wildcard LIKE detection ([#393]) +- **Installer validation** reporting historical collection errors as current failures — now filters to current run only ([#400]) +- **query_snapshots schema mismatch** after sp_WhoIsActive upgrade — collector auto-recreates daily table when column order changes ([#401]) +- **Missing upgrade script** for `default_trace_events` columns (`duration_us`, `end_time`) on 2.0.0→2.1.0 upgrade path ([#400]) + ## [2.0.0] - 2026-02-25 ### Important @@ -180,6 +234,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Delta normalization for per-second rate calculations - Dark theme UI +[2.1.0]: https://github.com/erikdarlingdata/PerformanceMonitor/compare/v2.0.0...v2.1.0 [2.0.0]: https://github.com/erikdarlingdata/PerformanceMonitor/compare/v1.3.0...v2.0.0 [1.3.0]: https://github.com/erikdarlingdata/PerformanceMonitor/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/erikdarlingdata/PerformanceMonitor/compare/v1.1.0...v1.2.0 @@ -274,3 +329,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#281]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/281 [#284]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/284 [#287]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/287 +[#313]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/313 +[#314]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/314 +[#17]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/17 +[#30]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/30 +[#319]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/319 +[#321]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/321 +[#325]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/325 +[#326]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/326 +[#327]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/327 +[#331]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/331 +[#332]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/332 +[#335]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/335 +[#337]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/337 +[#338]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/338 +[#341]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/341 +[#342]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/342 +[#343]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/343 +[#347]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/347 +[#348]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/348 +[#349]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/349 +[#355]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/355 +[#356]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/356 +[#358]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/358 +[#359]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/359 +[#362]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/362 +[#365]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/365 +[#366]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/366 +[#367]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/367 +[#370]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/370 +[#371]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/371 +[#373]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/373 +[#378]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/378 +[#379]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/379 +[#382]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/382 +[#383]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/383 +[#384]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/384 +[#387]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/387 +[#391]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/391 +[#392]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/392 +[#393]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/393 +[#289]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/289 +[#395]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/395 diff --git a/Dashboard/AddServerDialog.xaml b/Dashboard/AddServerDialog.xaml index 5d39a23..84d3e5f 100644 --- a/Dashboard/AddServerDialog.xaml +++ b/Dashboard/AddServerDialog.xaml @@ -2,29 +2,15 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Add SQL Server" - Height="550" Width="500" + SizeToContent="Height" Width="450" MaxHeight="700" WindowStartupLocation="CenterOwner" ResizeMode="NoResize" Background="{DynamicResource BackgroundBrush}" Foreground="{DynamicResource ForegroundBrush}"> - - - - - + @@ -34,97 +20,93 @@ Foreground="{DynamicResource ForegroundBrush}"/> - - - - - + + + + - - - + + + + + + + + + + + - - + + - - - - - + - - - - + + + - - - + + + + + - - - + + + + + - + + + + + - - - + + - + - - + - - - - - - - - - - - - - - - - + - + -