Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2213d86
Added exclusions in GetLongRunningQueriesAsync() method for SP_SERVER…
HannahVernon Feb 27, 2026
9b249fc
Added TOP parameter for query in GetLongRunningQueriesAsync() method …
HannahVernon Feb 27, 2026
adefd25
Replaced waitForFilter string constructor with C# string interpolatio…
HannahVernon Feb 27, 2026
481a3a6
Merge pull request #2 from HannahVernon/feature/long-running-query-ex…
HannahVernon Feb 27, 2026
f75bb2d
Added exclusion for backup-related waits to GetLongRunningQueriesAsyn…
HannahVernon Feb 27, 2026
9c2c5f2
Merge pull request #4 from HannahVernon/feature/add-exclusion-for-BAC…
HannahVernon Feb 27, 2026
fd9b497
Reverted Controls/LandingPage.xaml.cs
HannahVernon Mar 2, 2026
a98ef7b
Added BROKER_RECEIVE_WAITFOR wait type to waitforFilter exclusions. …
HannahVernon Mar 2, 2026
b6d3d3f
Merge pull request #5 from HannahVernon/feature/add-exclusion-for-BAC…
HannahVernon Mar 2, 2026
ab1c47d
Added filtering for SP_SERVER_DIAGNOSTICS, WAITFOR, BROKER_RECEIVE_WA…
HannahVernon Mar 2, 2026
a061f99
Merge pull request #6 from HannahVernon/feature/sync-long-running-que…
HannahVernon Mar 2, 2026
4962abd
Merge branch 'erikdarlingdata:dev' into dev
HannahVernon Mar 2, 2026
8ac4903
Add configurable max results setting for long-running queries
HannahVernon Mar 3, 2026
c5476dd
Apply max results validation and Lite parity for long-running queries
HannahVernon Mar 3, 2026
b121b4a
Use GetInt64() when loading long-running query max results from JSON
HannahVernon Mar 3, 2026
1ceb065
Add configurable long-running query filter toggles
HannahVernon Mar 3, 2026
e10771e
Merge pull request #7 from HannahVernon/feature/long-running-queries-…
HannahVernon Mar 3, 2026
2f54765
merged with incoming dev branch
HannahVernon Mar 3, 2026
f8998d1
Merged with incoming dev branch.
HannahVernon Mar 4, 2026
4d55c32
Merge branch 'dev' into feature/long-running-queries-config-settings
HannahVernon Mar 4, 2026
9642995
Parameterized TOP/LIMIT value in Dashboard/Services/DatabaseService.N…
HannahVernon Mar 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dashboard/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1065,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);
var health = await databaseService.GetAlertHealthAsync(connStatus.SqlEngineEdition, prefs.LongRunningQueryThresholdMinutes, prefs.LongRunningJobMultiplier, prefs.LongRunningQueryMaxResults, prefs.LongRunningQueryExcludeSpServerDiagnostics, prefs.LongRunningQueryExcludeWaitFor, prefs.LongRunningQueryExcludeBackups, prefs.LongRunningQueryExcludeMiscWaits);

if (health.IsOnline)
{
Expand Down
5 changes: 5 additions & 0 deletions Dashboard/Models/UserPreferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ 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 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;
Expand Down
44 changes: 29 additions & 15 deletions Dashboard/Services/DatabaseService.NocHealth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public async Task<AlertHealthResult> GetAlertHealthAsync(int engineEdition = 0, int longRunningQueryThresholdMinutes = 30, int longRunningJobMultiplier = 3)
public async Task<AlertHealthResult> 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();

Expand All @@ -136,7 +144,7 @@ public async Task<AlertHealthResult> 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, excludeSpServerDiagnostics, excludeWaitFor, excludeBackups, excludeMiscWaits);
var tempDbTask = GetTempDbSpaceAsync(connection);
var anomalousJobTask = GetAnomalousJobsAsync(connection, longRunningJobMultiplier);

Expand Down Expand Up @@ -603,24 +611,29 @@ 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.
/// </summary>
private async Task<List<LongRunningQueryInfo>> GetLongRunningQueriesAsync(SqlConnection connection, int thresholdMinutes)
private async Task<List<LongRunningQueryInfo>> 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, 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')" : "";

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,
Expand Down Expand Up @@ -651,6 +664,7 @@ ORDER BY r.total_elapsed_time DESC
using var cmd = new SqlCommand(query, connection);
cmd.CommandTimeout = 10;
cmd.Parameters.Add(new SqlParameter("@thresholdMs", SqlDbType.BigInt) { Value = (long)thresholdMinutes * 60 * 1000 });
cmd.Parameters.Add(new SqlParameter("@maxResults", SqlDbType.Int) { Value = maxResults});
using var reader = await cmd.ExecuteReaderAsync();

