Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 8 additions & 8 deletions Dashboard/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ public partial class MainWindow : Window
private readonly ConcurrentDictionary<string, DateTime> _lastBlockingAlert = new();
private readonly ConcurrentDictionary<string, DateTime> _lastDeadlockAlert = new();
private readonly ConcurrentDictionary<string, DateTime> _lastHighCpuAlert = new();
private static readonly TimeSpan AlertCooldown = TimeSpan.FromMinutes(5);
private readonly ConcurrentDictionary<string, bool> _activeBlockingAlert = new();
private readonly ConcurrentDictionary<string, bool> _activeDeadlockAlert = new();
private readonly ConcurrentDictionary<string, bool> _activeHighCpuAlert = new();
Expand Down Expand Up @@ -1097,6 +1096,7 @@ private async Task EvaluateAlertConditionsAsync(
string serverId, string serverName, AlertHealthResult health, DatabaseService databaseService)
{
var prefs = _preferencesService.GetPreferences();
var alertCooldown = TimeSpan.FromMinutes(prefs.AlertCooldownMinutes);

if (_alertStateService.IsAnySilencingActive(serverId))
{
Expand All @@ -1112,7 +1112,7 @@ private async Task EvaluateAlertConditionsAsync(
if (blockingExceeded)
{
_activeBlockingAlert[serverId] = true;
if (!_lastBlockingAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
if (!_lastBlockingAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= alertCooldown)
{
_notificationService?.ShowBlockingNotification(
serverName,
Expand Down Expand Up @@ -1158,7 +1158,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
if (deadlocksExceeded)
{
_activeDeadlockAlert[serverId] = true;
if (!_lastDeadlockAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
if (!_lastDeadlockAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= alertCooldown)
{
_notificationService?.ShowDeadlockNotification(
serverName,
Expand Down Expand Up @@ -1197,7 +1197,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
{
var totalCpu = health.TotalCpuPercent!.Value;
_activeHighCpuAlert[serverId] = true;
if (!_lastHighCpuAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
if (!_lastHighCpuAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= alertCooldown)
{
_notificationService?.ShowHighCpuNotification(
serverName,
Expand Down Expand Up @@ -1233,7 +1233,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
if (triggeredWaits.Count > 0)
{
_activePoisonWaitAlert[serverId] = true;
if (!_lastPoisonWaitAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
if (!_lastPoisonWaitAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= alertCooldown)
{
var worst = triggeredWaits[0];
_notificationService?.ShowPoisonWaitNotification(serverName, worst.WaitType, worst.AvgMsPerWait);
Expand Down Expand Up @@ -1270,7 +1270,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
if (longRunningTriggered)
{
_activeLongRunningQueryAlert[serverId] = true;
if (!_lastLongRunningQueryAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
if (!_lastLongRunningQueryAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= alertCooldown)
{
var worst = health.LongRunningQueries[0];
var elapsedMinutes = worst.ElapsedSeconds / 60;
Expand Down Expand Up @@ -1311,7 +1311,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
{
var tempDb = health.TempDbSpace!;
_activeTempDbSpaceAlert[serverId] = true;
if (!_lastTempDbSpaceAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
if (!_lastTempDbSpaceAlert.TryGetValue(serverId, out var lastAlert) || (now - lastAlert) >= alertCooldown)
{
_notificationService?.ShowTempDbSpaceNotification(serverName, tempDb.UsedPercent);
_lastTempDbSpaceAlert[serverId] = now;
Expand Down Expand Up @@ -1350,7 +1350,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
var worst = health.AnomalousJobs[0];
var jobKey = $"{serverId}:{worst.JobId}:{worst.StartTime:O}";

if (!_lastLongRunningJobAlert.TryGetValue(jobKey, out var lastAlert) || (now - lastAlert) >= AlertCooldown)
if (!_lastLongRunningJobAlert.TryGetValue(jobKey, out var lastAlert) || (now - lastAlert) >= alertCooldown)
{
var currentMinutes = worst.CurrentDurationSeconds / 60;
_notificationService?.ShowLongRunningJobNotification(
Expand Down
13 changes: 13 additions & 0 deletions Dashboard/Models/UserPreferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,19 @@ public class UserPreferences
public int TempDbSpaceThresholdPercent { get; set; } = 80; // Alert when TempDB used > X%
public bool NotifyOnLongRunningJobs { get; set; } = true;
public int LongRunningJobMultiplier { get; set; } = 3; // Alert when job runs > Nx historical average
private int _alertCooldownMinutes = 5;
public int AlertCooldownMinutes
{
get => _alertCooldownMinutes;
set => _alertCooldownMinutes = Math.Clamp(value, 1, 120);
}

private int _emailCooldownMinutes = 15;
public int EmailCooldownMinutes
{
get => _emailCooldownMinutes;
set => _emailCooldownMinutes = Math.Clamp(value, 1, 120);
}

// SMTP email alert settings
public bool SmtpEnabled { get; set; } = false;
Expand Down
5 changes: 2 additions & 3 deletions Dashboard/Services/EmailAlertService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public class EmailAlertService

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

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

var cooldownKey = $"{serverId}:{metricName}";
if (_cooldowns.TryGetValue(cooldownKey, out var lastSent) &&
DateTime.UtcNow - lastSent < CooldownPeriod)
DateTime.UtcNow - lastSent < TimeSpan.FromMinutes(prefs.EmailCooldownMinutes))
{
return;
}

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

string? sendError = null;
bool sent = false;
Expand Down
8 changes: 5 additions & 3 deletions Dashboard/Services/EmailTemplateBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ public static (string HtmlBody, string PlainTextBody) BuildAlertEmail(
string serverName,
string currentValue,
string thresholdValue,
int emailCooldownMinutes,
AlertContext? context = null)
{
var utcNow = DateTime.UtcNow;
var localNow = DateTime.Now;
var (accentColor, badgeText) = GetSeverity(metricName);

var html = BuildHtmlBody(metricName, serverName, currentValue,
thresholdValue, utcNow, localNow, accentColor, badgeText, context: context);
thresholdValue, utcNow, localNow, accentColor, badgeText, context: context, emailCooldownMinutes: emailCooldownMinutes);

var plain = BuildPlainTextBody(metricName, serverName, currentValue,
thresholdValue, utcNow, localNow, context);
Expand Down Expand Up @@ -87,7 +88,8 @@ private static string BuildHtmlBody(
string accentColor,
string badgeText,
bool isTest = false,
AlertContext? context = null)
AlertContext? context = null,
int emailCooldownMinutes = 15)
{
var sb = new StringBuilder(2048);

Expand Down Expand Up @@ -167,7 +169,7 @@ private static string BuildHtmlBody(
sb.Append($"Sent by {WebUtility.HtmlEncode(EditionName)}");
if (!isTest)
{
sb.Append(" &middot; 15-minute cooldown between repeat alerts");
sb.Append($" &middot; {emailCooldownMinutes}-minute cooldown between repeat alerts");
}
sb.Append("</span>");
sb.Append("</td></tr>");
Expand Down
10 changes: 10 additions & 0 deletions Dashboard/SettingsWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,16 @@
</StackPanel>
<TextBlock x:Name="AlertPreviewText" FontSize="11" FontStyle="Italic" Foreground="Gray"
TextWrapping="Wrap" Margin="0,12,0,0"/>
<StackPanel Orientation="Horizontal" Margin="0,12,0,0">
<TextBlock Text="Tray notification cooldown:" VerticalAlignment="Center" Margin="0,0,8,0"/>
<TextBox x:Name="AlertCooldownTextBox" Width="50" TextAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Text="minutes" VerticalAlignment="Center" Margin="4,0,0,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Email alert cooldown:" VerticalAlignment="Center" Margin="0,0,8,0"/>
<TextBox x:Name="EmailCooldownTextBox" Width="50" TextAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Text="minutes" VerticalAlignment="Center" Margin="4,0,0,0"/>
</StackPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
Expand Down
39 changes: 30 additions & 9 deletions Dashboard/SettingsWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ private void LoadSettings()
TempDbSpaceThresholdTextBox.Text = prefs.TempDbSpaceThresholdPercent.ToString(CultureInfo.InvariantCulture);
NotifyOnLongRunningJobsCheckBox.IsChecked = prefs.NotifyOnLongRunningJobs;
LongRunningJobMultiplierTextBox.Text = prefs.LongRunningJobMultiplier.ToString(CultureInfo.InvariantCulture);
AlertCooldownTextBox.Text = prefs.AlertCooldownMinutes.ToString(CultureInfo.InvariantCulture);
EmailCooldownTextBox.Text = prefs.EmailCooldownMinutes.ToString(CultureInfo.InvariantCulture);

UpdateNotificationCheckboxStates();

Expand Down Expand Up @@ -311,6 +313,8 @@ private void RestoreAlertDefaultsButton_Click(object sender, RoutedEventArgs e)
LongRunningQueryThresholdTextBox.Text = "30";
TempDbSpaceThresholdTextBox.Text = "80";
LongRunningJobMultiplierTextBox.Text = "3";
AlertCooldownTextBox.Text = "5";
EmailCooldownTextBox.Text = "15";
UpdateAlertPreviewText();
}

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

if (validationErrors.Count > 0)
{
MessageBox.Show(
"Some alert thresholds have invalid values and were not changed:\n\n" +
string.Join("\n", validationErrors),
"Validation", MessageBoxButton.OK, MessageBoxImage.Warning);
}
if (int.TryParse(AlertCooldownTextBox.Text, out int alertCooldown) && alertCooldown >= 1 && alertCooldown <= 120)
prefs.AlertCooldownMinutes = alertCooldown;
else
validationErrors.Add("Tray notification cooldown must be between 1 and 120 minutes");

if (int.TryParse(EmailCooldownTextBox.Text, out int emailCooldown) && emailCooldown >= 1 && emailCooldown <= 120)
prefs.EmailCooldownMinutes = emailCooldown;
else
validationErrors.Add("Email alert cooldown must be between 1 and 120 minutes");

// Save SMTP email settings
prefs.SmtpEnabled = SmtpEnabledCheckBox.IsChecked == true;
Expand All @@ -627,6 +633,8 @@ private void OkButton_Click(object sender, RoutedEventArgs e)
{
prefs.SmtpPort = smtpPort;
}
else
validationErrors.Add("Smtp Port failed validation - must be a valid TCP port number.");
prefs.SmtpUseSsl = SmtpSslCheckBox.IsChecked == true;
prefs.SmtpUsername = SmtpUsernameTextBox.Text?.Trim() ?? "";
prefs.SmtpFromAddress = SmtpFromTextBox.Text?.Trim() ?? "";
Expand All @@ -643,12 +651,25 @@ private void OkButton_Click(object sender, RoutedEventArgs e)
{
prefs.McpPort = mcpPort;
}
else
validationErrors.Add("MCP port failed validation - must be a valid TCP port number.");

_preferencesService.SavePreferences(prefs);

_saved = true;
DialogResult = true;
Close();

if (validationErrors.Count > 0)
{
MessageBox.Show(
"Some settings have invalid values and were not changed:\n\n" +
string.Join("\n", validationErrors),
"Validation", MessageBoxButton.OK, MessageBoxImage.Warning);
}
else
{
DialogResult = true;
Close();
}
}

private void CancelButton_Click(object sender, RoutedEventArgs e)
Expand Down
4 changes: 4 additions & 0 deletions Lite/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ public partial class App : Application
public static int AlertTempDbSpaceThresholdPercent { get; set; } = 80;
public static bool AlertLongRunningJobEnabled { get; set; } = true;
public static int AlertLongRunningJobMultiplier { get; set; } = 3;
public static int AlertCooldownMinutes { get; set; } = 5; // Tray notification cooldown between repeated alerts
public static int EmailCooldownMinutes { get; set; } = 15; // Email cooldown between repeated alerts

/* Connection settings */
public static int ConnectionTimeoutSeconds { get; set; } = 5;
Expand Down Expand Up @@ -256,6 +258,8 @@ public static void LoadAlertSettings()
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();
if (root.TryGetProperty("alert_long_running_job_multiplier", out v)) AlertLongRunningJobMultiplier = v.GetInt32();
if (root.TryGetProperty("alert_cooldown_minutes", out v)) AlertCooldownMinutes = (int)Math.Clamp(v.GetInt64(), 1, 120);
if (root.TryGetProperty("email_cooldown_minutes", out v)) EmailCooldownMinutes = (int)Math.Clamp(v.GetInt64(), 1, 120);

/* Connection settings */
if (root.TryGetProperty("connection_timeout_seconds", out v))
Expand Down
Loading
Loading