Skip to content

Commit fd68524

Browse files
Lite: CDC exclusion parity with Dashboard (#1096)
Mirror the Dashboard CDC long-running-query exclusion in Lite so both apps behave identically. Lite reads collected DuckDB snapshots and stores only statement-level query text, which can't carry the CDC proc name -- so the detection runs in the COLLECTOR (against the live server), where the whole batch/object text and msdb are available, and the result is stored as a per-row flag: - query_snapshots gains an is_cdc_capture BOOLEAN (schema v28 migration, ALTER ... ADD COLUMN IF NOT EXISTS, appended last to match the appender). - The non-Azure collector template computes is_cdc_capture with the same two-tier logic as Dashboard: program_name -> job_id via msdb.dbo.cdc_jobs (deferred through sp_executesql in TRY/CATCH), text fallback on the whole dest.text when msdb is unreadable. Azure SQL DB has no Agent/msdb, so it is hard-coded 0 there. - GetLongRunningQueriesAsync filters COALESCE(is_cdc_capture, FALSE) = FALSE when the new LongRunningQueryExcludeCdc toggle (default on) is set. - Settings checkbox + App static + JSON load/save, mirroring the existing four wait_type exclusion toggles. The schema-evolution-tolerant archive views (UNION ALL BY NAME + union_by_name=true) keep old parquet compatible. Collector SQL validated against SQL 2016: prologue falls back cleanly when cdc_jobs is absent and the is_cdc_capture column computes without error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6e83e26 commit fd68524

8 files changed

Lines changed: 66 additions & 6 deletions

Lite/App.xaml.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ public partial class App : Application
107107
public static bool AlertLongRunningQueryExcludeWaitFor { get; set; } = true;
108108
public static bool AlertLongRunningQueryExcludeBackups { get; set; } = true;
109109
public static bool AlertLongRunningQueryExcludeMiscWaits { get; set; } = true;
110+
public static bool AlertLongRunningQueryExcludeCdc { get; set; } = true;
110111
public static List<string> AlertExcludedDatabases { get; set; } = new();
111112
public static bool AlertTempDbSpaceEnabled { get; set; } = true;
112113
public static int AlertTempDbSpaceThresholdPercent { get; set; } = 80;
@@ -463,6 +464,7 @@ public static void LoadAlertSettings()
463464
if (root.TryGetProperty("alert_long_running_query_exclude_waitfor", out v)) AlertLongRunningQueryExcludeWaitFor = v.GetBoolean();
464465
if (root.TryGetProperty("alert_long_running_query_exclude_backups", out v)) AlertLongRunningQueryExcludeBackups = v.GetBoolean();
465466
if (root.TryGetProperty("alert_long_running_query_exclude_misc_waits", out v)) AlertLongRunningQueryExcludeMiscWaits = v.GetBoolean();
467+
if (root.TryGetProperty("alert_long_running_query_exclude_cdc", out v)) AlertLongRunningQueryExcludeCdc = v.GetBoolean();
466468
if (root.TryGetProperty("alert_excluded_databases", out v) && v.ValueKind == System.Text.Json.JsonValueKind.Array)
467469
{
468470
AlertExcludedDatabases = new List<string>();

Lite/Database/DuckDbInitializer.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public void Dispose()
9797
/// <summary>
9898
/// Current schema version. Increment this when schema changes require table rebuilds.
9999
/// </summary>
100-
internal const int CurrentSchemaVersion = 27;
100+
internal const int CurrentSchemaVersion = 28;
101101

102102
private readonly string _archivePath;
103103

@@ -732,6 +732,23 @@ New tables only — no existing table changes needed. Tables created by
732732
throw;
733733
}
734734
}
735+
736+
if (fromVersion < 28)
737+
{
738+
/* v28: Added is_cdc_capture flag to query_snapshots so the long-running query
739+
alert can exclude CDC capture sessions. The collector computes the flag
740+
server-side (program_name -> job_id via msdb.dbo.cdc_jobs, text fallback).
741+
Appended at the end to match the DuckDB appender's positional order. */
742+
_logger?.LogInformation("Running migration to v28: adding is_cdc_capture column to query_snapshots");
743+
try
744+
{
745+
await ExecuteNonQueryAsync(connection, "ALTER TABLE query_snapshots ADD COLUMN IF NOT EXISTS is_cdc_capture BOOLEAN DEFAULT false");
746+
}
747+
catch (Exception ex)
748+
{
749+
_logger?.LogWarning("Migration to v28 encountered an error (non-fatal): {Error}", ex.Message);
750+
}
751+
}
735752
}
736753

737754
/// <summary>

Lite/Database/Schema.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,8 @@ granted_query_memory_gb DECIMAL(18,2),
346346
host_name VARCHAR,
347347
program_name VARCHAR,
348348
open_transaction_count INTEGER,
349-
percent_complete DECIMAL(5,2)
349+
percent_complete DECIMAL(5,2),
350+
is_cdc_capture BOOLEAN DEFAULT false
350351
)";
351352

