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
1 change: 1 addition & 0 deletions Dashboard/Controls/AlertsHistoryContent.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ private void MuteThisAlert_Click(object sender, RoutedEventArgs e)
ServerName = item.ServerName,
MetricName = item.MetricName
};
context.PopulateFromDetailText(item.DetailText);

var dialog = new MuteRuleDialog(context) { Owner = Window.GetWindow(this) };
if (dialog.ShowDialog() == true)
Expand Down
3 changes: 2 additions & 1 deletion Dashboard/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
// Sync preferences
var startupPrefs = _preferencesService.GetPreferences();
TabHelpers.CsvSeparator = startupPrefs.CsvSeparator;
MuteRuleDialog.DefaultExpiration = startupPrefs.MuteRuleDefaultExpiration;
if (Enum.TryParse<Helpers.TimeDisplayMode>(startupPrefs.TimeDisplayMode, out var tdm))
Helpers.ServerTimeHelper.CurrentDisplayMode = tdm;

Expand Down Expand Up @@ -1654,7 +1655,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
private static string Truncate(string text, int maxLength = 300)
{
if (string.IsNullOrEmpty(text)) return "";
text = text.Trim();
text = text.Replace('\r', ' ').Replace('\n', ' ').Trim();
return text.Length <= maxLength ? text : text.Substring(0, maxLength) + "...";
}

Expand Down
278 changes: 177 additions & 101 deletions Dashboard/Models/MuteRule.cs
Original file line number Diff line number Diff line change
@@ -1,101 +1,177 @@
using System;

namespace PerformanceMonitorDashboard.Models
{
public class MuteRule
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public bool Enabled { get; set; } = true;
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime? ExpiresAtUtc { get; set; }
public string? Reason { get; set; }

public string? ServerName { get; set; }
public string? MetricName { get; set; }
public string? DatabasePattern { get; set; }
public string? QueryTextPattern { get; set; }
public string? WaitTypePattern { get; set; }
public string? JobNamePattern { get; set; }

public bool IsExpired => ExpiresAtUtc.HasValue && DateTime.UtcNow >= ExpiresAtUtc.Value;

public MuteRule Clone() => new()
{
Id = Id,
Enabled = Enabled,
CreatedAtUtc = CreatedAtUtc,
ExpiresAtUtc = ExpiresAtUtc,
Reason = Reason,
ServerName = ServerName,
MetricName = MetricName,
DatabasePattern = DatabasePattern,
QueryTextPattern = QueryTextPattern,
WaitTypePattern = WaitTypePattern,
JobNamePattern = JobNamePattern
};

public string ExpiresDisplay => ExpiresAtUtc.HasValue
? (IsExpired ? "Expired" : ExpiresAtUtc.Value.ToLocalTime().ToString("g"))
: "Never";

public string Summary
{
get
{
var parts = new System.Collections.Generic.List<string>();
if (MetricName != null) parts.Add(MetricName);
if (ServerName != null) parts.Add($"on {ServerName}");
if (DatabasePattern != null) parts.Add($"db≈{DatabasePattern}");
if (QueryTextPattern != null) parts.Add($"query≈{QueryTextPattern}");
if (WaitTypePattern != null) parts.Add($"wait≈{WaitTypePattern}");
if (JobNamePattern != null) parts.Add($"job≈{JobNamePattern}");
return parts.Count > 0 ? string.Join(", ", parts) : "(matches all alerts)";
}
}

public bool Matches(AlertMuteContext context)
{
if (!Enabled || IsExpired) return false;

if (ServerName != null &&
!string.Equals(ServerName, context.ServerName, StringComparison.OrdinalIgnoreCase))
return false;

if (MetricName != null &&
!string.Equals(MetricName, context.MetricName, StringComparison.OrdinalIgnoreCase))
return false;

if (DatabasePattern != null &&
(context.DatabaseName == null ||
!context.DatabaseName.Contains(DatabasePattern, StringComparison.OrdinalIgnoreCase)))
return false;

if (QueryTextPattern != null &&
(context.QueryText == null ||
!context.QueryText.Contains(QueryTextPattern, StringComparison.OrdinalIgnoreCase)))
return false;

if (WaitTypePattern != null &&
(context.WaitType == null ||
!context.WaitType.Contains(WaitTypePattern, StringComparison.OrdinalIgnoreCase)))
return false;

if (JobNamePattern != null &&
(context.JobName == null ||
!context.JobName.Contains(JobNamePattern, StringComparison.OrdinalIgnoreCase)))
return false;

return true;
}
}

public class AlertMuteContext
{
public string ServerName { get; set; } = "";
public string MetricName { get; set; } = "";
public string? DatabaseName { get; set; }
public string? QueryText { get; set; }
public string? WaitType { get; set; }
public string? JobName { get; set; }
}
}
using System;