while (await reader.ReadAsync())
Expand Down
22 changes: 21 additions & 1 deletion Dashboard/SettingsWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,28 @@
Margin="8,0,8,0"
VerticalAlignment="Center"
TextAlignment="Center"/>
<TextBlock Text="minutes" VerticalAlignment="Center"/>
<TextBlock Text="minutes," VerticalAlignment="Center"/>
<TextBlock Text="return up to" VerticalAlignment="Center" Margin="8,0,8,0"/>
<TextBox x:Name="LongRunningQueryMaxResultsTextBox"
Width="50"
VerticalAlignment="Center"
TextAlignment="Center"/>
<TextBlock Text="results" VerticalAlignment="Center" Margin="8,0,0,0"/>
</StackPanel>
<!-- Long Running Query Filters -->
<TextBlock Text="Long Running Query Filters" FontWeight="Bold" FontSize="12" Margin="0,12,0,6"/>
<CheckBox x:Name="LrqExcludeSpServerDiagnosticsCheckBox"
Content="Exclude SP_SERVER_DIAGNOSTICS"
Margin="0,0,0,4"/>
<CheckBox x:Name="LrqExcludeWaitForCheckBox"
Content="Exclude WAITFOR / BROKER_RECEIVE_WAITFOR"
Margin="0,0,0,4"/>
<CheckBox x:Name="LrqExcludeBackupsCheckBox"
Content="Exclude backup waits (BACKUPTHREAD, BACKUPIO)"
Margin="0,0,0,4"/>
<CheckBox x:Name="LrqExcludeMiscWaitsCheckBox"
Content="Exclude miscellaneous waits (XE_LIVE_TARGET_TVF)"
Margin="0,0,0,0"/>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<CheckBox x:Name="NotifyOnTempDbSpaceCheckBox"
Content="TempDB space usage over"
Expand Down
19 changes: 19 additions & 0 deletions Dashboard/SettingsWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ private void LoadSettings()
PoisonWaitThresholdTextBox.Text = prefs.PoisonWaitThresholdMs.ToString(CultureInfo.InvariantCulture);
NotifyOnLongRunningQueriesCheckBox.IsChecked = prefs.NotifyOnLongRunningQueries;
LongRunningQueryThresholdTextBox.Text = prefs.LongRunningQueryThresholdMinutes.ToString(CultureInfo.InvariantCulture);
LongRunningQueryMaxResultsTextBox.Text = prefs.LongRunningQueryMaxResults.ToString(CultureInfo.InvariantCulture);
LrqExcludeSpServerDiagnosticsCheckBox.IsChecked = prefs.LongRunningQueryExcludeSpServerDiagnostics;
LrqExcludeWaitForCheckBox.IsChecked = prefs.LongRunningQueryExcludeWaitFor;
LrqExcludeBackupsCheckBox.IsChecked = prefs.LongRunningQueryExcludeBackups;
LrqExcludeMiscWaitsCheckBox.IsChecked = prefs.LongRunningQueryExcludeMiscWaits;
NotifyOnTempDbSpaceCheckBox.IsChecked = prefs.NotifyOnTempDbSpace;
TempDbSpaceThresholdTextBox.Text = prefs.TempDbSpaceThresholdPercent.ToString(CultureInfo.InvariantCulture);
NotifyOnLongRunningJobsCheckBox.IsChecked = prefs.NotifyOnLongRunningJobs;
Expand Down Expand Up @@ -581,6 +586,20 @@ 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 >= 1 && lrqMaxResults <= int.MaxValue)
{
prefs.LongRunningQueryMaxResults = lrqMaxResults;
}
else
{
validationErrors.Add($"Long-running query max results must be between 1 and {int.MaxValue}");
}

prefs.LongRunningQueryExcludeSpServerDiagnostics = LrqExcludeSpServerDiagnosticsCheckBox.IsChecked == true;
prefs.LongRunningQueryExcludeWaitFor = LrqExcludeWaitForCheckBox.IsChecked == true;
prefs.LongRunningQueryExcludeBackups = LrqExcludeBackupsCheckBox.IsChecked == true;
prefs.LongRunningQueryExcludeMiscWaits = LrqExcludeMiscWaitsCheckBox.IsChecked == true;

