using System.Globalization; using SQLInsight.Core.Data; using SQLInsight.Core.Instances; namespace SQLInsight.Collectors.Instance; /// /// 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 sp_configure 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. /// public sealed class InstanceConfigurationCollector : Collector { // 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"; /// Whether this build has the instant_file_initialization_enabled column. 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 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, }; }