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>
144 lines
8.9 KiB
C#
144 lines
8.9 KiB
C#
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.";
|
||
}
|