sqlinsight-pro/src/SQLInsight.Repository/InsightRepository.Operations.cs
Søren Eggert Lundsteen Olsen 384e1f154f Long operations, full-text rules and statement activity
- 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>
2026-10-02 09:07:29 +02:00

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();
}
}