prefs.NotifyOnTempDbSpace = NotifyOnTempDbSpaceCheckBox.IsChecked == true;
if (int.TryParse(TempDbSpaceThresholdTextBox.Text, out int tempDbThreshold) && tempDbThreshold > 0 && tempDbThreshold <= 100)
prefs.TempDbSpaceThresholdPercent = tempDbThreshold;
Expand Down
10 changes: 10 additions & 0 deletions Lite/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ 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 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;
Expand Down Expand Up @@ -242,6 +247,11 @@ 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 = (int)Math.Clamp(v.GetInt64(), 1, 1000);
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();
Expand Down
2 changes: 1 addition & 1 deletion Lite/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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, App.AlertLongRunningQueryExcludeSpServerDiagnostics, App.AlertLongRunningQueryExcludeWaitFor, App.AlertLongRunningQueryExcludeBackups, App.AlertLongRunningQueryExcludeMiscWaits);

if (longRunning.Count > 0)
{
Expand Down
32 changes: 19 additions & 13 deletions Lite/Services/LocalDataService.WaitStats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,24 +195,29 @@ ORDER BY collection_time DESC
/// Gets long-running queries from the latest collection snapshot.
/// Returns sessions whose total elapsed time exceeds the given threshold.
/// </summary>
public async Task<List<LongRunningQueryInfo>> GetLongRunningQueriesAsync(int serverId, int thresholdMinutes)
public async Task<List<LongRunningQueryInfo>> GetLongRunningQueriesAsync(
int serverId,
int thresholdMinutes,
int maxResults = 5,
bool excludeSpServerDiagnostics = true,
bool excludeWaitFor = true,
bool excludeBackups = true,
bool excludeMiscWaits = true)
{
using var connection = await OpenConnectionAsync();
using var command = connection.CreateCommand();

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')" : "";
maxResults = Math.Clamp(maxResults, 1, 1000);

command.CommandText = @$"
SELECT
Expand All @@ -235,10 +240,11 @@ AND r.session_id > 50
{miscWaitsFilter}
AND r.total_elapsed_time_ms >= $2
ORDER BY r.total_elapsed_time_ms DESC
LIMIT 5;";
LIMIT $3;";

command.Parameters.Add(new DuckDBParameter { Value = serverId });
command.Parameters.Add(new DuckDBParameter { Value = thresholdMs });
command.Parameters.Add(new DuckDBParameter { Value = maxResults });

var items = new List<LongRunningQueryInfo>();
using var reader = await command.ExecuteReaderAsync();
Expand Down
22 changes: 21 additions & 1 deletion Lite/Windows/SettingsWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,29 @@
<CheckBox x:Name="AlertLongRunningQueryCheckBox" Content="Long-running queries over" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}"/>
<TextBox x:Name="AlertLongRunningQueryThresholdBox" Width="45" Text="30" Margin="6,0,0,0" VerticalAlignment="Center"/>
<TextBlock Text="minutes" VerticalAlignment="Center" Margin="2,0,0,0"
<TextBlock Text="minutes," VerticalAlignment="Center" Margin="2,0,0,0"
Foreground="{DynamicResource ForegroundBrush}"/>
<TextBlock Text="return up to" VerticalAlignment="Center" Margin="6,0,6,0"
Foreground="{DynamicResource ForegroundBrush}"/>
<TextBox x:Name="AlertLongRunningQueryMaxResultsBox" Width="45" Text="5" VerticalAlignment="Center"/>
<TextBlock Text="results" VerticalAlignment="Center" Margin="2,0,0,0"
Foreground="{DynamicResource ForegroundBrush}"/>
</StackPanel>
<!-- Long Running Query Filters -->
<TextBlock Text="Long Running Query Filters" FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource ForegroundBrush}" Margin="20,10,0,4"/>
<CheckBox x:Name="LrqExcludeSpServerDiagnosticsCheckBox"
Content="Exclude SP_SERVER_DIAGNOSTICS"
Foreground="{DynamicResource ForegroundBrush}" Margin="20,0,0,4"/>
<CheckBox x:Name="LrqExcludeWaitForCheckBox"
Content="Exclude WAITFOR / BROKER_RECEIVE_WAITFOR"
Foreground="{DynamicResource ForegroundBrush}" Margin="20,0,0,4"/>
<CheckBox x:Name="LrqExcludeBackupsCheckBox"
Content="Exclude backup waits (BACKUPTHREAD, BACKUPIO)"
Foreground="{DynamicResource ForegroundBrush}" Margin="20,0,0,4"/>
<CheckBox x:Name="LrqExcludeMiscWaitsCheckBox"
Content="Exclude miscellaneous waits (XE_LIVE_TARGET_TVF)"
Foreground="{DynamicResource ForegroundBrush}" Margin="20,0,0,0"/>
<StackPanel Orientation="Horizontal" Margin="20,6,0,0">
<CheckBox x:Name="AlertTempDbSpaceCheckBox" Content="TempDB space usage over" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}"/>
Expand Down
Loading
Loading