using System.Globalization; using SQLInsight.Core.Collection; using SQLInsight.Core.Data; using SQLInsight.Core.Findings; using SQLInsight.Core.Instances; using SQLInsight.Rules.Engine; namespace SQLInsight.Rules.Instance; /// /// Important entries in the SQL Server error log, summed per error number and kept open for a day after they were /// last seen. Messages can name logins and client addresses: they are sensitive evidence. /// public abstract class ErrorLogRule(ErrorLogCategory category, bool needsText) : Rule { public override IReadOnlyCollection RequiredCapabilities => [Capability.DynamicManagementViews]; protected abstract Severity SeverityOf(ErrorLogGroup group); protected abstract string TitleOf(ErrorLogGroup group); protected abstract string Explanation { get; } protected abstract string Recommendation { get; } protected virtual IEnumerable Relevant(ErrorLogSnapshot snapshot) => snapshot.Recent.Where(g => g.Category == category); public override RuleEvaluation Evaluate(RuleContext context) { ArgumentNullException.ThrowIfNull(context); var snapshot = context.Data.Get(); if (needsText && snapshot.Source != ErrorLogSource.ErrorLog) { throw new DataUnavailableException(snapshot.Coverage ?? ErrorLogCollectorText.Coverage); } return Result(Relevant(snapshot).Select(g => CreateFinding( SeverityOf(g), TitleOf(g), Explanation, Recommendation, [ new Evidence("Fejl", g.Error?.ToString(CultureInfo.InvariantCulture) ?? "–"), new Evidence("Alvor (severity)", g.Severity?.ToString(CultureInfo.InvariantCulture) ?? "–"), new Evidence("Antal", g.Count.ToString(CultureInfo.InvariantCulture)), new Evidence("Første", Utc(g.FirstAt)), new Evidence("Seneste", Utc(g.LastAt)), new Evidence("Eksempel", g.Sample ?? "(kilden har ikke teksten)", Sensitive: true), new Evidence("Kilde", snapshot.Source switch { ErrorLogSource.ErrorLog => "fejlloggen", ErrorLogSource.DefaultTrace => "default trace", _ => "exception ring buffer" }), ], subject: $"errorlog: {category}/{g.Error}"))); } private static string Utc(DateTimeOffset value) => value.UtcDateTime.ToString("yyyy-MM-dd HH:mm 'UTC'", CultureInfo.InvariantCulture); } internal static class ErrorLogCollectorText { public const string Coverage = "Fejlloggen kan ikke læses fuldt ud med agentens rettigheder. Giv agenten ALTER TRACE (valgfrit i grant-least-privilege.sql)."; } public sealed class CorruptionErrorRule() : ErrorLogRule(ErrorLogCategory.Corruption, needsText: false) { public override string Id => "SQLI-LOG-001"; public override string Title => "SQL Server har læst beskadigede sider (fejl 823/824/825)"; protected override Severity SeverityOf(ErrorLogGroup group) => group.Error == 825 ? Severity.High : Severity.Critical; protected override string TitleOf(ErrorLogGroup group) => group.Error == 825 ? $"SQL Server måtte læse sider igen for at få dem (fejl 825, {group.Count} gange)" : $"SQL Server har læst beskadigede sider (fejl {group.Error}, {group.Count} gange)"; protected override string Explanation => "823 og 824 betyder, at en side i en database var beskadiget eller ikke kunne læses: data kan være tabt. 825 betyder, at læsningen først lykkedes " + "efter flere forsøg, et tidligt varsel om fejlende storage. Begge peger næsten altid på disk, controller, driver eller SAN."; protected override string Recommendation => "Kør DBCC CHECKDB på databasen nu, og sørg for, at der findes en god backup og log-backups. Tjek Windows' systemlog og storage for fejl. " + "Se msdb.dbo.suspect_pages for de berørte sider. Ret ikke med REPAIR_ALLOW_DATA_LOSS uden at have talt med en DBA."; } public sealed class SlowIoErrorRule() : ErrorLogRule(ErrorLogCategory.SlowIo, needsText: true) { public override string Id => "SQLI-LOG-002"; public override string Title => "I/O-forespørgsler, der tager over 15 sekunder (fejl 833)"; protected override Severity SeverityOf(ErrorLogGroup group) => Severity.High; protected override string TitleOf(ErrorLogGroup group) => $"Storage har brugt over 15 sekunder på I/O ({group.Count} gange)"; protected override string Explanation => "SQL Server skriver fejl 833, når en læsning eller skrivning har ventet over 15 sekunder. Brugerne oplever det som hængende forespørgsler, " + "og det skyldes næsten altid storage: overbelastede diske, SAN, multipathing, en virtuel disk på en travl host eller en backup der kører samtidig."; protected override string Recommendation => "Se hvilken fil og hvilket drev meddelelsen nævner (Diskhastighed pr. drev), og tjek storage og hosten på tidspunktet. Fejlen skal ikke ignoreres."; } public sealed class SevereErrorRule() : ErrorLogRule(ErrorLogCategory.Severe, needsText: false) { public override string Id => "SQLI-LOG-003"; public override string Title => "Alvorlige fejl i SQL Servers fejllog (severity 17 og derover)"; protected override Severity SeverityOf(ErrorLogGroup group) => group.Severity >= 20 ? Severity.High : Severity.Medium; protected override string TitleOf(ErrorLogGroup group) => $"Fejl {group.Error} med severity {group.Severity} ({group.Count} gange)"; protected override string Explanation => "Severity 17–19 er fejl, brugeren ikke selv kan rette (mangel på ressourcer som hukommelse, låse eller plads). Severity 20 og derover er fatale: " + "forbindelsen afbrydes, og ved 22–25 kan databasen eller instansen være berørt."; protected override string Recommendation => "Slå fejlnummeret op (Microsofts liste over databasemotorfejl) og se meddelelsen i fejlloggen. Gentagne fejl af samme slags skal undersøges."; } public sealed class LoginFailureBurstRule() : ErrorLogRule(ErrorLogCategory.LoginFailed, needsText: false) { public override string Id => "SQLI-LOG-004"; public override string Title => "Mange mislykkede logins"; protected override Severity SeverityOf(ErrorLogGroup group) => Severity.Medium; protected override string TitleOf(ErrorLogGroup group) => $"{group.Count} mislykkede logins på kort tid"; protected override string Explanation => "Mange mislykkede logins (fejl 18456) på kort tid er enten en applikation eller tjeneste med et forkert eller udløbet kodeord, eller et forsøg på at gætte kodeord."; protected override string Recommendation => "Se fejlloggen for login og klientadresse (State fortæller hvorfor: 8 = forkert kodeord, 5 = ukendt login, 38 = database findes ikke). " + "Ret applikationens forbindelse, eller blokér adressen, hvis det er et angreb. Overvej Windows-godkendelse og at deaktivere sa."; /// Only a burst in the latest interval, not the sum of a whole day. protected override IEnumerable Relevant(ErrorLogSnapshot snapshot) => snapshot.Groups.Where(g => g.Category == ErrorLogCategory.LoginFailed && g.Count >= snapshot.LoginFailureBurst); } public sealed class StackDumpRule() : ErrorLogRule(ErrorLogCategory.StackDump, needsText: true) { public override string Id => "SQLI-LOG-005"; public override string Title => "SQL Server har lavet et stack dump"; protected override Severity SeverityOf(ErrorLogGroup group) => Severity.High; protected override string TitleOf(ErrorLogGroup group) => $"SQL Server har lavet stack dump ({group.Count} gange)"; protected override string Explanation => "Et stack dump betyder en intern fejl i SQL Server (access violation, assertion, en tråd der ikke svarer). Det kan skyldes en kendt fejl " + "rettet i en senere CU, tredjepartssoftware i processen eller hardware."; protected override string Recommendation => "Find SQLDump*.mdmp/.txt i SQL Servers logmappe og meddelelsen i fejlloggen. Installér seneste CU (se versionsfund), og kontakt Microsoft eller en DBA, hvis det gentager sig."; } public sealed class AvailabilityGroupErrorRule() : ErrorLogRule(ErrorLogCategory.AvailabilityGroup, needsText: false) { public override string Id => "SQLI-LOG-006"; public override string Title => "Fejl i Always On availability groups"; protected override Severity SeverityOf(ErrorLogGroup group) => Severity.High; protected override string TitleOf(ErrorLogGroup group) => $"Availability group-fejl {group.Error} ({group.Count} gange)"; protected override string Explanation => "Fejlloggen viser fejl fra Always On: failover, tabt forbindelse til en replika, lease-timeout eller en database, der ikke synkroniserer."; protected override string Recommendation => "Se AG-fundene og AG-dashboardet i SSMS, Windows-clusterets log og netværket mellem replikaerne på tidspunktet."; }