- 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>
290 lines
12 KiB
Text
290 lines
12 KiB
Text
@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;
|
||
}
|