From 0dc78668086e271ffcb5efecf23c4144a01e71fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Eggert=20Lundsteen=20Olsen?= Date: Fri, 2 Oct 2026 09:59:50 +0200 Subject: [PATCH] Separate "check by hand" from "not relevant" for version-gated rules A rule whose topic applies but whose way of measuring needs a newer version or patch now gives the new outcome ManualCheck with concrete Danish guidance, instead of NotApplicable. It is marked explicitly per rule (IRule.ManualCheck) and only used when every missing capability is a measurement capability (InstanceCapabilities.IsMeasurement): CHECKDB status (SQLI-DB-009), VLFs (SQLI-DB-011), spills and memory grants (SQLI-PLAN-004/005) and deadlocks on 2005 (SQLI-DLK-001). Query Store, AG and Azure-managed topics stay "not relevant". Shown in a visible "Skal tjekkes manuelt" section on the instance page, a KPI on the overview, a "Tjek manuelt" column in the management report's controls, manualChecks in /api/v1/instances and manualCheck in rule-states; not counted as not measurable. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/rapporter.md | 6 ++++++ .../Instances/InstanceCapabilities.cs | 8 +++++++ src/SQLInsight.Reports/ManagementReport.cs | 8 ++++--- src/SQLInsight.Reports/ReportData.cs | 11 +++++++++- .../InsightRepository.Read.cs | 8 +++++-- .../Concurrency/ConcurrencyRules.cs | 4 ++++ .../Databases/DatabaseRules.cs | 8 +++++++ src/SQLInsight.Rules/Engine/IRule.cs | 7 +++++++ src/SQLInsight.Rules/Engine/Rule.cs | 1 + src/SQLInsight.Rules/Engine/RuleEngine.cs | 14 +++++++++++++ src/SQLInsight.Rules/Engine/RuleEvaluation.cs | 7 +++++++ src/SQLInsight.Rules/Performance/PlanRules.cs | 8 +++++++ src/SQLInsight.Web/Api/ApiV1Endpoints.cs | 1 + .../Api/IntegrationEndpoints.cs | 1 + .../Components/Pages/Home.razor | 1 + .../Components/Pages/Instance.razor | 17 +++++++++++++++ .../ComplianceControlTests.cs | 3 +++ .../WebApiTests.cs | 21 ++++++++++++++++++- .../DatabaseRuleTests.cs | 10 +++++++-- tests/SQLInsight.Rules.Tests/PlanRuleTests.cs | 4 ++-- 20 files changed, 137 insertions(+), 11 deletions(-) diff --git a/docs/rapporter.md b/docs/rapporter.md index 3e441c1..fbe0bb2 100644 --- a/docs/rapporter.md +++ b/docs/rapporter.md @@ -49,3 +49,9 @@ Agenten kan sende sidste måneds rapport automatisk. Den bruger SMTP-indstilling ``` Rapporten dækker den foregående kalendermåned og sendes på den angivne dag og time. Var agenten stoppet på det tidspunkt, sendes den, så snart den kører igen i samme måned. Den sendes kun én gang pr. måned. + +## Ikke relevant, ikke målt og tjek manuelt + +- **Ikke relevant**: funktionen findes ikke i SQL Server-versionen (fx Query Store før 2016, availability groups før 2012) eller håndteres af Azure. Tælles ikke med. +- **Ikke målt**: rettigheder eller en fejl forhindrede målingen. Det er et hul i overvågningen og tælles som "ikke målbar". +- **Tjek manuelt**: emnet er relevant, men målemetoden kræver en nyere version eller patch (fx status for DBCC CHECKDB før SQL Server 2016 SP2, VLF-antal, spill og memory grants før 2016 SP2/2017 CU3, deadlocks på 2005). Reglen beskriver, hvordan det tjekkes i hånden. Det vises i sektionen **Skal tjekkes manuelt** på instansens side, i nøgletallet "skal tjekkes manuelt" på overblikket, i kontrollerne i driftsrapporten og som `manualCheck` i API'et – og tæller ikke som "ikke målbar". diff --git a/src/SQLInsight.Core/Instances/InstanceCapabilities.cs b/src/SQLInsight.Core/Instances/InstanceCapabilities.cs index 959a46f..5afdbf6 100644 --- a/src/SQLInsight.Core/Instances/InstanceCapabilities.cs +++ b/src/SQLInsight.Core/Instances/InstanceCapabilities.cs @@ -86,6 +86,14 @@ public sealed class InstanceCapabilities public static bool IsInherent(Capability capability) => MinimumVersion.ContainsKey(capability) || capability is Capability.QueryStatsMemoryColumns or Capability.SelfManagedInfrastructure; + /// + /// Capabilities that are a way of measuring, not a feature: without them the topic still applies on the version and + /// can be checked by hand (see IRule.ManualCheck), unlike Query Store or availability groups, which simply are not there. + /// + public static bool IsMeasurement(Capability capability) => capability is Capability.LastGoodCheckDbTime or Capability.LogInfoDmv + or Capability.QueryStatsMemoryColumns or Capability.SystemHealthSession or Capability.SystemHealthFileTarget or Capability.ExtendedEvents + or Capability.SessionWaitStats or Capability.LastActualPlan or Capability.TextQueryPlan or Capability.VolumeStats; + /// /// "kræver Query Store (fra SQL Server 2016)" for the missing capabilities, for not-applicable reasons. /// diff --git a/src/SQLInsight.Reports/ManagementReport.cs b/src/SQLInsight.Reports/ManagementReport.cs index 8b52d93..b1ed2e2 100644 --- a/src/SQLInsight.Reports/ManagementReport.cs +++ b/src/SQLInsight.Reports/ManagementReport.cs @@ -209,10 +209,10 @@ public static class ManagementReport private static void Controls(Section section, ReportData data) { section.AddParagraph("Kontroller", StyleNames.Heading1); - var intro = section.AddParagraph("Opfyldt betyder, at kontrollen er målt og intet fund er registreret. Ikke målt betyder, at rettighederne eller en fejl forhindrede målingen; det er ikke det samme som opfyldt. Instanser, hvor kontrollen ikke er relevant for SQL Server-versionen, tælles ikke med."); + var intro = section.AddParagraph("Opfyldt betyder, at kontrollen er målt og intet fund er registreret. Ikke målt betyder, at rettighederne eller en fejl forhindrede målingen; det er ikke det samme som opfyldt. Tjek manuelt betyder, at kontrollen er relevant, men SQL Server-versionen ikke kan måle den; den skal tjekkes i hånden (se instansens side). Instanser, hvor kontrollen ikke er relevant for SQL Server-versionen, tælles ikke med."); intro.Style = "Muted"; - var table = Table(section, ("Kontrol", 5.2), ("Opfyldt", 1.8), ("Ikke opfyldt", 2.1), ("Ikke målt", 1.9), ("Instanser, der ikke opfylder", 6)); + var table = Table(section, ("Kontrol", 4.6), ("Opfyldt", 1.6), ("Ikke opfyldt", 1.9), ("Ikke målt", 1.7), ("Tjek manuelt", 1.9), ("Instanser, der ikke opfylder", 5.3)); foreach (var control in ComplianceControl.All) { var statuses = data.Instances.Select(i => (i.Name, Status: control.StatusOn(i.Name, data.RuleStates))).ToList(); @@ -223,7 +223,9 @@ public static class ManagementReport Number(row.Cells[1], statuses.Count(s => s.Status == ControlStatus.Met), Good); Number(row.Cells[2], failing.Count, failing.Count > 0 ? Critical : Muted); Number(row.Cells[3], statuses.Count(s => s.Status == ControlStatus.NotMeasured), Muted); - row.Cells[4].AddParagraph(failing.Count == 0 ? "–" : string.Join(", ", failing.Take(12)) + (failing.Count > 12 ? $" og {failing.Count - 12} flere" : "")); + var manual = statuses.Count(s => s.Status == ControlStatus.ManualCheck); + Number(row.Cells[4], manual, manual > 0 ? Medium : Muted); + row.Cells[5].AddParagraph(failing.Count == 0 ? "–" : string.Join(", ", failing.Take(12)) + (failing.Count > 12 ? $" og {failing.Count - 12} flere" : "")); } } diff --git a/src/SQLInsight.Reports/ReportData.cs b/src/SQLInsight.Reports/ReportData.cs index fed4a12..2ccc65e 100644 --- a/src/SQLInsight.Reports/ReportData.cs +++ b/src/SQLInsight.Reports/ReportData.cs @@ -83,7 +83,13 @@ public sealed record ComplianceControl(string Name, string Description, params s return ControlStatus.NotMet; } - // The core rule does not apply to this version (an integrity check that needs 2016 SP2 on 2005 …): neither met nor a gap. + // The core rule applies but cannot be measured on this version (CHECKDB status before 2016 SP2 …): check by hand. + if (relevant.Any(s => s.RuleId == Rules[0] && s.Outcome == RuleOutcome.ManualCheck)) + { + return ControlStatus.ManualCheck; + } + + // The core rule does not apply to this version: neither met nor a gap. if (relevant.Any(s => s.RuleId == Rules[0] && s.Outcome == RuleOutcome.NotApplicable)) { return ControlStatus.NotApplicable; @@ -103,4 +109,7 @@ public enum ControlStatus /// The control does not apply to the instance's SQL Server version; counted neither as met nor as not measured. NotApplicable, + + /// Applies, but this version cannot measure it: to be checked by hand; not counted as not measured. + ManualCheck, } diff --git a/src/SQLInsight.Repository/InsightRepository.Read.cs b/src/SQLInsight.Repository/InsightRepository.Read.cs index 9e780ac..1c974b6 100644 --- a/src/SQLInsight.Repository/InsightRepository.Read.cs +++ b/src/SQLInsight.Repository/InsightRepository.Read.cs @@ -28,6 +28,9 @@ public sealed record InstanceOverview public int Info { get; init; } public int NotMeasurable { get; init; } + /// Rules that apply but must be checked by hand on this version (RuleOutcome.ManualCheck). + public int ManualChecks { get; init; } + /// The most recent contact failed. public bool IsFailing => LastErrorAt is { } e && (LastSeenAt is null || e > LastSeenAt); @@ -82,13 +85,14 @@ public sealed partial class InsightRepository SELECT i.InstanceId, i.Name, i.ServerName, i.ProductVersion, i.ProductLevel, i.ProductUpdateLevel, i.Edition, i.EngineEdition, i.ConnectionProvider, i.UsedLegacyFallback, i.LastSeenAt, i.LastErrorAt, i.LastError, ISNULL(f.Critical, 0) AS Critical, ISNULL(f.High, 0) AS High, ISNULL(f.Medium, 0) AS Medium, ISNULL(f.Low, 0) AS Low, ISNULL(f.Info, 0) AS Info, - ISNULL(r.NotMeasurable, 0) AS NotMeasurable + ISNULL(r.NotMeasurable, 0) AS NotMeasurable, ISNULL(r.ManualChecks, 0) AS ManualChecks FROM insight.Instances i OUTER APPLY (SELECT SUM(CASE WHEN Severity = 4 THEN 1 ELSE 0 END) AS Critical, SUM(CASE WHEN Severity = 3 THEN 1 ELSE 0 END) AS High, SUM(CASE WHEN Severity = 2 THEN 1 ELSE 0 END) AS Medium, SUM(CASE WHEN Severity = 1 THEN 1 ELSE 0 END) AS Low, SUM(CASE WHEN Severity = 0 THEN 1 ELSE 0 END) AS Info FROM insight.Findings WHERE InstanceId = i.InstanceId AND ResolvedAt IS NULL) f - OUTER APPLY (SELECT COUNT(*) AS NotMeasurable FROM insight.RuleStates WHERE InstanceId = i.InstanceId AND Outcome IN (2, 3)) r + OUTER APPLY (SELECT SUM(CASE WHEN Outcome IN (2, 3) THEN 1 ELSE 0 END) AS NotMeasurable, SUM(CASE WHEN Outcome = 5 THEN 1 ELSE 0 END) AS ManualChecks + FROM insight.RuleStates WHERE InstanceId = i.InstanceId) r ORDER BY i.Name; """).ConfigureAwait(false); return rows.ToList(); diff --git a/src/SQLInsight.Rules/Concurrency/ConcurrencyRules.cs b/src/SQLInsight.Rules/Concurrency/ConcurrencyRules.cs index a81c248..8ad1f7e 100644 --- a/src/SQLInsight.Rules/Concurrency/ConcurrencyRules.cs +++ b/src/SQLInsight.Rules/Concurrency/ConcurrencyRules.cs @@ -96,6 +96,10 @@ public sealed class DeadlockRule : Rule public override string Title => "Deadlocks"; public override IReadOnlyCollection RequiredCapabilities => [Capability.SystemHealthSession]; + public override string ManualCheck => + "SQL Server 2005 har ingen system_health-session. Slå trace flag 1222 til (DBCC TRACEON (1222, -1) og -T1222 i opstartsparametrene), " + + "så skrives hver deadlock i SQL Servers fejllog, og gennemgå fejlloggen jævnligt."; + public override RuleEvaluation Evaluate(RuleContext context) { ArgumentNullException.ThrowIfNull(context); diff --git a/src/SQLInsight.Rules/Databases/DatabaseRules.cs b/src/SQLInsight.Rules/Databases/DatabaseRules.cs index 1cc2a18..43d8745 100644 --- a/src/SQLInsight.Rules/Databases/DatabaseRules.cs +++ b/src/SQLInsight.Rules/Databases/DatabaseRules.cs @@ -250,6 +250,10 @@ public sealed class CheckDbRule : Rule public override string Title => "DBCC CHECKDB er ikke kørt for nylig"; public override IReadOnlyCollection RequiredCapabilities => [Capability.SelfManagedInfrastructure, Capability.LastGoodCheckDbTime]; + public override string ManualCheck => + "Se seneste gode DBCC CHECKDB pr. database: kør som sysadmin DBCC DBINFO ('databasenavn') WITH TABLERESULTS og find feltet dbi_dbccLastKnownGood, " + + "eller se historikken for vedligeholdelsesjobbet med CHECKDB i SQL Server Agent (msdb.dbo.sysjobhistory). Opgradér til SQL Server 2016 SP2 eller nyere, så måles det automatisk."; + public override RuleEvaluation Evaluate(RuleContext context) { ArgumentNullException.ThrowIfNull(context); @@ -323,6 +327,10 @@ public sealed class VirtualLogFileRule : Rule public override string Title => "Transaktionsloggen har for mange VLF'er"; public override IReadOnlyCollection RequiredCapabilities => [Capability.SelfManagedInfrastructure, Capability.LogInfoDmv]; + public override string ManualCheck => + "Tæl VLF'er pr. database: kør DBCC LOGINFO i databasen (én række pr. VLF; kræver sysadmin). Over ca. 1.000 bør loggen krympes og vokse igen i store, faste trin. " + + "Fra SQL Server 2016 SP2 måles det automatisk."; + public override RuleEvaluation Evaluate(RuleContext context) { ArgumentNullException.ThrowIfNull(context); diff --git a/src/SQLInsight.Rules/Engine/IRule.cs b/src/SQLInsight.Rules/Engine/IRule.cs index 1c634cc..4decd2f 100644 --- a/src/SQLInsight.Rules/Engine/IRule.cs +++ b/src/SQLInsight.Rules/Engine/IRule.cs @@ -18,5 +18,12 @@ public interface IRule /// IReadOnlyCollection RequiredCapabilities { get; } + /// + /// How to check by hand what the rule measures, when the measurement needs a newer version or patch than the + /// instance has (missing capabilities that are about the measurement, not the feature). Null: the rule is simply + /// not relevant on such a version. + /// + string? ManualCheck => null; + RuleEvaluation Evaluate(RuleContext context); } diff --git a/src/SQLInsight.Rules/Engine/Rule.cs b/src/SQLInsight.Rules/Engine/Rule.cs index 1d1b4cc..f800687 100644 --- a/src/SQLInsight.Rules/Engine/Rule.cs +++ b/src/SQLInsight.Rules/Engine/Rule.cs @@ -11,6 +11,7 @@ public abstract class Rule : IRule public abstract string Id { get; } public abstract string Title { get; } public virtual IReadOnlyCollection RequiredCapabilities => []; + public virtual string? ManualCheck => null; public abstract RuleEvaluation Evaluate(RuleContext context); diff --git a/src/SQLInsight.Rules/Engine/RuleEngine.cs b/src/SQLInsight.Rules/Engine/RuleEngine.cs index 9290d02..0b42f29 100644 --- a/src/SQLInsight.Rules/Engine/RuleEngine.cs +++ b/src/SQLInsight.Rules/Engine/RuleEngine.cs @@ -70,6 +70,20 @@ public sealed class RuleEngine // Missing because of what the instance is (its version, or Azure) is not a gap in the monitoring. var inherent = missing.All(InstanceCapabilities.IsInherent); var release = context.Instance.Release.DisplayName(); + + // The topic applies, only the way of measuring it needs a newer version: someone has to check by hand. + if (inherent && rule.ManualCheck is { } manual && missing.All(InstanceCapabilities.IsMeasurement)) + { + return new RuleResult + { + RuleId = rule.Id, + RuleTitle = rule.Title, + Outcome = RuleOutcome.ManualCheck, + MissingCapabilities = missing, + Reason = $"Kan ikke måles på {release} – tjek manuelt (målingen kræver {InstanceCapabilities.Describe(missing)}). {manual}", + }; + } + return new RuleResult { RuleId = rule.Id, diff --git a/src/SQLInsight.Rules/Engine/RuleEvaluation.cs b/src/SQLInsight.Rules/Engine/RuleEvaluation.cs index 03861ad..232b594 100644 --- a/src/SQLInsight.Rules/Engine/RuleEvaluation.cs +++ b/src/SQLInsight.Rules/Engine/RuleEvaluation.cs @@ -58,4 +58,11 @@ public enum RuleOutcome /// Last in the enum: outcomes are stored as numbers. /// NotApplicable, + + /// + /// The topic applies, but the way the rule measures it needs a newer version or patch (CHECKDB status before 2016 + /// SP2 …): someone has to check it by hand (). Not a gap in the monitoring's permissions + /// or a failure, so not counted as not measurable. Stored as a number: keep last. + /// + ManualCheck, } diff --git a/src/SQLInsight.Rules/Performance/PlanRules.cs b/src/SQLInsight.Rules/Performance/PlanRules.cs index 471ae9e..eb914cc 100644 --- a/src/SQLInsight.Rules/Performance/PlanRules.cs +++ b/src/SQLInsight.Rules/Performance/PlanRules.cs @@ -125,6 +125,10 @@ public sealed class SpillRule : PlanRule public override string Title => "Sorteringer og hash joins spilder til tempdb"; public override IReadOnlyCollection RequiredCapabilities => [Capability.QueryStatsMemoryColumns]; + public override string ManualCheck => + "Se efter Sort Warnings og Hash Warnings i de faktiske eksekveringsplaner for de tunge forespørgsler (SSMS: Include Actual Execution Plan), " + + "eller lyt efter hændelserne Sort Warnings/Hash Warning med SQL Profiler i en kort periode. Fra SQL Server 2016 SP2/2017 CU3 måles det automatisk."; + protected override Finding? Check(RuleContext context, AnalyzedStatement s) { if (s.TotalSpills is not { } spills || s.Executions == 0 || (double)spills / s.Executions < MinSpillPagesPerExecution) @@ -156,6 +160,10 @@ public sealed class ExcessiveGrantRule : PlanRule public override string Title => "Forespørgsel får langt mere hukommelse, end den bruger"; public override IReadOnlyCollection RequiredCapabilities => [Capability.QueryStatsMemoryColumns]; + public override string ManualCheck => + "Se de aktuelle memory grants under belastning: SELECT * FROM sys.dm_exec_query_memory_grants (requested_memory_kb mod used_memory_kb og max_used_memory_kb). " + + "Fra SQL Server 2016 SP2/2017 CU3 måles det automatisk."; + protected override Finding? Check(RuleContext context, AnalyzedStatement s) { if (s.TotalGrantKb is not { } granted || s.TotalUsedGrantKb is not { } used || s.Executions == 0) diff --git a/src/SQLInsight.Web/Api/ApiV1Endpoints.cs b/src/SQLInsight.Web/Api/ApiV1Endpoints.cs index 6319e5d..7cbf694 100644 --- a/src/SQLInsight.Web/Api/ApiV1Endpoints.cs +++ b/src/SQLInsight.Web/Api/ApiV1Endpoints.cs @@ -193,6 +193,7 @@ public static class ApiV1Endpoints Rules.Engine.RuleOutcome.Findings => "findings", Rules.Engine.RuleOutcome.NotMeasurable => "notMeasurable", Rules.Engine.RuleOutcome.NotApplicable => "notApplicable", + Rules.Engine.RuleOutcome.ManualCheck => "manualCheck", _ => "error", }, s.Reason, diff --git a/src/SQLInsight.Web/Api/IntegrationEndpoints.cs b/src/SQLInsight.Web/Api/IntegrationEndpoints.cs index c113c68..f5c3281 100644 --- a/src/SQLInsight.Web/Api/IntegrationEndpoints.cs +++ b/src/SQLInsight.Web/Api/IntegrationEndpoints.cs @@ -30,6 +30,7 @@ public static class IntegrationEndpoints i.LastError, Findings = new { i.Critical, i.High, i.Medium, i.Low, i.Info }, i.NotMeasurable, + i.ManualChecks, })) .WithSummary("Instanser med status og antal fund / Instances"); diff --git a/src/SQLInsight.Web/Components/Pages/Home.razor b/src/SQLInsight.Web/Components/Pages/Home.razor index 7bcda5d..4c3838b 100644 --- a/src/SQLInsight.Web/Components/Pages/Home.razor +++ b/src/SQLInsight.Web/Components/Pages/Home.razor @@ -23,6 +23,7 @@ else
@_instances.Sum(i => i.High)høje fund
@_instances.Count(i => i.IsFailing)kan ikke nås
@_instances.Sum(i => i.NotMeasurable)regler ikke målbare
+
@_instances.Sum(i => i.ManualChecks)skal tjekkes manuelt
diff --git a/src/SQLInsight.Web/Components/Pages/Instance.razor b/src/SQLInsight.Web/Components/Pages/Instance.razor index 29b4104..6138a48 100644 --- a/src/SQLInsight.Web/Components/Pages/Instance.razor +++ b/src/SQLInsight.Web/Components/Pages/Instance.razor @@ -188,6 +188,21 @@ else
+@if (_manual.Count > 0) +{ +

Skal tjekkes manuelt

@_manual.Count regler
+

Emnet er relevant, men denne SQL Server-version kan ikke måle det, som SQLInsight Pro gør. Tjek det selv som beskrevet; det tæller ikke som "ikke målbar".

+ + + + @foreach (var r in _manual) + { + + } + +
RegelSådan tjekkes det
@r.RuleId@r.RuleTitle@r.Reason
+} +

