sqlinsight-pro/src/SQLInsight.Rules/Instance/ErrorLogRules.cs
Søren Eggert Lundsteen Olsen e6840e40d7 SQL Server error log without elevated rights (8a)
A five-minute collector reads important error log entries from the best
source the agent's rights allow: the error log itself (xp_readerrorlog,
only with securityadmin), the default trace's ErrorLog and Audit Login
Failed events (optional ALTER TRACE), or the exception and connectivity
ring buffers (VIEW SERVER STATE: numbers and severity, failed logins).
Repetitions are summed per error; the watermark is the newest entry
read, because the default trace is buffered. Stored in
insight.ErrorLogEvents (migration 0013, EventDays), resumed after restart.

Rules SQLI-LOG-001..006: corruption 823/824/825, I/O over 15 s (833),
severe errors (17+), login failure bursts (configurable), stack dumps and
AG errors; rules needing the log text are "not measurable" with the
right to grant. Page /instanser/{navn}/fejllog (menu Overvågning) and
GET /api/v1/instances/{name}/error-log with messages redacted.

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

144 lines
8.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// 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.
/// </summary>
public abstract class ErrorLogRule(ErrorLogCategory category, bool needsText) : Rule
{
public override IReadOnlyCollection<Capability> 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<ErrorLogGroup> Relevant(ErrorLogSnapshot snapshot) => snapshot.Recent.Where(g => g.Category == category);
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
var snapshot = context.Data.Get<ErrorLogSnapshot>();
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.";
/// <summary>Only a burst in the latest interval, not the sum of a whole day.</summary>
protected override IEnumerable<ErrorLogGroup> 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.";
}