A one-minute collector reads open user transactions (age, log bytes used
and reserved, database, session, login, program, statement) and user
requests running longer than the threshold, leaving out the agent's own
and system sessions, BACKUP/RESTORE/DBCC (long operations) and requests
waiting on purpose. Transactions open a minute or holding 100 MB are kept
in insight.LongTransactions (migration 0014, RawDays).
Rules SQLI-TRAN-001 (open > 10 min, High at 6x), SQLI-TRAN-002 (> 1 GB
log) and SQLI-REQ-001 (statement > 15 min), configurable under
SQLInsight:Activity. Shown "right now" on the instance page and in the
time window analysis and PDF; GET /api/v1/instances/{name}/activity and
the analysis JSON leave logins and statements out.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
54 lines
2.6 KiB
C#
54 lines
2.6 KiB
C#
using SQLInsight.Core.Data;
|
|
using SQLInsight.Core.Findings;
|
|
using SQLInsight.Rules.Concurrency;
|
|
using SQLInsight.Rules.Engine;
|
|
using static SQLInsight.Rules.Tests.TestInstances;
|
|
|
|
namespace SQLInsight.Rules.Tests;
|
|
|
|
public class ActivityRuleTests
|
|
{
|
|
private static RuleResult Run(IRule rule, RuleContext context) => Assert.Single(new RuleEngine([rule]).Evaluate(context));
|
|
|
|
private static OpenTransaction Tran(double minutes, long logMb = 1) =>
|
|
new(61, 4711, "user_transaction", Now.AddMinutes(-minutes), logMb * 1024 * 1024, 0, "AX", 1, @"KUNDE\anne", "PC01", "Microsoft Dynamics AX", "sleeping", null, "UPDATE INVENTSUM SET …");
|
|
|
|
private static ActivitySnapshot Snapshot(IReadOnlyList<OpenTransaction>? transactions = null, IReadOnlyList<LongRunningRequest>? requests = null) =>
|
|
new() { CapturedAt = Now, Transactions = transactions ?? [], LongRequests = requests ?? [] };
|
|
|
|
[Theory]
|
|
[InlineData(9, null)]
|
|
[InlineData(12, Severity.Medium)]
|
|
[InlineData(61, Severity.High)]
|
|
public void Open_transactions_by_age(double minutes, Severity? expected)
|
|
{
|
|
var result = Run(new LongTransactionRule(), With(Snapshot([Tran(minutes)])));
|
|
|
|
Assert.Equal(expected, result.Findings.SingleOrDefault()?.Severity);
|
|
if (expected is not null)
|
|
{
|
|
var finding = result.Findings.Single();
|
|
Assert.True(finding.Evidence.Single(e => e.Name == "Login").Sensitive);
|
|
Assert.True(finding.Evidence.Single(e => e.Name == "Sætning").Sensitive);
|
|
Assert.DoesNotContain("anne", finding.Title + finding.Subject, StringComparison.Ordinal);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(500, RuleOutcome.Passed)]
|
|
[InlineData(2048, RuleOutcome.Findings)]
|
|
public void Transactions_holding_much_log(long megabytes, RuleOutcome expected) =>
|
|
Assert.Equal(expected, Run(new LargeTransactionRule(), With(Snapshot([Tran(1, megabytes)]))).Outcome);
|
|
|
|
[Fact]
|
|
public void Long_running_statements_are_found_but_not_backups_or_waitfor()
|
|
{
|
|
var select = new LongRunningRequest(70, Now.AddMinutes(-20), 20 * 60_000, "SELECT", "suspended", "LCK_M_S", 1000, "AX", "anne", "PC01", "AX", 61, "SELECT …");
|
|
|
|
Assert.True(ActivityAnalysis.IsStatement(select));
|
|
Assert.False(ActivityAnalysis.IsStatement(select with { Command = "BACKUP DATABASE" }));
|
|
Assert.False(ActivityAnalysis.IsStatement(select with { WaitType = "WAITFOR" }));
|
|
var finding = Assert.Single(Run(new LongRunningStatementRule(), With(Snapshot(requests: [select]))).Findings);
|
|
Assert.Equal("61", finding.Evidence.Single(e => e.Name == "Blokeret af").Value);
|
|
}
|
|
}
|