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) <noreply@anthropic.com>
This commit is contained in:
parent
f3551b1bf0
commit
0dc7866808
20 changed files with 137 additions and 11 deletions
|
|
@ -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".
|
||||
|
|
|
|||
|
|
@ -86,6 +86,14 @@ public sealed class InstanceCapabilities
|
|||
public static bool IsInherent(Capability capability) =>
|
||||
MinimumVersion.ContainsKey(capability) || capability is Capability.QueryStatsMemoryColumns or Capability.SelfManagedInfrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// "kræver Query Store (fra SQL Server 2016)" for the missing capabilities, for not-applicable reasons.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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" : ""));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|||
|
||||
/// <summary>The control does not apply to the instance's SQL Server version; counted neither as met nor as not measured.</summary>
|
||||
NotApplicable,
|
||||
|
||||
/// <summary>Applies, but this version cannot measure it: to be checked by hand; not counted as not measured.</summary>
|
||||
ManualCheck,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ public sealed record InstanceOverview
|
|||
public int Info { get; init; }
|
||||
public int NotMeasurable { get; init; }
|
||||
|
||||
/// <summary>Rules that apply but must be checked by hand on this version (RuleOutcome.ManualCheck).</summary>
|
||||
public int ManualChecks { get; init; }
|
||||
|
||||
/// <summary>The most recent contact failed.</summary>
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -96,6 +96,10 @@ public sealed class DeadlockRule : Rule
|
|||
public override string Title => "Deadlocks";
|
||||
public override IReadOnlyCollection<Capability> 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);
|
||||
|
|
|
|||
|
|
@ -250,6 +250,10 @@ public sealed class CheckDbRule : Rule
|
|||
public override string Title => "DBCC CHECKDB er ikke kørt for nylig";
|
||||
public override IReadOnlyCollection<Capability> 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<Capability> 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);
|
||||
|
|
|
|||
|
|
@ -18,5 +18,12 @@ public interface IRule
|
|||
/// </summary>
|
||||
IReadOnlyCollection<Capability> RequiredCapabilities { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
string? ManualCheck => null;
|
||||
|
||||
RuleEvaluation Evaluate(RuleContext context);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ public abstract class Rule : IRule
|
|||
public abstract string Id { get; }
|
||||
public abstract string Title { get; }
|
||||
public virtual IReadOnlyCollection<Capability> RequiredCapabilities => [];
|
||||
public virtual string? ManualCheck => null;
|
||||
|
||||
public abstract RuleEvaluation Evaluate(RuleContext context);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -58,4 +58,11 @@ public enum RuleOutcome
|
|||
/// Last in the enum: outcomes are stored as numbers.
|
||||
/// </summary>
|
||||
NotApplicable,
|
||||
|
||||
/// <summary>
|
||||
/// 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 (<see cref="IRule.ManualCheck"/>). Not a gap in the monitoring's permissions
|
||||
/// or a failure, so not counted as not measurable. Stored as a number: keep last.
|
||||
/// </summary>
|
||||
ManualCheck,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,6 +125,10 @@ public sealed class SpillRule : PlanRule
|
|||
public override string Title => "Sorteringer og hash joins spilder til tempdb";
|
||||
public override IReadOnlyCollection<Capability> 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<Capability> 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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ else
|
|||
<div class="kpi sev-high"><span class="kpi-icon"><Icon Name="alert" /></span><div><strong>@_instances.Sum(i => i.High)</strong><span>høje fund</span></div></div>
|
||||
<div class="kpi sev-info"><span class="kpi-icon"><Icon Name="unplug" /></span><div><strong>@_instances.Count(i => i.IsFailing)</strong><span>kan ikke nås</span></div></div>
|
||||
<div class="kpi sev-info"><span class="kpi-icon"><Icon Name="gauge" /></span><div><strong>@_instances.Sum(i => i.NotMeasurable)</strong><span>regler ikke målbare</span></div></div>
|
||||
<div class="kpi sev-info"><span class="kpi-icon"><Icon Name="list" /></span><div><strong>@_instances.Sum(i => i.ManualChecks)</strong><span>skal tjekkes manuelt</span></div></div>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
|
|
|
|||
|
|
@ -188,6 +188,21 @@ else
|
|||
</div>
|
||||
|
||||
<section class="card section">
|
||||
@if (_manual.Count > 0)
|
||||
{
|
||||
<div class="card-head" id="manuelt"><h2>Skal tjekkes manuelt</h2><span class="muted">@_manual.Count regler</span></div>
|
||||
<p class="muted">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".</p>
|
||||
<table>
|
||||
<thead><tr><th>Regel</th><th></th><th>Sådan tjekkes det</th></tr></thead>
|
||||
<tbody>
|
||||
@foreach (var r in _manual)
|
||||
{
|
||||
<tr><td class="mono">@r.RuleId</td><td>@r.RuleTitle</td><td>@r.Reason</td></tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
<div class="card-head"><h2>Ikke målbart på denne instans</h2></div>
|
||||
@if (_notMeasurable.Count == 0)
|
||||
{
|
||||
|
|
@ -252,6 +267,7 @@ else
|
|||
private IReadOnlyList<StoredFinding> _resolved = [];
|
||||
private IReadOnlyList<StoredRuleState> _notMeasurable = [];
|
||||
private IReadOnlyList<StoredRuleState> _notApplicable = [];
|
||||
private IReadOnlyList<StoredRuleState> _manual = [];
|
||||
private readonly Dictionary<string, IReadOnlyList<MetricPoint>> _metrics = [];
|
||||
private SQLInsight.Reports.MetricContext _context = new();
|
||||
private IReadOnlyList<WaitTotal> _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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue