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>
108 lines
6.1 KiB
C#
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,
|
|
};
|
|
}
|