Ikke målbart på denne instans

@if (_notMeasurable.Count == 0) { @@ -252,6 +267,7 @@ else private IReadOnlyList _resolved = []; private IReadOnlyList _notMeasurable = []; private IReadOnlyList _notApplicable = []; + private IReadOnlyList _manual = []; private readonly Dictionary> _metrics = []; private SQLInsight.Reports.MetricContext _context = new(); private IReadOnlyList _waits = []; @@ -277,6 +293,7 @@ else var states = await Repository.GetRuleStatesAsync(id); _notMeasurable = states.Where(r => r.Outcome is RuleOutcome.NotMeasurable or RuleOutcome.Error).ToList(); _notApplicable = states.Where(r => r.Outcome == RuleOutcome.NotApplicable).ToList(); + _manual = states.Where(r => r.Outcome == RuleOutcome.ManualCheck).ToList(); foreach (var definition in SQLInsight.Reports.TimeWindowAnalysis.MetricDefinitions) { _metrics[definition.Metric] = await Repository.GetMetricAsync(id, definition.Metric, day); diff --git a/tests/SQLInsight.Agent.Tests/ComplianceControlTests.cs b/tests/SQLInsight.Agent.Tests/ComplianceControlTests.cs index 028b39a..0be184b 100644 --- a/tests/SQLInsight.Agent.Tests/ComplianceControlTests.cs +++ b/tests/SQLInsight.Agent.Tests/ComplianceControlTests.cs @@ -18,11 +18,14 @@ public class ComplianceControlTests new InstanceRuleState("SQL01", "SQLI-DB-009", RuleOutcome.NotMeasurable, "Adgang nægtet"), new InstanceRuleState("SQL02", "SQLI-DB-009", RuleOutcome.Passed, null), new InstanceRuleState("SQL03", "SQLI-DB-010", RuleOutcome.Findings, null), + new InstanceRuleState("SQL2014", "SQLI-DB-009", RuleOutcome.ManualCheck, "Kan ikke måles på SQL Server 2014 – tjek manuelt"), + new InstanceRuleState("SQL2014", "SQLI-DB-010", RuleOutcome.Passed, null), }; Assert.Equal(ControlStatus.NotApplicable, Integrity.StatusOn("KGAA-S05", states)); Assert.Equal(ControlStatus.NotMeasured, Integrity.StatusOn("SQL01", states)); // a real gap stays a gap Assert.Equal(ControlStatus.Met, Integrity.StatusOn("SQL02", states)); Assert.Equal(ControlStatus.NotMet, Integrity.StatusOn("SQL03", states)); // a finding always counts + Assert.Equal(ControlStatus.ManualCheck, Integrity.StatusOn("SQL2014", states)); // relevant, but checked by hand } } diff --git a/tests/SQLInsight.IntegrationTests/WebApiTests.cs b/tests/SQLInsight.IntegrationTests/WebApiTests.cs index cb8e1f4..e7ef14a 100644 --- a/tests/SQLInsight.IntegrationTests/WebApiTests.cs +++ b/tests/SQLInsight.IntegrationTests/WebApiTests.cs @@ -167,7 +167,8 @@ public class ApiV1Tests(SqlServerFixture sql) : IAsyncLifetime }).ToArray(); await _repository.SaveRuleResultsAsync(_instanceId, [new RuleResult { RuleId = "SQLI-DB-007", RuleTitle = "Fuld backup", Outcome = RuleOutcome.Findings, Findings = findings }, - new RuleResult { RuleId = "LOCAL-AX-001", RuleTitle = "Lokal regel", Outcome = RuleOutcome.Passed }], now); + new RuleResult { RuleId = "LOCAL-AX-001", RuleTitle = "Lokal regel", Outcome = RuleOutcome.Passed }, + new RuleResult { RuleId = "SQLI-DB-009", RuleTitle = "CHECKDB", Outcome = RuleOutcome.ManualCheck, Reason = "Kan ikke måles på SQL Server 2014 – tjek manuelt. Se dbi_dbccLastKnownGood." }], now); await _repository.SaveCollectedAsync(_instanceId, "wait-sampling", WaitSampling.Aggregate( [new WaitingRequest(60, "LCK_M_U", 3000, "AXDB", "0xAB", "UPDATE NUMBERSEQUENCETABLE SET NEXTREC = 1", 55)], now.AddMinutes(-1))); @@ -281,6 +282,24 @@ public class ApiV1Tests(SqlServerFixture sql) : IAsyncLifetime Assert.Equal(1, all.GetProperty("instances").GetArrayLength()); } + + [Fact] + public async Task Manual_checks_are_their_own_outcome_on_pages_and_in_the_api() + { + var client = _factory.CreateClient(); + var states = await GetJsonAsync($"/api/v1/instances/{Uri.EscapeDataString(Name)}/rule-states"); + var instances = await GetJsonAsync("/api/v1/instances"); + var page = await client.GetStringAsync($"/instanser/{Uri.EscapeDataString(Name)}"); + var home = await client.GetStringAsync("/"); + + Assert.Contains(states.EnumerateArray(), s => s.GetProperty("ruleId").GetString() == "SQLI-DB-009" && s.GetProperty("outcome").GetString() == "manualCheck"); + var instance = Assert.Single(instances.EnumerateArray()); + Assert.Equal(1, instance.GetProperty("manualChecks").GetInt32()); + Assert.Equal(0, instance.GetProperty("notMeasurable").GetInt32()); + Assert.Contains("Skal tjekkes manuelt", page, StringComparison.Ordinal); + Assert.Contains("dbi_dbccLastKnownGood", page, StringComparison.Ordinal); + Assert.Contains("skal tjekkes manuelt", home, StringComparison.Ordinal); + } [Fact] public async Task Openapi_describes_every_api_v1_endpoint() { diff --git a/tests/SQLInsight.Rules.Tests/DatabaseRuleTests.cs b/tests/SQLInsight.Rules.Tests/DatabaseRuleTests.cs index d8de50e..1313d6d 100644 --- a/tests/SQLInsight.Rules.Tests/DatabaseRuleTests.cs +++ b/tests/SQLInsight.Rules.Tests/DatabaseRuleTests.cs @@ -147,13 +147,19 @@ public class DatabaseRuleTests } [Fact] - public void CheckDb_is_not_applicable_before_2016_sp2() + public void CheckDb_before_2016_sp2_must_be_checked_by_hand() { var result = Run(new CheckDbRule(), WithVersion("13.0.4001.0", DatabaseList(Database()))); - Assert.Equal(RuleOutcome.NotApplicable, result.Outcome); + Assert.Equal(RuleOutcome.ManualCheck, result.Outcome); + Assert.Contains("tjek manuelt", result.Reason, StringComparison.Ordinal); + Assert.Contains("dbi_dbccLastKnownGood", result.Reason, StringComparison.Ordinal); } + [Fact] + public void Query_store_before_2016_is_not_relevant_rather_than_a_manual_check() => + Assert.Equal(RuleOutcome.NotApplicable, Run(new QueryStoreStateRule(), WithVersion("12.0.6024.0", DatabaseList(Database()))).Outcome); + [Fact] public void Suspect_pages_ignore_repaired_pages() { diff --git a/tests/SQLInsight.Rules.Tests/PlanRuleTests.cs b/tests/SQLInsight.Rules.Tests/PlanRuleTests.cs index 0dbda6f..4a51ff8 100644 --- a/tests/SQLInsight.Rules.Tests/PlanRuleTests.cs +++ b/tests/SQLInsight.Rules.Tests/PlanRuleTests.cs @@ -86,9 +86,9 @@ public class PlanRuleTests } [Fact] - public void Memory_rules_are_not_applicable_without_the_columns() + public void Memory_rules_must_be_checked_by_hand_without_the_columns() { - Assert.Equal(RuleOutcome.NotApplicable, Run(new SpillRule(), WithVersion("14.0.1000.169", new PlanAnalysisSnapshot { CapturedAt = Now, Statements = [] })).Outcome); + Assert.Equal(RuleOutcome.ManualCheck, Run(new SpillRule(), WithVersion("14.0.1000.169", new PlanAnalysisSnapshot { CapturedAt = Now, Statements = [] })).Outcome); } [Fact]