Skip to content

Commit 7b5a759

Browse files
Merge pull request #444 from HannahVernon/feature/add-cooldown-period-setting
Feature/add cooldown period setting
2 parents 478f915 + 0746f3f commit 7b5a759

13 files changed

Lines changed: 151 additions & 45 deletions

Dashboard/MainWindow.xaml.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ public partial class MainWindow : Window
6060
private readonly ConcurrentDictionary<string, DateTime> _lastBlockingAlert = new();
6161
private readonly ConcurrentDictionary<string, DateTime> _lastDeadlockAlert = new();
6262
private readonly ConcurrentDictionary<string, DateTime> _lastHighCpuAlert = new();
63-
private static readonly TimeSpan AlertCooldown = TimeSpan.FromMinutes(5);
6463
private readonly ConcurrentDictionary<string, bool> _activeBlockingAlert = new();
6564
private readonly ConcurrentDictionary<string, bool> _activeDeadlockAlert = new();
6665
private readonly ConcurrentDictionary<string, bool> _activeHighCpuAlert = new();
@@ -1097,6 +1096,7 @@ private async Task EvaluateAlertConditionsAsync(
10971096
string serverId, string serverName, AlertHealthResult health, DatabaseService databaseService)
10981097
{
10991098
var prefs = _preferencesService.GetPreferences();
1099+
var alertCooldown = TimeSpan.FromMinutes(prefs.AlertCooldownMinutes);
11001100

11011101
if (_alertStateService.IsAnySilencingActive(serverId))
11021102
{
@@ -1112,7 +1112,7 @@ private async Task EvaluateAlertConditionsAsync(
11121112
if (blockingExceeded)
11131113
{
11141114
_activeBlockingAlert[serverId] = true;
1115-
if (!_lastBlockingAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
1115+
if (!_lastBlockingAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= alertCooldown)
11161116
{
11171117
_notificationService?.ShowBlockingNotification(
11181118
serverName,
@@ -1158,7 +1158,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
11581158
if (deadlocksExceeded)
11591159
{
11601160
_activeDeadlockAlert[serverId] = true;
1161-
if (!_lastDeadlockAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
1161+
if (!_lastDeadlockAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= alertCooldown)
11621162
{
11631163
_notificationService?.ShowDeadlockNotification(
11641164
serverName,
@@ -1197,7 +1197,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
11971197
{
11981198
var totalCpu = health.TotalCpuPercent!.Value;
11991199
_activeHighCpuAlert[serverId] = true;
1200-
if (!_lastHighCpuAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
1200+
if (!_lastHighCpuAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= alertCooldown)
12011201
{
12021202
_notificationService?.ShowHighCpuNotification(
12031203
serverName,
@@ -1233,7 +1233,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
12331233
if (triggeredWaits.Count > 0)
12341234
{
12351235
_activePoisonWaitAlert[serverId] = true;
1236-
if (!_lastPoisonWaitAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
1236+
if (!_lastPoisonWaitAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= alertCooldown)
12371237
{
12381238
var worst = triggeredWaits[0];
12391239
_notificationService?.ShowPoisonWaitNotification(serverName, worst.WaitType, worst.AvgMsPerWait);
@@ -1270,7 +1270,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
12701270
if (longRunningTriggered)
12711271
{
12721272
_activeLongRunningQueryAlert[serverId] = true;
1273-
if (!_lastLongRunningQueryAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
1273+
if (!_lastLongRunningQueryAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= alertCooldown)
12741274
{
12751275
var worst = health.LongRunningQueries[0];
12761276
var elapsedMinutes = worst.ElapsedSeconds / 60;
@@ -1311,7 +1311,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
13111311
{
13121312
var tempDb = health.TempDbSpace!;
13131313
_activeTempDbSpaceAlert[serverId] = true;
1314-
if (!_lastTempDbSpaceAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
1314+
if (!_lastTempDbSpaceAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= alertCooldown)
13151315
{
13161316
_notificationService?.ShowTempDbSpaceNotification(serverName, tempDb.UsedPercent);
13171317
_lastTempDbSpaceAlert[serverId] = now;
@@ -1350,7 +1350,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
13501350
var worst = health.AnomalousJobs[0];
13511351
var jobKey = $"{serverId}:{worst.JobId}:{worst.StartTime:O}";
13521352

1353-
if (!_lastLongRunningJobAlert.TryGetValue(jobKey, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
1353+
if (!_lastLongRunningJobAlert.TryGetValue(jobKey, out var lastAlert) || (now - lastAlert) >= alertCooldown)
13541354
{
13551355
var currentMinutes = worst.CurrentDurationSeconds / 60;
13561356
_notificationService?.ShowLongRunningJobNotification(

Dashboard/Models/UserPreferences.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,19 @@ public class UserPreferences
9494
public int TempDbSpaceThresholdPercent { get; set; } = 80; // Alert when TempDB used > X%
9595
public bool NotifyOnLongRunningJobs { get; set; } = true;
9696
public int LongRunningJobMultiplier { get; set; } = 3; // Alert when job runs > Nx historical average
97+
private int _alertCooldownMinutes = 5;
98+
public int AlertCooldownMinutes
99+
{
100+
get => _alertCooldownMinutes;
101+
set => _alertCooldownMinutes = Math.Clamp(value, 1, 120);
102+
}
103+
104+
private int _emailCooldownMinutes = 15;
105+
public int EmailCooldownMinutes
106+
{
107+
get => _emailCooldownMinutes;
108+
set => _emailCooldownMinutes = Math.Clamp(value, 1, 120);
109+
}
97110

98111
// SMTP email alert settings
99112
public bool SmtpEnabled { get; set; } = false;

Dashboard/Services/EmailAlertService.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ public class EmailAlertService
3333

3434
private readonly UserPreferencesService _preferencesService;
3535
private readonly ConcurrentDictionary<string, DateTime> _cooldowns = new();
36-
private static readonly TimeSpan CooldownPeriod = TimeSpan.FromMinutes(15);
3736

3837
/* Alert log — loaded from JSON on startup, saved on exit, new alerts added in-memory */
3938
private readonly List<AlertLogEntry> _alertLog = new();
@@ -90,14 +89,14 @@ public async Task TrySendAlertEmailAsync(
9089

9190
var cooldownKey = $"{serverId}:{metricName}";
9291
if (_cooldowns.TryGetValue(cooldownKey, out var lastSent) &&
93-
DateTime.UtcNow - lastSent < CooldownPeriod)
92+
DateTime.UtcNow - lastSent < TimeSpan.FromMinutes(prefs.EmailCooldownMinutes))
9493
{
9594
return;
9695
}
9796

9897
var subject = $"[SQL Monitor Alert] {metricName} on {serverName}";
9998
var (htmlBody, plainTextBody) = EmailTemplateBuilder.BuildAlertEmail(
100-
metricName, serverName, currentValue, thresholdValue, context);
99+
metricName, serverName, currentValue, thresholdValue, prefs.EmailCooldownMinutes, context);
101100

102101
string? sendError = null;
103102
bool sent = false;

Dashboard/Services/EmailTemplateBuilder.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ public static (string HtmlBody, string PlainTextBody) BuildAlertEmail(
2727
string serverName,
2828
string currentValue,
2929
string thresholdValue,
30+
int emailCooldownMinutes,
3031
AlertContext? context = null)
3132
{
3233
var utcNow = DateTime.UtcNow;
3334
var localNow = DateTime.Now;
3435
var (accentColor, badgeText) = GetSeverity(metricName);
3536

3637
var html = BuildHtmlBody(metricName, serverName, currentValue,
37-
thresholdValue, utcNow, localNow, accentColor, badgeText, context: context);
38+
thresholdValue, utcNow, localNow, accentColor, badgeText, context: context, emailCooldownMinutes: emailCooldownMinutes);
3839

3940
var plain = BuildPlainTextBody(metricName, serverName, currentValue,
4041
thresholdValue, utcNow, localNow, context);
@@ -87,7 +88,8 @@ private static string BuildHtmlBody(
8788
string accentColor,
8889
string badgeText,
8990
bool isTest = false,
90-
AlertContext? context = null)
91+
AlertContext? context = null,
92+
int emailCooldownMinutes = 15)
9193
{
9294
var sb = new StringBuilder(2048);
9395

@@ -167,7 +169,7 @@ private static string BuildHtmlBody(
167169
sb.Append($"Sent by {WebUtility.HtmlEncode(EditionName)}");
168170
if (!isTest)
169171
{
170-
sb.Append(" &middot; 15-minute cooldown between repeat alerts");
172+
sb.Append($" &middot; {emailCooldownMinutes}-minute cooldown between repeat alerts");
171173
}
172174
sb.Append("</span>");
173175
sb.Append("</td></tr>");

Dashboard/SettingsWindow.xaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,16 @@
267267
</StackPanel>
268268
<TextBlock x:Name="AlertPreviewText" FontSize="11" FontStyle="Italic" Foreground="Gray"
269269
TextWrapping="Wrap" Margin="0,12,0,0"/>
270+
<StackPanel Orientation="Horizontal" Margin="0,12,0,0">
271+
<TextBlock Text="Tray notification cooldown:" VerticalAlignment="Center" Margin="0,0,8,0"/>
272+
<TextBox x:Name="AlertCooldownTextBox" Width="50" TextAlignment="Center" VerticalAlignment="Center"/>
273+
<TextBlock Text="minutes" VerticalAlignment="Center" Margin="4,0,0,0"/>
274+
</StackPanel>
275+
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
276+
<TextBlock Text="Email alert cooldown:" VerticalAlignment="Center" Margin="0,0,8,0"/>
277+
<TextBox x:Name="EmailCooldownTextBox" Width="50" TextAlignment="Center" VerticalAlignment="Center"/>
278+
<TextBlock Text="minutes" VerticalAlignment="Center" Margin="4,0,0,0"/>
279+
</StackPanel>
270280
</StackPanel>
271281
</StackPanel>
272282
</ScrollViewer>

Dashboard/SettingsWindow.xaml.cs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ private void LoadSettings()
171171
TempDbSpaceThresholdTextBox.Text = prefs.TempDbSpaceThresholdPercent.ToString(CultureInfo.InvariantCulture);
172172
NotifyOnLongRunningJobsCheckBox.IsChecked = prefs.NotifyOnLongRunningJobs;
173173
LongRunningJobMultiplierTextBox.Text = prefs.LongRunningJobMultiplier.ToString(CultureInfo.InvariantCulture);
174+
AlertCooldownTextBox.Text = prefs.AlertCooldownMinutes.ToString(CultureInfo.InvariantCulture);
175+
EmailCooldownTextBox.Text = prefs.EmailCooldownMinutes.ToString(CultureInfo.InvariantCulture);
174176

175177
UpdateNotificationCheckboxStates();
176178

@@ -311,6 +313,8 @@ private void RestoreAlertDefaultsButton_Click(object sender, RoutedEventArgs e)
311313
LongRunningQueryThresholdTextBox.Text = "30";
312314
TempDbSpaceThresholdTextBox.Text = "80";
313315
LongRunningJobMultiplierTextBox.Text = "3";
316+
AlertCooldownTextBox.Text = "5";
317+
EmailCooldownTextBox.Text = "15";
314318
UpdateAlertPreviewText();
315319
}
316320

@@ -612,13 +616,15 @@ private void OkButton_Click(object sender, RoutedEventArgs e)
612616
else if (prefs.NotifyOnLongRunningJobs)
613617
validationErrors.Add("Job multiplier must be a positive number");
614618

615-
if (validationErrors.Count > 0)
616-
{
617-
MessageBox.Show(
618-
"Some alert thresholds have invalid values and were not changed:\n\n" +
619-
string.Join("\n", validationErrors),
620-
"Validation", MessageBoxButton.OK, MessageBoxImage.Warning);
621-
}
619+
if (int.TryParse(AlertCooldownTextBox.Text, out int alertCooldown) && alertCooldown >= 1 && alertCooldown <= 120)
620+
prefs.AlertCooldownMinutes = alertCooldown;
621+
else
622+
validationErrors.Add("Tray notification cooldown must be between 1 and 120 minutes");
623+
624+
if (int.TryParse(EmailCooldownTextBox.Text, out int emailCooldown) && emailCooldown >= 1 && emailCooldown <= 120)
625+
prefs.EmailCooldownMinutes = emailCooldown;
626+
else
627+
validationErrors.Add("Email alert cooldown must be between 1 and 120 minutes");
622628

623629
// Save SMTP email settings
624630
prefs.SmtpEnabled = SmtpEnabledCheckBox.IsChecked == true;
@@ -627,6 +633,8 @@ private void OkButton_Click(object sender, RoutedEventArgs e)
627633
{
628634
prefs.SmtpPort = smtpPort;
629635
}
636+
else
637+
validationErrors.Add("Smtp Port failed validation - must be a valid TCP port number.");
630638
prefs.SmtpUseSsl = SmtpSslCheckBox.IsChecked == true;
631639
prefs.SmtpUsername = SmtpUsernameTextBox.Text?.Trim() ?? "";
632640
prefs.SmtpFromAddress = SmtpFromTextBox.Text?.Trim() ?? "";
@@ -643,12 +651,25 @@ private void OkButton_Click(object sender, RoutedEventArgs e)
643651
{
644652
prefs.McpPort = mcpPort;
645653
}
654+
else
655+
validationErrors.Add("MCP port failed validation - must be a valid TCP port number.");
646656

647657
_preferencesService.SavePreferences(prefs);
648658

649659
_saved = true;
650-
DialogResult = true;
651-
Close();
660+
661+
if (validationErrors.Count > 0)
662+
{
663+
MessageBox.Show(
664+
"Some settings have invalid values and were not changed:\n\n" +
665+
string.Join("\n", validationErrors),
666+
"Validation", MessageBoxButton.OK, MessageBoxImage.Warning);
667+
}
668+
else
669+
{
670+
DialogResult = true;
671+
Close();
672+
}
652673
}
653674

654675
private void CancelButton_Click(object sender, RoutedEventArgs e)

Lite/App.xaml.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ public partial class App : Application
7373
public static int AlertTempDbSpaceThresholdPercent { get; set; } = 80;
7474
public static bool AlertLongRunningJobEnabled { get; set; } = true;
7575
public static int AlertLongRunningJobMultiplier { get; set; } = 3;
76+
public static int AlertCooldownMinutes { get; set; } = 5; // Tray notification cooldown between repeated alerts
77+
public static int EmailCooldownMinutes { get; set; } = 15; // Email cooldown between repeated alerts
7678

7779
/* Connection settings */
7880
public static int ConnectionTimeoutSeconds { get; set; } = 5;
@@ -256,6 +258,8 @@ public static void LoadAlertSettings()
256258
if (root.TryGetProperty("alert_tempdb_space_threshold_percent", out v)) AlertTempDbSpaceThresholdPercent = v.GetInt32();
257259
if (root.TryGetProperty("alert_long_running_job_enabled", out v)) AlertLongRunningJobEnabled = v.GetBoolean();
258260
if (root.TryGetProperty("alert_long_running_job_multiplier", out v)) AlertLongRunningJobMultiplier = v.GetInt32();
261+
if (root.TryGetProperty("alert_cooldown_minutes", out v)) AlertCooldownMinutes = (int)Math.Clamp(v.GetInt64(), 1, 120);
262+
if (root.TryGetProperty("email_cooldown_minutes", out v)) EmailCooldownMinutes = (int)Math.Clamp(v.GetInt64(), 1, 120);
259263

260264
/* Connection settings */
261265
if (root.TryGetProperty("connection_timeout_seconds", out v))

0 commit comments

Comments
 (0)