352353
public const string CreateTempdbStatsTable = @"

Lite/MainWindow.xaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1634,7 +1634,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
16341634
{
16351635
try
16361636
{
1637-
var longRunning = await _dataService.GetLongRunningQueriesAsync(summary.ServerId, App.AlertLongRunningQueryThresholdMinutes, App.AlertLongRunningQueryMaxResults, App.AlertLongRunningQueryExcludeSpServerDiagnostics, App.AlertLongRunningQueryExcludeWaitFor, App.AlertLongRunningQueryExcludeBackups, App.AlertLongRunningQueryExcludeMiscWaits);
1637+
var longRunning = await _dataService.GetLongRunningQueriesAsync(summary.ServerId, App.AlertLongRunningQueryThresholdMinutes, App.AlertLongRunningQueryMaxResults, App.AlertLongRunningQueryExcludeSpServerDiagnostics, App.AlertLongRunningQueryExcludeWaitFor, App.AlertLongRunningQueryExcludeBackups, App.AlertLongRunningQueryExcludeMiscWaits, App.AlertLongRunningQueryExcludeCdc);
16381638

16391639
if (App.AlertExcludedDatabases.Count > 0)
16401640
{

Lite/Services/LocalDataService.WaitStats.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,8 @@ public async Task<List<LongRunningQueryInfo>> GetLongRunningQueriesAsync(
436436
bool excludeSpServerDiagnostics = true,
437437
bool excludeWaitFor = true,
438438
bool excludeBackups = true,
439-
bool excludeMiscWaits = true)
439+
bool excludeMiscWaits = true,
440+
bool excludeCdc = true)
440441
{
441442
using var connection = await OpenConnectionAsync();
442443
using var command = connection.CreateCommand();
@@ -451,6 +452,10 @@ public async Task<List<LongRunningQueryInfo>> GetLongRunningQueriesAsync(
451452
? "AND r.wait_type NOT IN (N'BACKUPTHREAD', N'BACKUPIO')" : "";
452453
string miscWaitsFilter = excludeMiscWaits
453454
? "AND r.wait_type NOT IN (N'XE_LIVE_TARGET_TVF')" : "";
455+
// CDC capture sessions are flagged server-side by the collector (is_cdc_capture). COALESCE
456+
// guards pre-migration / archived rows where the column is NULL.
457+
string cdcFilter = excludeCdc
458+
? "AND COALESCE(r.is_cdc_capture, FALSE) = FALSE" : "";
454459
maxResults = Math.Clamp(maxResults, 1, 1000);
455460

456461
command.CommandText = @$"
@@ -473,6 +478,7 @@ AND r.session_id > 50
473478
{waitForFilter}
474479
{backupsFilter}
475480
{miscWaitsFilter}
481+
{cdcFilter}
476482
AND r.total_elapsed_time_ms >= $2
477483
ORDER BY r.total_elapsed_time_ms DESC
478484
LIMIT $3;";

Lite/Services/RemoteCollectorService.QuerySnapshots.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ public partial class RemoteCollectorService
2525
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
2626
SET LOCK_TIMEOUT 1000;
2727
28+
DECLARE @cdc_capture_jobs TABLE (job_id uniqueidentifier PRIMARY KEY);
29+
DECLARE @cdc_readable bit = 0;
30+
BEGIN TRY
31+
INSERT @cdc_capture_jobs (job_id)
32+
EXEC sys.sp_executesql N'SELECT cj.job_id FROM msdb.dbo.cdc_jobs AS cj WHERE cj.job_type = N''capture'';';
33+
SET @cdc_readable = 1;
34+
END TRY
35+
BEGIN CATCH
36+
SET @cdc_readable = 0;
37+
END CATCH;
38+
2839
SELECT /* PerformanceMonitorLite */
2940
der.session_id,
3041
database_name = d.name,
@@ -68,7 +79,21 @@ WHEN 5 THEN 'Snapshot'
6879
des.host_name,
6980
des.program_name,
7081
des.open_transaction_count,
71-
der.percent_complete
82+
der.percent_complete,
83+
is_cdc_capture =
84+
CONVERT(bit,
85+
CASE
86+
WHEN @cdc_readable = 1
87+
AND des.program_name LIKE N'SQLAgent - TSQL JobStep (Job 0x%'
88+
AND TRY_CONVERT(uniqueidentifier, TRY_CONVERT(binary(16), SUBSTRING(des.program_name, 32, 32), 2))
89+
IN (SELECT j.job_id FROM @cdc_capture_jobs AS j)
90+
THEN 1
91+
WHEN @cdc_readable = 0
92+
AND dest.text IS NOT NULL
93+
AND (dest.text LIKE N'%sp_MScdc_capture_job%' OR dest.text LIKE N'%sp_cdc_scan%')
94+
THEN 1
95+
ELSE 0
96+
END)
7297
FROM sys.dm_exec_requests AS der
7398
JOIN sys.dm_exec_sessions AS des
7499
ON des.session_id = der.session_id
@@ -174,7 +199,9 @@ WHEN 5 THEN 'Snapshot'
174199
des.host_name,
175200
des.program_name,
176201
des.open_transaction_count,
177-
der.percent_complete
202+
der.percent_complete,
203+
/* Azure SQL Database has no SQL Agent / msdb.dbo.cdc_jobs (CDC there is scheduler-based), so no capture job to exclude. */
204+
is_cdc_capture = CONVERT(bit, 0)
178205
FROM #req AS der
179206
JOIN sys.dm_exec_sessions AS des
180207
ON des.session_id = der.session_id
@@ -300,6 +327,7 @@ clause is "" so nothing changes. */
300327
.AppendValue(reader.IsDBNull(22) ? (string?)null : reader.GetString(22)) /* program_name */
301328
.AppendValue(reader.IsDBNull(23) ? 0 : Convert.ToInt32(reader.GetValue(23))) /* open_transaction_count */
302329
.AppendValue(reader.IsDBNull(24) ? 0m : Convert.ToDecimal(reader.GetValue(24))) /* percent_complete */
330+
.AppendValue(!reader.IsDBNull(25) && Convert.ToBoolean(reader.GetValue(25))) /* is_cdc_capture */
303331
.EndRow();
304332

305333
rowsCollected++;

Lite/Windows/SettingsWindow.xaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@
211211
Foreground="{DynamicResource ForegroundBrush}" Margin="20,0,0,4"/>
212212
<CheckBox x:Name="LrqExcludeMiscWaitsCheckBox"
213213
Content="Exclude miscellaneous waits (XE_LIVE_TARGET_TVF)"
214+
Foreground="{DynamicResource ForegroundBrush}" Margin="20,0,0,4"/>
215+
<CheckBox x:Name="LrqExcludeCdcCheckBox"
216+
Content="Exclude CDC capture jobs (sp_MScdc_capture_job, sp_cdc_scan)"
214217
Foreground="{DynamicResource ForegroundBrush}" Margin="20,0,0,0"/>
215218
<StackPanel Orientation="Horizontal" Margin="20,6,0,0">
216219
<CheckBox x:Name="AlertTempDbSpaceCheckBox" Content="TempDB space usage over" VerticalAlignment="Center"

Lite/Windows/SettingsWindow.xaml.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@ private void LoadAlertSettings()
593593
LrqExcludeWaitForCheckBox.IsChecked = App.AlertLongRunningQueryExcludeWaitFor;
594594
LrqExcludeBackupsCheckBox.IsChecked = App.AlertLongRunningQueryExcludeBackups;
595595
LrqExcludeMiscWaitsCheckBox.IsChecked = App.AlertLongRunningQueryExcludeMiscWaits;
596+
LrqExcludeCdcCheckBox.IsChecked = App.AlertLongRunningQueryExcludeCdc;
596597
AlertExcludedDatabasesBox.Text = string.Join(", ", App.AlertExcludedDatabases);
597598
AlertTempDbSpaceCheckBox.IsChecked = App.AlertTempDbSpaceEnabled;
598599
AlertTempDbSpaceThresholdBox.Text = App.AlertTempDbSpaceThresholdPercent.ToString();
@@ -642,6 +643,7 @@ private bool SaveAlertSettings()
642643
App.AlertLongRunningQueryExcludeWaitFor = LrqExcludeWaitForCheckBox.IsChecked == true;
643644
App.AlertLongRunningQueryExcludeBackups = LrqExcludeBackupsCheckBox.IsChecked == true;
644645
App.AlertLongRunningQueryExcludeMiscWaits = LrqExcludeMiscWaitsCheckBox.IsChecked == true;
646+
App.AlertLongRunningQueryExcludeCdc = LrqExcludeCdcCheckBox.IsChecked == true;
645647
App.AlertExcludedDatabases = AlertExcludedDatabasesBox.Text
646648
.Split(',')
647649
.Select(s => s.Trim())
@@ -709,6 +711,7 @@ private bool SaveAlertSettings()
709711
root["alert_long_running_query_exclude_waitfor"] = App.AlertLongRunningQueryExcludeWaitFor;
710712
root["alert_long_running_query_exclude_backups"] = App.AlertLongRunningQueryExcludeBackups;
711713
root["alert_long_running_query_exclude_misc_waits"] = App.AlertLongRunningQueryExcludeMiscWaits;
714+
root["alert_long_running_query_exclude_cdc"] = App.AlertLongRunningQueryExcludeCdc;
712715
var dbArray = new System.Text.Json.Nodes.JsonArray();
713716
foreach (var db in App.AlertExcludedDatabases) dbArray.Add(db);
714717
root["alert_excluded_databases"] = dbArray;

0 commit comments

Comments
 (0)