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.";
}