namespace PerformanceMonitorDashboard.Models
{
public class MuteRule
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public bool Enabled { get; set; } = true;
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime? ExpiresAtUtc { get; set; }
public string? Reason { get; set; }

public string? ServerName { get; set; }
public string? MetricName { get; set; }
public string? DatabasePattern { get; set; }
public string? QueryTextPattern { get; set; }
public string? WaitTypePattern { get; set; }
public string? JobNamePattern { get; set; }

public bool IsExpired => ExpiresAtUtc.HasValue && DateTime.UtcNow >= ExpiresAtUtc.Value;

public MuteRule Clone() => new()
{
Id = Id,
Enabled = Enabled,
CreatedAtUtc = CreatedAtUtc,
ExpiresAtUtc = ExpiresAtUtc,
Reason = Reason,
ServerName = ServerName,
MetricName = MetricName,
DatabasePattern = DatabasePattern,
QueryTextPattern = QueryTextPattern,
WaitTypePattern = WaitTypePattern,
JobNamePattern = JobNamePattern
};

public string ExpiresDisplay => ExpiresAtUtc.HasValue
? (IsExpired ? "Expired" : ExpiresAtUtc.Value.ToLocalTime().ToString("g"))
: "Never";

public string Summary
{
get
{
var parts = new System.Collections.Generic.List<string>();
if (MetricName != null) parts.Add(MetricName);
if (ServerName != null) parts.Add($"on {ServerName}");
if (DatabasePattern != null) parts.Add($"db≈{DatabasePattern}");
if (QueryTextPattern != null) parts.Add($"query≈{QueryTextPattern}");
if (WaitTypePattern != null) parts.Add($"wait≈{WaitTypePattern}");
if (JobNamePattern != null) parts.Add($"job≈{JobNamePattern}");
return parts.Count > 0 ? string.Join(", ", parts) : "(matches all alerts)";
}
}

public bool Matches(AlertMuteContext context)
{
if (!Enabled || IsExpired) return false;

if (ServerName != null &&
!string.Equals(ServerName, context.ServerName, StringComparison.OrdinalIgnoreCase))
return false;

if (MetricName != null &&
!string.Equals(MetricName, context.MetricName, StringComparison.OrdinalIgnoreCase))
return false;

if (DatabasePattern != null &&
(context.DatabaseName == null ||
!context.DatabaseName.Contains(DatabasePattern, StringComparison.OrdinalIgnoreCase)))
return false;

if (QueryTextPattern != null &&
(context.QueryText == null ||
!context.QueryText.Contains(QueryTextPattern, StringComparison.OrdinalIgnoreCase)))
return false;

if (WaitTypePattern != null &&
(context.WaitType == null ||
!context.WaitType.Contains(WaitTypePattern, StringComparison.OrdinalIgnoreCase)))
return false;

if (JobNamePattern != null &&
(context.JobName == null ||
!context.JobName.Contains(JobNamePattern, StringComparison.OrdinalIgnoreCase)))
return false;

return true;
}
}

public class AlertMuteContext
{
public string ServerName { get; set; } = "";
public string MetricName { get; set; } = "";
public string? DatabaseName { get; set; }
public string? QueryText { get; set; }
public string? WaitType { get; set; }
public string? JobName { get; set; }

/// <summary>
/// Extracts context fields (Database, Query, Wait Type, Job Name) from the
/// structured detail_text stored with each alert. The format is label/value
/// pairs indented with two spaces, e.g. " Database: MyDB".
/// Query values may span multiple lines and use variant labels
/// (Blocked Query, Blocking Query, Victim SQL).
/// </summary>
public void PopulateFromDetailText(string? detailText)
{
if (string.IsNullOrEmpty(detailText)) return;

System.Text.StringBuilder? queryBuilder = null;
var lines = detailText.Split('\n');

foreach (var line in lines)
{
var trimmed = line.TrimStart();

if (DatabaseName == null && trimmed.StartsWith("Database: ", StringComparison.Ordinal))
{
FlushQuery(ref queryBuilder);
DatabaseName = trimmed.Substring("Database: ".Length).Trim();
}
else if (WaitType == null && trimmed.StartsWith("Wait Type: ", StringComparison.Ordinal))
{
FlushQuery(ref queryBuilder);
WaitType = trimmed.Substring("Wait Type: ".Length).Trim();
}
else if (JobName == null && trimmed.StartsWith("Job Name: ", StringComparison.Ordinal))
{
FlushQuery(ref queryBuilder);
JobName = trimmed.Substring("Job Name: ".Length).Trim();
}
else if (QueryText == null && queryBuilder == null && TryExtractQueryValue(trimmed, out var qv))
{
queryBuilder = new System.Text.StringBuilder(qv);
}
else if (queryBuilder != null)
{
// Continuation lines from multi-line query values don't start
// with the two-space indent used by ContextToDetailText fields.
if (string.IsNullOrWhiteSpace(trimmed) || line.StartsWith(" ", StringComparison.Ordinal))
{
FlushQuery(ref queryBuilder);
}
else
{
queryBuilder.Append(' ').Append(trimmed.Trim());
}
}
}

FlushQuery(ref queryBuilder);
}

private void FlushQuery(ref System.Text.StringBuilder? builder)
{
if (builder != null && QueryText == null)
QueryText = builder.ToString();
builder = null;
}

private static bool TryExtractQueryValue(string trimmed, out string value)
{
foreach (var prefix in new[] { "Query: ", "Blocked Query: ", "Blocking Query: ", "Victim SQL: " })
{
if (trimmed.StartsWith(prefix, StringComparison.Ordinal))
{
value = trimmed.Substring(prefix.Length).Trim();
return true;
}
}
value = "";
return false;
}
}
}
3 changes: 3 additions & 0 deletions Dashboard/Models/UserPreferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ private static string GetDefaultCsvSeparator()
// Alert database exclusions
public List<string> AlertExcludedDatabases { get; set; } = new();

// Default mute rule expiration ("1 hour", "24 hours", "7 days", "Never")
public string MuteRuleDefaultExpiration { get; set; } = "24 hours";

// Alert suppression (persisted)
public List<string> SilencedServers { get; set; } = new();
public List<string> SilencedServerTabs { get; set; } = new();
Expand Down
Loading
Loading