Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Waiting to run
- ASP.NET Core Blazor static SSR: overview with KPI tiles, instance page (findings, CPU/batch/PLE trends, top waits, file latency, blocking, deadlocks with .xdl download, not-measurable rules, resolved), finding detail with explanation, evidence, fix script and references - Light-first design (sidebar, cards, self-hosted Inter, teal accent), optional dark theme via toggle; strict CSP, no inline styles - Windows authentication (Negotiate/IIS) with optional group restriction; refuses to run without authentication outside Development - Hosting: IIS (in-process ASP.NET Core Module), Windows service with Kestrel, or systemd; web reads the repository with db_datareader only - docs/installation.md for repository, permissions, agent and web - Fix: CPU used by other processes is not derived on Linux (SystemIdle is unreliable there); deadlocks are not re-reported or stored twice after an agent restart; Dapper sends datetime2 so time comparisons are exact Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
114 lines
4.9 KiB
C#
114 lines
4.9 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using SQLInsight.Agent;
|
|
using SQLInsight.Agent.Monitoring;
|
|
using SQLInsight.Collectors;
|
|
using SQLInsight.Connectivity;
|
|
using SQLInsight.Core.Findings;
|
|
using SQLInsight.Repository;
|
|
using SQLInsight.Rules;
|
|
using SQLInsight.Rules.Engine;
|
|
|
|
namespace SQLInsight.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// The agent's real pipeline end to end: least-privilege connection, all collectors, all rules, repository.
|
|
/// </summary>
|
|
[Collection(SqlServerTestGroup.Name)]
|
|
[Trait("Category", "Integration")]
|
|
public class AgentPipelineTests(SqlServerFixture sql) : IAsyncLifetime
|
|
{
|
|
private readonly string _database = "SQLInsightAgent_" + Guid.NewGuid().ToString("N")[..8];
|
|
private InsightRepository _repository = null!;
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
await sql.ExecuteAsSaAsync($"CREATE DATABASE [{_database}];");
|
|
_repository = new InsightRepository(sql.DatabaseConnectionString(_database));
|
|
await _repository.MigrateAsync();
|
|
}
|
|
|
|
public Task DisposeAsync() => sql.ExecuteAsSaAsync($"ALTER DATABASE [{_database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_database}];");
|
|
|
|
private InstanceMonitor Monitor(string name, string? password = null) => new(
|
|
new MonitoredInstance
|
|
{
|
|
Name = name,
|
|
Server = sql.Server,
|
|
Authentication = AuthenticationMode.SqlLogin,
|
|
UserName = SqlServerFixture.AgentLogin,
|
|
Password = password ?? SqlServerFixture.AgentPassword,
|
|
Provider = ConnectionProvider.SqlClient,
|
|
TrustServerCertificate = true,
|
|
},
|
|
BuiltInCollectors.Create(),
|
|
new Dictionary<string, TimeSpan>(),
|
|
new SqlConnectionFactory(),
|
|
new RuleEngine(BuiltInRules.Create()),
|
|
_repository,
|
|
NullLogger.Instance);
|
|
|
|
[Fact]
|
|
public async Task Two_cycles_store_findings_rule_states_and_time_series()
|
|
{
|
|
var monitor = Monitor("container");
|
|
var start = DateTimeOffset.UtcNow;
|
|
|
|
var first = await monitor.RunCycleAsync(start, CancellationToken.None);
|
|
await sql.ExecuteAsSaAsync("SELECT COUNT(*) FROM sys.objects a CROSS JOIN sys.objects b;");
|
|
var nothingDue = await monitor.RunCycleAsync(start.AddSeconds(10), CancellationToken.None);
|
|
var second = await monitor.RunCycleAsync(start.AddMinutes(16), CancellationToken.None);
|
|
|
|
Assert.NotNull(first);
|
|
Assert.Null(nothingDue);
|
|
Assert.NotNull(second);
|
|
Assert.Equal(BuiltInRules.Create().Count, second.Count);
|
|
Assert.DoesNotContain(second, r => r.Outcome == RuleOutcome.Error);
|
|
|
|
var instance = await _repository.GetInstanceAsync("container");
|
|
Assert.NotNull(instance);
|
|
Assert.False(instance.IsFailing);
|
|
|
|
var findings = await _repository.GetActiveFindingsAsync(instance.InstanceId);
|
|
// A fresh container has well-known problems: default max server memory and master never backed up.
|
|
Assert.Contains(findings, f => f.RuleId == "SQLI-CFG-001" && f.Severity == Severity.High);
|
|
Assert.Contains(findings, f => f.RuleId == "SQLI-DB-007" && f.Subject == "database: master");
|
|
|
|
var states = await _repository.GetRuleStatesAsync(instance.InstanceId);
|
|
Assert.Equal(BuiltInRules.Create().Count, states.Count);
|
|
|
|
// Wait stats need two samples; after the second cycle they are measurable.
|
|
Assert.NotEqual(RuleOutcome.NotMeasurable, states.Single(s => s.RuleId == "SQLI-WAIT-003").Outcome);
|
|
Assert.NotEmpty(await _repository.GetTopWaitsAsync(instance.InstanceId, start.AddHours(-1)));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Restarted_agent_does_not_report_or_store_deadlocks_again()
|
|
{
|
|
var now = DateTimeOffset.UtcNow;
|
|
await Monitor("restart").RunCycleAsync(now, CancellationToken.None);
|
|
var instance = await _repository.GetInstanceAsync("restart");
|
|
var stored = (await _repository.GetDeadlocksAsync(instance!.InstanceId, now.AddDays(-2))).Count;
|
|
|
|
// A new monitor is what the agent has after a restart: no memory of where it stopped.
|
|
var results = await Monitor("restart").RunCycleAsync(now.AddMinutes(1), CancellationToken.None);
|
|
|
|
Assert.Equal(stored, (await _repository.GetDeadlocksAsync(instance.InstanceId, now.AddDays(-2))).Count);
|
|
if (stored > 0)
|
|
{
|
|
Assert.Equal(RuleOutcome.Passed, results!.Single(r => r.RuleId == "SQLI-DLK-001").Outcome);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Unreachable_instance_is_recorded_as_failing()
|
|
{
|
|
var monitor = Monitor("wrong-password", password: "not-the-password");
|
|
|
|
var result = await monitor.RunCycleAsync(DateTimeOffset.UtcNow, CancellationToken.None);
|
|
|
|
Assert.Null(result);
|
|
var instance = await _repository.GetInstanceAsync("wrong-password");
|
|
Assert.True(instance!.IsFailing);
|
|
Assert.Contains("Login failed", instance.LastError, StringComparison.Ordinal);
|
|
}
|
|
}
|