sqlinsight-pro/src/SQLInsight.Collectors/Instance/InstanceConfigurationCollector.cs
Søren Eggert Lundsteen Olsen 2284e7f082 SQL Scripts: one reviewable script per rule
A DBA should be able to review every statement SQLInsight sends to a
monitored server before granting it access. "SQL Scripts/<rule-id>.sql"
holds, per rule (82 built-in, 16 community), a Danish header (what and
why, how C# judges the data, thresholds with config keys, severity,
exact GRANTs, versions, collector file and member) and the collector's
SQL verbatim, every version variant as its own batch.
"_collectors/<Collector>.sql" holds each collector's queries once.

The only statement that is not strictly read-only is documented in the
scripts: the deadlock collector's session temp table (#system_health).

Inline SQL in the collectors moved to internal constants and Build*Sql
methods (no text changed), so SqlScriptTests can check that the scripts
still contain the SQL the code sends, that every rule has a script, that
every collector query is registered, and that nothing writes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:20:06 +02:00

108 lines
6.1 KiB
C#

using System.Globalization;
using SQLInsight.Core.Data;
using SQLInsight.Core.Instances;
namespace SQLInsight.Collectors.Instance;
/// <summary>
/// The instance's setup for the configuration page and its change log: collation, services and accounts, Instant File
/// Initialization, Lock Pages in Memory, tempdb files and default paths, plus <c>sp_configure</c> and trace flags for
/// the fingerprint. Changes are computed against the previous collection, which the agent restores from the repository
/// after a restart, so a change made while the agent was down is still reported.
/// </summary>
public sealed class InstanceConfigurationCollector : Collector<InstanceConfigurationSnapshot>
{
// SERVERPROPERTY returns NULL for properties an older version does not know, so this runs everywhere.
internal const string PropertiesSql = """
SELECT CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), CAST(SERVERPROPERTY('ProductLevel') AS nvarchar(128)),
CAST(SERVERPROPERTY('Edition') AS nvarchar(128)), CAST(SERVERPROPERTY('Collation') AS nvarchar(128)),
CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(260)), CAST(SERVERPROPERTY('InstanceDefaultLogPath') AS nvarchar(260))
""";
// Which optional columns this build has: Instant File Initialization and the memory model came in service packs.
internal const string ColumnsSql = """
SELECT COL_LENGTH('sys.dm_server_services', 'instant_file_initialization_enabled'), COL_LENGTH('sys.dm_os_sys_info', 'sql_memory_model_desc')
""";
internal const string TempdbSql = """
SELECT name, type_desc, CAST(size AS bigint) / 128, growth, is_percent_growth, max_size
FROM sys.master_files WHERE database_id = 2 ORDER BY file_id
""";
internal const string MemoryModelSql = "SELECT sql_memory_model_desc FROM sys.dm_os_sys_info";
/// <param name="instantFileInitialization">Whether this build has the instant_file_initialization_enabled column.</param>
internal static string BuildServicesSql(bool instantFileInitialization)
{
var ifi = instantFileInitialization ? "instant_file_initialization_enabled" : "CAST(NULL AS nvarchar(1))";
return $"SELECT servicename, service_account, startup_type_desc, status_desc, {ifi} FROM sys.dm_server_services";
}
public override string Name => "instance-config";
public override TimeSpan DefaultInterval => TimeSpan.FromMinutes(15);
protected override async Task<InstanceConfigurationSnapshot> CollectAsync(CollectorContext context, InstanceConfigurationSnapshot? previous)
{
var ct = context.CancellationToken;
var connection = context.Connection;
var properties = (await Db.QueryAsync(connection, PropertiesSql, r => (Version: r.NullableString(0), Level: r.NullableString(1),
Edition: r.NullableString(2), Collation: r.NullableString(3), DataPath: r.NullableString(4), LogPath: r.NullableString(5)), ct).ConfigureAwait(false)).FirstOrDefault();
var snapshot = new InstanceConfigurationSnapshot
{
CapturedAt = context.Now,
ProductVersion = properties.Version,
ProductLevel = properties.Level,
Edition = properties.Edition,
Collation = properties.Collation,
DefaultDataPath = properties.DataPath,
DefaultLogPath = properties.LogPath,
};
if (context.Instance.Version.Major >= 9)
{
var tempdb = await Db.QueryAsync(connection, TempdbSql, r => new TempdbFile(
r.String(0), r.String(1), r.Long(2), Growth(r.Long(3), r.Bool(4)), MaxSize(r.Long(5))), ct).ConfigureAwait(false);
snapshot = snapshot with { TempdbFiles = tempdb };
}
if (context.Capabilities.Has(Capability.VolumeStats))
{
var columns = (await Db.QueryAsync(connection, ColumnsSql, r => (Ifi: r.NullableInt(0) is not null, Memory: r.NullableInt(1) is not null), ct).ConfigureAwait(false)).FirstOrDefault();
var services = await Db.QueryAsync(connection, BuildServicesSql(columns.Ifi),
r => new ServiceInfo(r.String(0), r.NullableString(1), r.NullableString(2), r.NullableString(3), YesNo(r.NullableString(4))), ct).ConfigureAwait(false);
var memoryModel = columns.Memory
? (await Db.QueryAsync(connection, MemoryModelSql, r => r.NullableString(0), ct).ConfigureAwait(false)).FirstOrDefault()
: null;
snapshot = snapshot with
{
Services = services,
ServicesAvailable = true,
InstantFileInitialization = services.FirstOrDefault(s => s.Name.StartsWith("SQL Server (", StringComparison.OrdinalIgnoreCase))?.InstantFileInitialization,
MemoryModel = memoryModel,
};
}
var options = await Db.QueryAsync(connection, context.Instance.Version.Major >= 9 ? ConfigurationCollector.Sql2005 : ConfigurationCollector.Sql2000,
r => new ConfigurationOption(r.String(0), r.Long(1), r.Long(2)), ct).ConfigureAwait(false);
var flags = await TraceFlagCollector.ReadGlobalFlagsAsync(connection, ct).ConfigureAwait(false);
var items = ConfigurationFingerprint.Items(snapshot, options, flags);
var changes = previous is null ? [] : ConfigurationFingerprint.Diff(previous.Items, items, context.Now);
var recent = (previous?.RecentChanges ?? []).Where(c => context.Now - c.DetectedAt < ConfigurationFingerprint.RecentWindow).Concat(changes).ToList();
return snapshot with { Items = items, Changes = changes, RecentChanges = recent };
}
private static string Growth(long growth, bool percent) =>
growth == 0 ? "ingen" : percent ? $"{growth} %" : $"{(growth / 128).ToString(CultureInfo.InvariantCulture)} MB";
private static string MaxSize(long maxSize) =>
maxSize is -1 or 268435456 ? "ubegrænset" : $"{(maxSize / 128).ToString(CultureInfo.InvariantCulture)} MB";
private static bool? YesNo(string? value) => value switch
{
"Y" or "y" => true,
"N" or "n" => false,
_ => null,
};
}