Skip to content

Commit add515e

Browse files
Merge pull request erikdarlingdata#906 from erikdarlingdata/feature/887-lite-per-database-exclusions
Per-database exclusions for collectors — Lite side (closes erikdarlingdata#887)
2 parents 6c55dc6 + f79c14c commit add515e

14 files changed

Lines changed: 397 additions & 9 deletions

Lite/Models/ServerConnection.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ public bool UseWindowsAuth
9595
/// </summary>
9696
public bool MultiSubnetFailover { get; set; } = false;
9797

98+
/// <summary>
99+
/// User databases to skip in per-database collectors (query_store, file_io_stats, etc.).
100+
/// System databases (master/tempdb/model/msdb) and the connection database itself are always
101+
/// excluded by the collectors and aren't represented here.
102+
/// </summary>
103+
public System.Collections.Generic.List<string> ExcludedDatabases { get; set; } = new();
104+
98105
/// <summary>
99106
/// Server name with "(Read-Only)" suffix when ReadOnlyIntent is enabled.
100107
/// Used for sidebar subtitle and status text.

Lite/Services/RemoteCollectorService.DatabaseSize.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ private async Task<int> CollectDatabaseSizeStatsAsync(ServerConnection server, C
3030
var serverStatus = _serverManager.GetConnectionStatus(server.Id);
3131
bool isAzureSqlDb = serverStatus?.SqlEngineEdition == 5;
3232

33-
const string onPremQuery = @"
33+
string onPremQuery = @"
3434
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
3535
SET NOCOUNT ON;
3636
@@ -52,6 +52,7 @@ FROM sys.databases AS d
5252
WHERE d.state_desc = N'ONLINE'
5353
AND d.database_id > 0
5454
AND HAS_DBACCESS(d.name) = 1
55+
/*EXCLUSION_FILTER_CURSOR*/
5556
ORDER BY
5657
d.name;
5758
@@ -131,11 +132,19 @@ LEFT JOIN #file_space AS fs
131132
ON fs.database_id = mf.database_id
132133
AND fs.file_id = mf.file_id
133134
WHERE d.state_desc = N'ONLINE'
135+
/*EXCLUSION_FILTER_OUTER*/
134136
ORDER BY
135137
d.name,
136138
mf.file_id
137139
OPTION(RECOMPILE);";
138140

141+
/* Both filter sites (cursor SELECT and final SELECT) are in outer T-SQL, not nested dynamic SQL,
142+
so parameter bindings work fine and the same @excl_db_N can be referenced twice. */
143+
var (dbSizeExclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
144+
onPremQuery = onPremQuery
145+
.Replace("/*EXCLUSION_FILTER_CURSOR*/", dbSizeExclusionClause)
146+
.Replace("/*EXCLUSION_FILTER_OUTER*/", dbSizeExclusionClause);
147+
139148
const string azureSqlDbQuery = @"
140149
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
141150
@@ -231,6 +240,8 @@ ORDER BY
231240
using var sqlConnection = await CreateConnectionAsync(server, cancellationToken);
232241
using var command = new SqlCommand(onPremQuery, sqlConnection);
233242
command.CommandTimeout = CommandTimeoutSeconds;
243+
var (_, dbSizeExclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
244+
foreach (var p in dbSizeExclusionParams) command.Parameters.Add(p);
234245

235246
using var reader = await command.ExecuteReaderAsync(cancellationToken);
236247
while (await reader.ReadAsync(cancellationToken))

Lite/Services/RemoteCollectorService.FileIo.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,13 @@ LEFT JOIN sys.databases AS d
8383
WHERE (vfs.database_id > 4 OR vfs.database_id = 2)
8484
AND vfs.database_id < 32761
8585
AND vfs.database_id <> ISNULL(DB_ID(N'PerformanceMonitor'), 0)
86+
/*EXCLUSION_FILTER*/
8687
OPTION(RECOMPILE);";
8788

89+
/* Azure path filters via GetAzureDatabaseListAsync; on-prem path injects here */
90+
var (fileIoExclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
91+
query = query.Replace("/*EXCLUSION_FILTER*/", isAzureSqlDb ? string.Empty : fileIoExclusionClause);
92+
8893
var serverId = GetServerId(server);
8994
var collectionTime = DateTime.UtcNow;
9095
var rowsCollected = 0;
@@ -125,6 +130,8 @@ AND vfs.database_id < 32761
125130
{
126131
using var sqlConnection = await CreateConnectionAsync(server, cancellationToken);
127132
using var command = new SqlCommand(query, sqlConnection) { CommandTimeout = CommandTimeoutSeconds };
133+
var (_, fileIoExclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
134+
foreach (var p in fileIoExclusionParams) command.Parameters.Add(p);
128135
using var reader = await command.ExecuteReaderAsync(cancellationToken);
129136
while (await reader.ReadAsync(cancellationToken))
130137
fileStats.Add(ReadFileIoRow(reader));

Lite/Services/RemoteCollectorService.ProcedureStats.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ so the standard NOT IN filter excludes everything. Use a simplified query. */
3232
/* total_spills/min_spills/max_spills exist in dm_exec_procedure_stats and dm_exec_trigger_stats
3333
on all supported versions, but do NOT exist in dm_exec_function_stats on any version.
3434
Use dynamic SQL to handle this. */
35-
const string standardQuery = @"
35+
string standardQuery = @"
3636
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
3737
3838
DECLARE
@@ -81,6 +81,7 @@ INNER JOIN sys.databases AS d
8181
WHERE d.state = 0
8282
AND pa.dbid NOT IN (1, 3, 4, 32761, 32767, ISNULL(DB_ID(N''PerformanceMonitor''), 0))
8383
AND s.last_execution_time >= DATEADD(MINUTE, -10, GETDATE())
84+
/*EXCLUSION_FILTER*/
8485
8586
UNION ALL
8687
@@ -136,6 +137,7 @@ INNER JOIN sys.databases AS d
136137
WHERE d.state = 0
137138
AND pa.dbid NOT IN (1, 3, 4, 32761, 32767, ISNULL(DB_ID(N''PerformanceMonitor''), 0))
138139
AND s.last_execution_time >= DATEADD(MINUTE, -10, GETDATE())
140+
/*EXCLUSION_FILTER*/
139141
140142
UNION ALL
141143
@@ -178,6 +180,7 @@ INNER JOIN sys.databases AS d
178180
WHERE d.state = 0
179181
AND pa.dbid NOT IN (1, 3, 4, 32761, 32767, ISNULL(DB_ID(N''PerformanceMonitor''), 0))
180182
AND s.last_execution_time >= DATEADD(MINUTE, -10, GETDATE())
183+
/*EXCLUSION_FILTER*/
181184
) AS combined
182185
ORDER BY total_elapsed_time DESC
183186
OPTION(RECOMPILE);' AS nvarchar(max));
@@ -220,9 +223,19 @@ No trigger stats or function stats — Azure SQL DB scope is single-database. */
220223
FROM sys.dm_exec_procedure_stats AS s
221224
WHERE s.database_id = DB_ID()
222225
AND s.last_execution_time >= DATEADD(MINUTE, -10, GETDATE())
226+
/*EXCLUSION_FILTER*/
223227
ORDER BY s.total_elapsed_time DESC
224228
OPTION(RECOMPILE);";
225229

230+
/* Standard query is dynamic SQL (built into @sql then passed to sp_executesql), so the
231+
exclusion filter is interpolated as literal N'...' values rather than parameter bindings.
232+
Names come from a user-picked checklist of existing DBs, escaped against single-quote
233+
injection. forNestedDynamicSql=true doubles the escape because @sql is itself a
234+
single-quoted T-SQL string at the outer layer. */
235+
string standardExclusionClause = BuildDatabaseExclusionLiteralClause(
236+
server.ExcludedDatabases, "d.name", forNestedDynamicSql: true);
237+
standardQuery = standardQuery.Replace("/*EXCLUSION_FILTER*/", standardExclusionClause);
238+
226239
string query = isAzureSqlDb ? azureSqlDbQuery : standardQuery;
227240

228241
var serverId = GetServerId(server);

Lite/Services/RemoteCollectorService.QuerySnapshots.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,18 @@ private async Task<int> CollectQuerySnapshotsAsync(ServerConnection server, Canc
232232

233233
var query = BuildQuerySnapshotsQuery(supportsLiveQueryPlan, isAzureSqlDatabase);
234234

235+
/* Append the per-database exclusion filter to the WHERE clause. The base query joins
236+
sys.databases AS d, so we filter on d.name. When ExcludedDatabases is empty the
237+
clause is "" so nothing changes. */
238+
var (qsExclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
239+
if (!string.IsNullOrEmpty(qsExclusionClause))
240+
{
241+
/* Inject just before the OPTION clause so it lands inside the WHERE. */
242+
query = query.Replace(
243+
"ORDER BY der.cpu_time DESC",
244+
qsExclusionClause + "\nORDER BY der.cpu_time DESC");
245+
}
246+
235247
var serverId = GetServerId(server);
236248
var collectionTime = DateTime.UtcNow;
237249
var rowsCollected = 0;
@@ -242,6 +254,8 @@ private async Task<int> CollectQuerySnapshotsAsync(ServerConnection server, Canc
242254
using var sqlConnection = await CreateConnectionAsync(server, cancellationToken);
243255
using var command = new SqlCommand(query, sqlConnection);
244256
command.CommandTimeout = CommandTimeoutSeconds;
257+
var (_, qsExclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
258+
foreach (var p in qsExclusionParams) command.Parameters.Add(p);
245259

246260
using var reader = await command.ExecuteReaderAsync(cancellationToken);
247261
sqlSw.Stop();

Lite/Services/RemoteCollectorService.QueryStats.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Use a simplified query that skips plan_attributes entirely — there's only one
3131
var serverStatus = _serverManager.GetConnectionStatus(server.Id);
3232
bool isAzureSqlDb = serverStatus.SqlEngineEdition == 5;
3333

34-
const string standardQuery = @"
34+
string standardQuery = @"
3535
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
3636
3737
SELECT /* PerformanceMonitorLite */ TOP (200)
@@ -106,10 +106,14 @@ INNER JOIN sys.databases AS d
106106
WHERE pa.dbid NOT IN (1, 2, 3, 4, 32761, 32767, ISNULL(DB_ID(N'PerformanceMonitor'), 0))
107107
AND st.text NOT LIKE N'%PerformanceMonitorLite%'
108108
AND qs.last_execution_time >= DATEADD(MINUTE, -10, GETDATE())
109+
/*EXCLUSION_FILTER*/
109110
ORDER BY
110111
qs.total_elapsed_time DESC
111112
OPTION(RECOMPILE);";
112113

114+
var (exclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
115+
standardQuery = standardQuery.Replace("/*EXCLUSION_FILTER*/", exclusionClause);
116+
113117
/* Azure SQL DB: skip plan_attributes, use DB_NAME() for the single database context */
114118
const string azureSqlDbQuery = @"
115119
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
@@ -229,6 +233,11 @@ qs.total_elapsed_time DESC
229233
{
230234
using var command = new SqlCommand(query, sqlConnection);
231235
command.CommandTimeout = CommandTimeoutSeconds;
236+
if (!isAzureSqlDb)
237+
{
238+
var (_, freshParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
239+
foreach (var p in freshParams) command.Parameters.Add(p);
240+
}
232241

233242
using var reader = await command.ExecuteReaderAsync(cancellationToken);
234243

Lite/Services/RemoteCollectorService.QueryStore.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Uses sys.database_query_store_options.actual_state instead of
3232
var serverStatus = _serverManager.GetConnectionStatus(server.Id);
3333
bool isAzureSqlDb = serverStatus?.SqlEngineEdition == 5;
3434

35-
const string onPremDbQuery = @"
35+
string onPremDbQuery = @"
3636
SET NOCOUNT ON;
3737
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
3838
@@ -60,6 +60,7 @@ AND d.name <> N'PerformanceMonitor'
6060
drs.database_id IS NULL /*not in any AG*/
6161
OR drs.is_primary_replica = 1 /*primary replica*/
6262
)
63+
/*EXCLUSION_FILTER*/
6364
OPTION(RECOMPILE);
6465
6566
OPEN db_check;
@@ -103,7 +104,7 @@ FROM @result
103104
ORDER BY
104105
name;";
105106

106-
const string azureDbQuery = @"
107+
string azureDbQuery = @"
107108
SET NOCOUNT ON;
108109
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
109110
@@ -123,6 +124,7 @@ WHERE d.database_id > 4
123124
AND d.database_id < 32761
124125
AND d.state_desc = N'ONLINE'
125126
AND d.name <> N'PerformanceMonitor'
127+
/*EXCLUSION_FILTER*/
126128
OPTION(RECOMPILE);
127129
128130
OPEN db_check;
@@ -166,6 +168,10 @@ FROM @result
166168
ORDER BY
167169
name;";
168170

171+
var (exclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
172+
onPremDbQuery = onPremDbQuery.Replace("/*EXCLUSION_FILTER*/", exclusionClause);
173+
azureDbQuery = azureDbQuery.Replace("/*EXCLUSION_FILTER*/", exclusionClause);
174+
169175
string dbQuery = isAzureSqlDb ? azureDbQuery : onPremDbQuery;
170176

171177
var serverId = GetServerId(server);
@@ -187,6 +193,8 @@ ORDER BY
187193
using (var dbCommand = new SqlCommand(dbQuery, sqlConnection))
188194
{
189195
dbCommand.CommandTimeout = CommandTimeoutSeconds;
196+
var (_, exclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
197+
foreach (var p in exclusionParams) dbCommand.Parameters.Add(p);
190198
using var dbReader = await dbCommand.ExecuteReaderAsync(cancellationToken);
191199
while (await dbReader.ReadAsync(cancellationToken))
192200
{

Lite/Services/RemoteCollectorService.ServerConfig.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ private async Task<int> CollectDatabaseConfigAsync(ServerConnection server, Canc
149149
is_optimized_locking_on = d.is_optimized_locking_on";
150150
}
151151

152+
var (dbConfigExclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
153+
152154
var query = $@"
153155
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
154156
@@ -158,6 +160,7 @@ FROM sys.databases AS d
158160
WHERE (d.database_id > 4 OR d.database_id = 2)
159161
AND d.database_id < 32761
160162
AND d.name <> N'PerformanceMonitor'
163+
{dbConfigExclusionClause}
161164
ORDER BY d.name
162165
OPTION(RECOMPILE);";
163166

@@ -173,6 +176,8 @@ ORDER BY d.name
173176
using var sqlConnection = await CreateConnectionAsync(server, cancellationToken);
174177
using var command = new SqlCommand(query, sqlConnection);
175178
command.CommandTimeout = CommandTimeoutSeconds;
179+
var (_, dbConfigExclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
180+
foreach (var p in dbConfigExclusionParams) command.Parameters.Add(p);
176181

177182
using var reader = await command.ExecuteReaderAsync(cancellationToken);
178183
while (await reader.ReadAsync(cancellationToken))
@@ -329,7 +334,7 @@ private async Task<int> CollectDatabaseScopedConfigAsync(ServerConnection server
329334
var serverStatus = _serverManager.GetConnectionStatus(server.Id);
330335
bool isAzureSqlDb = serverStatus?.SqlEngineEdition == 5;
331336

332-
const string onPremDbQuery = @"
337+
string onPremDbQuery = @"
333338
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
334339
335340
SELECT
@@ -347,10 +352,11 @@ AND d.name <> N'PerformanceMonitor'
347352
drs.database_id IS NULL /*not in any AG*/
348353
OR drs.is_primary_replica = 1 /*primary replica*/
349354
)
355+
/*EXCLUSION_FILTER*/
350356
ORDER BY d.name
351357
OPTION(RECOMPILE);";
352358

353-
const string azureDbQuery = @"
359+
string azureDbQuery = @"
354360
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
355361
356362
SELECT
@@ -360,9 +366,14 @@ FROM sys.databases AS d
360366
AND d.database_id < 32761
361367
AND d.name <> N'PerformanceMonitor'
362368
AND d.state_desc = N'ONLINE'
369+
/*EXCLUSION_FILTER*/
363370
ORDER BY d.name
364371
OPTION(RECOMPILE);";
365372

373+
var (scopedExclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
374+
onPremDbQuery = onPremDbQuery.Replace("/*EXCLUSION_FILTER*/", scopedExclusionClause);
375+
azureDbQuery = azureDbQuery.Replace("/*EXCLUSION_FILTER*/", scopedExclusionClause);
376+
366377
string dbQuery = isAzureSqlDb ? azureDbQuery : onPremDbQuery;
367378

368379
var serverId = GetServerId(server);
@@ -379,6 +390,8 @@ ORDER BY d.name
379390
using (var dbCommand = new SqlCommand(dbQuery, sqlConnection))
380391
{
381392
dbCommand.CommandTimeout = CommandTimeoutSeconds;
393+
var (_, scopedExclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
394+
foreach (var p in scopedExclusionParams) dbCommand.Parameters.Add(p);
382395
using var dbReader = await dbCommand.ExecuteReaderAsync(cancellationToken);
383396
while (await dbReader.ReadAsync(cancellationToken))
384397
{

Lite/Services/RemoteCollectorService.WaitingTasks.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public partial class RemoteCollectorService
2424
/// </summary>
2525
private async Task<int> CollectWaitingTasksAsync(ServerConnection server, CancellationToken cancellationToken)
2626
{
27-
const string query = @"
27+
string query = @"
2828
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
2929
3030
SELECT /* PerformanceMonitorLite */
@@ -42,8 +42,12 @@ LEFT JOIN sys.databases AS d
4242
AND wt.session_id <> @@SPID
4343
AND wt.wait_type IS NOT NULL
4444
AND er.database_id <> ISNULL(DB_ID(N'PerformanceMonitor'), 0)
45+
/*EXCLUSION_FILTER*/
4546
OPTION(RECOMPILE);";
4647

48+
var (exclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
49+
query = query.Replace("/*EXCLUSION_FILTER*/", exclusionClause);
50+
4751
var serverId = GetServerId(server);
4852
var collectionTime = DateTime.UtcNow;
4953
var rowsCollected = 0;
@@ -54,6 +58,8 @@ AND wt.wait_type IS NOT NULL
5458
using var sqlConnection = await CreateConnectionAsync(server, cancellationToken);
5559
using var command = new SqlCommand(query, sqlConnection);
5660
command.CommandTimeout = CommandTimeoutSeconds;
61+
var (_, exclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
62+
foreach (var p in exclusionParams) command.Parameters.Add(p);
5763

5864
using var reader = await command.ExecuteReaderAsync(cancellationToken);
5965
sqlSw.Stop();

0 commit comments

Comments
 (0)