- Long operations: a one-minute collector reads operations with progress from sys.dm_exec_requests (BACKUP, RESTORE, DBCC, REORGANIZE, SHRINK, ROLLBACK): command, database, start, percent, expected end, login. Stored in insight.LongOperations (migration 0011, RawDays), shown on the instance page, in the time window analysis and its PDF. The last 10 durations per command and database survive restarts; SQLI-OPS-001 flags a backup/restore running over twice the median. - Full-text community rules COM-FT-001..005 (metadata only, 2005 syntax): disk-full or paused/throttled catalogs, stale manual/off change tracking indexes and disabled indexes. - Statement activity (CPU time / time in plan cache): column in top SQL and SQLI-CPU-002 when above SQLInsight:TopSql:ActivityThresholdPercent (default 50) for at least an hour. Never clears the plan cache. - BuiltInCollectors.Create takes a CollectorSettings record. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
68 lines
3.5 KiB
C#
68 lines
3.5 KiB
C#
using System.Globalization;
|
|
using System.Text.Json;
|
|
using Dapper;
|
|
using Microsoft.Data.SqlClient;
|
|
using SQLInsight.Core.Data;
|
|
|
|
namespace SQLInsight.Repository;
|
|
|
|
/// <summary>A long operation as stored; <see cref="LoginName"/> is sensitive (local UI only).</summary>
|
|
public sealed record StoredLongOperation(int SessionId, DateTime StartedAt, string Command, string? DatabaseName, string? LoginName,
|
|
double PercentComplete, DateTime? EstimatedEnd, DateTime LastSeenAt)
|
|
{
|
|
/// <summary>Still running at the latest sample (seen within the last two minutes).</summary>
|
|
public bool IsRunning(DateTime utcNow) => utcNow - LastSeenAt < TimeSpan.FromMinutes(2);
|
|
|
|
public TimeSpan Duration => LastSeenAt - StartedAt;
|
|
}
|
|
|
|
public sealed partial class InsightRepository
|
|
{
|
|
private static async Task SaveLongOperationsAsync(SqlConnection connection, int instanceId, LongOperationSnapshot snapshot)
|
|
{
|
|
if (snapshot.Running.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await connection.ExecuteAsync("""
|
|
MERGE insight.LongOperations AS t
|
|
USING (SELECT * FROM OPENJSON(@json) WITH (SessionId int, StartedAt datetime2(0), Command nvarchar(32), DatabaseName nvarchar(128),
|
|
LoginName nvarchar(128), PercentComplete float, EstimatedEnd datetime2(0), LastSeenAt datetime2(0))) AS s
|
|
ON t.InstanceId = @instanceId AND t.StartedAt = s.StartedAt AND t.SessionId = s.SessionId AND t.Command = s.Command
|
|
WHEN MATCHED THEN UPDATE SET PercentComplete = s.PercentComplete, EstimatedEnd = s.EstimatedEnd, LastSeenAt = s.LastSeenAt
|
|
WHEN NOT MATCHED THEN INSERT (InstanceId, SessionId, StartedAt, Command, DatabaseName, LoginName, PercentComplete, EstimatedEnd, LastSeenAt)
|
|
VALUES (@instanceId, s.SessionId, s.StartedAt, s.Command, s.DatabaseName, s.LoginName, s.PercentComplete, s.EstimatedEnd, s.LastSeenAt);
|
|
""", new
|
|
{
|
|
instanceId,
|
|
json = JsonSerializer.Serialize(snapshot.Running.Select(o => new
|
|
{
|
|
o.SessionId,
|
|
StartedAt = Iso(o.StartedAt),
|
|
Command = o.Command.Length <= 32 ? o.Command : o.Command[..32],
|
|
o.DatabaseName,
|
|
o.LoginName,
|
|
o.PercentComplete,
|
|
EstimatedEnd = o.EstimatedEnd is { } end ? Iso(end) : null,
|
|
LastSeenAt = Iso(o.LastSeenAt),
|
|
}), Json),
|
|
}).ConfigureAwait(false);
|
|
|
|
static string Iso(DateTimeOffset value) => Utc(value).ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
/// <summary>Long operations that ran at some point in [from, to], newest first.</summary>
|
|
public async Task<IReadOnlyList<StoredLongOperation>> GetLongOperationsAsync(int instanceId, DateTimeOffset from, DateTimeOffset to, int top = 200,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await using var connection = await OpenAsync(cancellationToken).ConfigureAwait(false);
|
|
var rows = await connection.QueryAsync<StoredLongOperation>("""
|
|
SELECT TOP (@top) SessionId, StartedAt, Command, DatabaseName, LoginName, PercentComplete, EstimatedEnd, LastSeenAt
|
|
FROM insight.LongOperations
|
|
WHERE InstanceId = @instanceId AND StartedAt <= @to AND LastSeenAt >= @from
|
|
ORDER BY StartedAt DESC;
|
|
""", new { instanceId, from = from.UtcDateTime, to = to.UtcDateTime, top }).ConfigureAwait(false);
|
|
return rows.ToList();
|
|
}
|
|
}
|