sqlinsight-pro/src/SQLInsight.Web/Components/Pages/Instance.razor
Søren Eggert Lundsteen Olsen ac13e3d826 Disk speed per drive: IOPS, MB/s and latency
- file-io records each file's drive: sys.dm_os_volume_stats from 2008 R2 SP1
  (new capability VolumeStats, mount points included), otherwise derived
  from the path (D:\..., \server\share\, / on Linux), so 2005 works too
- migration 0007: Volume and IntervalSeconds on FileIoSamples, and
  DiskHourly (rolled up by maintenance, kept HourlyDays)
- DiskActivity sums files per drive: read/write IOPS, MB/s and latency
  weighted by I/O count
- instance page: "Diskhastighed pr. drev" (table and MB/s + latency charts);
  time window analysis and its PDF: per drive against the same hours of the
  four previous weeks
- SQLI-IO-001/002 now report per drive (one finding when the storage is
  slow, the slowest files in the evidence) instead of per file

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

290 lines
12 KiB
Text
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.

@page "/instanser/{Name}"
@inject InsightRepository Repository
@inject Display Display
@if (_instance is null)
{
<PageTitle>Ukendt instans · SQLInsight Pro</PageTitle>
<h1>Ukendt instans</h1>
<p>Der findes ingen instans med navnet <strong>@Name</strong>.</p>
return;
}
<PageTitle>@_instance.Name · SQLInsight Pro</PageTitle>
<p class="crumbs"><a href="/">Instanser</a> / @_instance.Name</p>
<h1>@_instance.Name</h1>
<p class="lead">
@_instance.ServerName · @Display.Release(_instance.ProductVersion, _instance.EngineEdition) (@_instance.ProductVersion @_instance.ProductLevel @_instance.ProductUpdateLevel) · @_instance.Edition
· sidst set @Display.Ago(_instance.LastSeenAt)
</p>
@if (_instance.IsFailing)
{
<div class="alert">
<strong>Seneste forsøg på at nå instansen fejlede</strong> (@Display.Time(_instance.LastErrorAt)):
<pre>@_instance.LastError</pre>
</div>
}
@if (_instance.UsedLegacyFallback)
{
<div class="alert warn">Forbindelsen bruger legacy ODBC-laget, fordi instansen er for gammel til SqlClient. Krypteringen kan være svagere.</div>
}
<section class="card">
<div class="card-head"><h2>Aktive fund</h2><span class="muted">@_findings.Count fund</span></div>
<FindingList Findings="_findings" Empty="Ingen aktive fund." />
</section>
<div class="section-head">
<h2 class="section">Belastning, seneste 24 timer</h2>
<a class="button" href="@AnalysisUrl(null)">Analysér et tidsrum</a>
</div>
<div class="grid2">
<div class="panel"><Sparkline Points="_cpuSql" Label="SQL Server CPU" Unit=" %" Max="100" /></div>
<div class="panel"><Sparkline Points="_cpuOther" Label="CPU brugt af andre processer" Unit=" %" Max="100" Empty="kan ikke måles på Linux, eller endnu ikke nok målinger." /></div>
<div class="panel"><Sparkline Points="_batches" Label="Batch requests pr. sekund" Decimals="1" /></div>
<div class="panel"><Sparkline Points="_ple" Label="Page life expectancy (s)" /></div>
</div>
<section class="card section">
<div class="card-head"><h2>Største ventetider, seneste 24 timer</h2><span class="muted">uden harmløse baggrundsventetider</span></div>
@if (_waits.Count == 0)
{
<p class="muted">Endnu ikke nok målinger (ventetider kræver to målinger).</p>
}
else
{
var total = Math.Max(1, _waits.Sum(w => w.WaitTimeMs));
<table>
<thead><tr><th>Wait type</th><th>Kategori</th><th class="num">Ventetid</th><th class="num">Andel</th><th class="num">Gns. pr. wait</th><th class="num">Signal</th></tr></thead>
<tbody>
@foreach (var w in _waits)
{
<tr>
<td class="mono">@w.WaitType</td>
<td>@w.Category</td>
<td class="num">@Display.Number(w.WaitTimeMs / 1000.0, 1) s</td>
<td class="num"><meter min="0" max="1" value="@(((double)w.WaitTimeMs / total).ToString("0.###", System.Globalization.CultureInfo.InvariantCulture))"></meter> @Display.Number(100.0 * w.WaitTimeMs / total) %</td>
<td class="num">@Display.Number(w.WaitingTasks == 0 ? 0 : (double)w.WaitTimeMs / w.WaitingTasks, 1) ms</td>
<td class="num">@Display.Number(w.WaitTimeMs == 0 ? 0 : 100.0 * w.SignalWaitTimeMs / w.WaitTimeMs) %</td>
</tr>
}
</tbody>
</table>
}
</section>
<section class="card">
<div class="card-head"><h2>Diskhastighed pr. drev, seneste 24 timer</h2></div>
<DiskTable Disks="_disks" Empty="Endnu ingen målinger pr. drev (kræver to målinger efter opgradering til 1.0.5)." />
@if (_diskSeries.Count > 0)
{
<div class="grid2">
@foreach (var s in _diskSeries.Take(6))
{
<div class="panel"><Sparkline Points="s.MbPerSecond" Label="@($"{s.Volume} MB/s")" Decimals="1" /></div>
<div class="panel"><Sparkline Points="s.LatencyMs" Label="@($"{s.Volume} gns. latens (ms)")" Decimals="1" /></div>
}
</div>
}
</section>
<section class="card">
<div class="card-head"><h2>I/O-latens pr. fil, seneste 24 timer</h2></div>
@if (_files.Count == 0)
{
<p class="muted">Endnu ikke nok målinger.</p>
}
else
{
<table>
<thead><tr><th>Database</th><th>Fil</th><th>Type</th><th class="num">Læsninger</th><th class="num">Gns. læs</th><th class="num">Skrivninger</th><th class="num">Gns. skriv</th></tr></thead>
<tbody>
@foreach (var f in _files.Take(25))
{
<tr>
<td>@f.DatabaseName</td>
<td>@f.FileId</td>
<td>@f.FileType</td>
<td class="num">@Display.Number(f.Reads)</td>
<td class="num @LatencyClass(f.AverageReadMs, f.FileType == "LOG" ? 20 : 20)">@Ms(f.AverageReadMs)</td>
<td class="num">@Display.Number(f.Writes)</td>
<td class="num @LatencyClass(f.AverageWriteMs, f.FileType == "LOG" ? 5 : 20)">@Ms(f.AverageWriteMs)</td>
</tr>
}
</tbody>
</table>
}
</section>
<div class="grid2">
<section class="card">
<div class="card-head"><h2>Blocking, seneste 24 timer</h2></div>
@if (_blocking.Count == 0)
{
<p class="muted">Ingen blocking over 5 sekunder.</p>
}
else
{
<table>
<thead><tr><th>Tidspunkt</th><th>Head blocker</th><th class="num">Blokerede</th><th class="num">Længste</th></tr></thead>
<tbody>
@foreach (var b in _blocking.Take(20))
{
var head = b.Sessions.FirstOrDefault();
<tr>
<td><a href="@AnalysisUrl(b.SampledAt)" title="Analysér tidsrummet omkring">@Display.Time(b.SampledAt)</a></td>
<td>@b.HeadSessionId <span class="muted">@head?.Status @head?.ProgramName</span></td>
<td class="num">@b.BlockedCount</td>
<td class="num">@Display.Number(b.MaxWaitMs / 1000.0) s</td>
</tr>
}
</tbody>
</table>
}
</section>
<section class="card">
<div class="card-head"><h2>Deadlocks, seneste 7 dage</h2></div>
@if (_deadlocks.Count == 0)
{
<p class="muted">Ingen deadlocks registreret.</p>
}
else
{
<table>
<thead><tr><th>Tidspunkt</th><th>Offer</th><th>Graf</th></tr></thead>
<tbody>
@foreach (var d in _deadlocks.Take(20))
{
var victim = d.Processes.FirstOrDefault(p => p.IsVictim);
<tr>
<td><a href="@AnalysisUrl(d.OccurredAt)" title="Analysér tidsrummet omkring">@Display.Time(d.OccurredAt)</a></td>
<td>session @victim?.SessionId <span class="muted">@victim?.ClientApp</span></td>
<td><a href="/deadlocks/@(d.DeadlockId).xdl">.xdl</a></td>
</tr>
}
</tbody>
</table>
}
</section>
</div>
<section class="card section">
<div class="card-head"><h2>Ikke målbart på denne instans</h2></div>
@if (_notMeasurable.Count == 0)
{
<p class="muted">Alle regler, der er relevante for instansen, kunne evalueres.</p>
}
else
{
<p class="muted">Disse regler kunne ikke vurderes. Det er ikke det samme som "OK".</p>
<table>
<thead><tr><th>Regel</th><th></th><th>Årsag</th></tr></thead>
<tbody>
@foreach (var r in _notMeasurable)
{
<tr><td class="mono">@r.RuleId</td><td>@r.RuleTitle</td><td>@r.Reason</td></tr>
}
</tbody>
</table>
}
@if (_notApplicable.Count > 0)
{
<details class="not-applicable">
<summary>Ikke relevant for @Display.Release(_instance.ProductVersion, _instance.EngineEdition) (@_notApplicable.Count)</summary>
<p class="muted">Regler om noget, denne SQL Server-version ikke har (fx Query Store eller system_health). Det er ikke et hul i overvågningen.</p>
<table>
<thead><tr><th>Regel</th><th></th><th>Hvorfor</th></tr></thead>
<tbody>
@foreach (var r in _notApplicable)
{
<tr><td class="mono">@r.RuleId</td><td>@r.RuleTitle</td><td>@r.Reason</td></tr>
}
</tbody>
</table>
</details>
}
</section>
<section class="card">
<div class="card-head"><h2>Løst de seneste 30 dage</h2></div>
@if (_resolved.Count == 0)
{
<p class="muted">Ingen.</p>
}
else
{
<table>
<thead><tr><th>Fund</th><th>Aktivt fra</th><th>Løst</th></tr></thead>
<tbody>
@foreach (var f in _resolved)
{
<tr><td><a href="/fund/@f.FindingId">@f.Title</a></td><td>@Display.Time(f.FirstSeenAt)</td><td>@Display.Time(f.ResolvedAt)</td></tr>
}
</tbody>
</table>
}
</section>
@code {
[Parameter] public string Name { get; set; } = string.Empty;
private InstanceOverview? _instance;
private IReadOnlyList<StoredFinding> _findings = [];
private IReadOnlyList<StoredFinding> _resolved = [];
private IReadOnlyList<StoredRuleState> _notMeasurable = [];
private IReadOnlyList<StoredRuleState> _notApplicable = [];
private IReadOnlyList<MetricPoint> _cpuSql = [], _cpuOther = [], _batches = [], _ple = [];
private IReadOnlyList<WaitTotal> _waits = [];
private IReadOnlyList<FileLatency> _files = [];
private IReadOnlyList<SQLInsight.Reports.DiskComparison> _disks = [];
private IReadOnlyList<DiskSeries> _diskSeries = [];
private IReadOnlyList<StoredBlocking> _blocking = [];
private IReadOnlyList<StoredDeadlock> _deadlocks = [];
protected override async Task OnInitializedAsync()
{
_instance = await Repository.GetInstanceAsync(Name);
if (_instance is null)
{
return;
}
var id = _instance.InstanceId;
var day = DateTimeOffset.UtcNow.AddDays(-1);
_findings = await Repository.GetActiveFindingsAsync(id);
_resolved = await Repository.GetRecentlyResolvedAsync(id, 30);
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();
_cpuSql = await Repository.GetMetricAsync(id, Metrics.CpuSqlServer, day);
_cpuOther = await Repository.GetMetricAsync(id, Metrics.CpuOther, day);
_batches = await Repository.GetMetricAsync(id, Metrics.BatchRequestsPerSecond, day);
_ple = await Repository.GetMetricAsync(id, Metrics.PageLifeExpectancy, day);
_waits = await Repository.GetTopWaitsAsync(id, day, 12);
_files = await Repository.GetFileLatencyAsync(id, day);
_disks = (await Repository.GetDiskActivityAsync(id, day, DateTimeOffset.UtcNow)).Select(d => new SQLInsight.Reports.DiskComparison(d, null)).ToList();
_diskSeries = await Repository.GetDiskSeriesAsync(id, day);
_blocking = await Repository.GetBlockingAsync(id, day);
_deadlocks = await Repository.GetDeadlocksAsync(id, DateTimeOffset.UtcNow.AddDays(-7));
}
private static string Ms(double? value) => value is { } v ? $"{Display.Number(v, 1)} ms" : "–";
/// <summary>The time window analysis: the last hour, or half an hour on each side of an event.</summary>
private string AnalysisUrl(DateTime? around)
{
var url = "/instanser/" + Uri.EscapeDataString(_instance!.Name) + "/analyse";
if (around is not { } at)
{
return url;
}
var (from, to) = SQLInsight.Reports.TimeWindow.Around(at);
return url + "?fra=" + Uri.EscapeDataString(SQLInsight.Reports.TimeWindow.ToInput(from, Display.Zone))
+ "&til=" + Uri.EscapeDataString(SQLInsight.Reports.TimeWindow.ToInput(to, Display.Zone));
}
private static string LatencyClass(double? value, double threshold) => value >= threshold ? "bad" : string.Empty;
}