Axapta 3.0 and AX 4.0 store batch and cleanup times as a local date plus seconds. They were turned into UTC with the SQL Server's offset at collection time, so every time from the other side of a DST change was an hour off (a job planned 01:50 CET on the spring night looked 70 minutes late at 03:00 CEST), and a SQL Server running UTC behind a CET AOS shifted every time. AxLocalTime converts with the rules of the AX time zone on that date: SQLInsight:DynamicsAx:LocalTimeZone (Windows or IANA id), else the agent's own zone when its offset matches the SQL Server's, else the server's offset (to the nearest quarter hour) with a Danish data note. The skipped spring hour and the repeated autumn hour get standard time (deterministic, never throws, never a false "late"); seconds outside 0..86400 are unknown and 86400 is midnight. The AX 3.0 cleanup time is local by version, also when LATESTCLEANTIME is missing. AX 2009+ UTC times are untouched. Also: remaining numbers cannot overflow for HIGHEST = Int64.MaxValue, and run keys use the invariant culture. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
209 lines
9.2 KiB
C#
209 lines
9.2 KiB
C#
using SQLInsight.Connectivity;
|
|
using SQLInsight.Repository;
|
|
|
|
namespace SQLInsight.Agent;
|
|
|
|
public sealed class AgentOptions
|
|
{
|
|
public const string SectionName = "SQLInsight";
|
|
|
|
/// <summary>How often the scheduler checks whether collectors are due.</summary>
|
|
public TimeSpan Tick { get; set; } = TimeSpan.FromSeconds(15);
|
|
|
|
/// <summary>How many instances are checked at the same time.</summary>
|
|
public int MaxParallelInstances { get; set; } = 4;
|
|
|
|
/// <summary>Overrides of collector intervals by collector name, e.g. <c>"blocking": "00:00:30"</c>.</summary>
|
|
public IDictionary<string, TimeSpan> CollectorIntervals { get; } = new Dictionary<string, TimeSpan>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
public RepositoryOptions Repository { get; set; } = new();
|
|
|
|
public RetentionOptions Retention { get; set; } = new();
|
|
|
|
public PrivilegeOptions Privileges { get; set; } = new();
|
|
|
|
public TopSqlOptions TopSql { get; set; } = new();
|
|
|
|
public ErrorLogOptions ErrorLog { get; set; } = new();
|
|
|
|
public ActivityOptions Activity { get; set; } = new();
|
|
|
|
public TempdbOptions Tempdb { get; set; } = new();
|
|
|
|
public DynamicsAxOptions DynamicsAx { get; set; } = new();
|
|
|
|
public IList<MonitoredInstance> Instances { get; } = [];
|
|
}
|
|
|
|
public sealed class RepositoryOptions
|
|
{
|
|
/// <summary>
|
|
/// Connection string to the SQLInsight repository database. Use a SQL Server 2016+ instance other than the
|
|
/// ones being monitored.
|
|
/// </summary>
|
|
public string? ConnectionString { get; set; }
|
|
}
|
|
|
|
public sealed class MonitoredInstance
|
|
{
|
|
/// <summary>Stable name shown in the UI and used as the key in the repository and SQLInsight Hub.</summary>
|
|
public string Name { get; set; } = string.Empty;
|
|
|
|
public string Server { get; set; } = string.Empty;
|
|
public AuthenticationMode Authentication { get; set; } = AuthenticationMode.Windows;
|
|
public string? UserName { get; set; }
|
|
public string? Password { get; set; }
|
|
public ConnectionProvider Provider { get; set; } = ConnectionProvider.Auto;
|
|
public EncryptionMode Encryption { get; set; } = EncryptionMode.Mandatory;
|
|
public bool TrustServerCertificate { get; set; }
|
|
public string OdbcDriver { get; set; } = "ODBC Driver 18 for SQL Server";
|
|
|
|
/// <summary>Entra tenant id, for EntraServicePrincipal.</summary>
|
|
public string? TenantId { get; set; }
|
|
|
|
public ConnectionTarget ToTarget() => new()
|
|
{
|
|
Server = Server,
|
|
Authentication = Authentication,
|
|
UserName = UserName,
|
|
Password = Password,
|
|
Provider = Provider,
|
|
Encryption = Encryption,
|
|
TrustServerCertificate = TrustServerCertificate,
|
|
OdbcDriver = OdbcDriver,
|
|
TenantId = TenantId,
|
|
};
|
|
}
|
|
|
|
/// <summary>The privilege audit (sysadmin and the other elevated server rights).</summary>
|
|
public sealed class PrivilegeOptions
|
|
{
|
|
/// <summary>More people and accounts with sysadmin than this is a finding (SQLI-SEC-002).</summary>
|
|
public int MaxSysadminAccounts { get; set; } = 3;
|
|
|
|
/// <summary>Unfold Windows groups with server rights to the people in them, by asking Active Directory (Windows only).</summary>
|
|
public bool ExpandWindowsGroups { get; set; } = true;
|
|
}
|
|
|
|
/// <summary>Top SQL (the heaviest statements).</summary>
|
|
public sealed class TopSqlOptions
|
|
{
|
|
/// <summary>
|
|
/// A statement whose CPU time is more than this share of the time it has been in the plan cache runs almost
|
|
/// constantly (SQLI-PERF-…); 100 means one CPU core all the time.
|
|
/// </summary>
|
|
public double ActivityThresholdPercent { get; set; } = 50;
|
|
}
|
|
|
|
/// <summary>The SQL Server error log.</summary>
|
|
public sealed class ErrorLogOptions
|
|
{
|
|
/// <summary>Failed logins within one collection (5 minutes) before it is a finding (SQLI-LOG-004).</summary>
|
|
public int LoginFailureBurst { get; set; } = 20;
|
|
}
|
|
|
|
/// <summary>Open transactions and long-running statements.</summary>
|
|
public sealed class ActivityOptions
|
|
{
|
|
/// <summary>A transaction open longer than this (minutes) is a finding (SQLI-TRAN-001); six times as long is High.</summary>
|
|
public int TransactionMinutes { get; set; } = 10;
|
|
|
|
/// <summary>A transaction holding more transaction log than this (MB) is a finding (SQLI-TRAN-002).</summary>
|
|
public int TransactionLogMegabytes { get; set; } = 1024;
|
|
|
|
/// <summary>A user statement running longer than this (minutes) is a finding (SQLI-REQ-001).</summary>
|
|
public int RequestMinutes { get; set; } = 15;
|
|
}
|
|
|
|
/// <summary>tempdb and the version store.</summary>
|
|
public sealed class TempdbOptions
|
|
{
|
|
/// <summary>The version store above this (MB) is a finding (SQLI-TEMP-001).</summary>
|
|
public int VersionStoreMegabytes { get; set; } = 1024;
|
|
|
|
/// <summary>A session using more tempdb than this (MB) for 15 minutes is a finding (SQLI-TEMP-003).</summary>
|
|
public int SessionMegabytes { get; set; } = 1024;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Dynamics AX checks (SQLInsight:DynamicsAx). They only run in databases recognised as AX and chosen for monitoring;
|
|
/// the data checks need the optional read access (db/permissions/grant-dynamics-ax-read.sql).
|
|
/// </summary>
|
|
public sealed class DynamicsAxOptions
|
|
{
|
|
/// <summary>A waiting batch job this many minutes past its planned start is late (SQLI-AX-003).</summary>
|
|
public int BatchLateMinutes { get; set; } = 30;
|
|
|
|
/// <summary>Late by this many hours: in practice never started (High).</summary>
|
|
public int BatchNeverStartedHours { get; set; } = 24;
|
|
|
|
/// <summary>A batch job executing this many hours is stuck (SQLI-AX-004).</summary>
|
|
public int BatchStuckHours { get; set; } = 6;
|
|
|
|
/// <summary>Running this many times longer than its own 90th percentile is unusually long (SQLI-AX-002).</summary>
|
|
public double BatchLongRunningFactor { get; set; } = 2.0;
|
|
|
|
/// <summary>…but never for runs shorter than this (minutes).</summary>
|
|
public int BatchLongRunningMinimumMinutes { get; set; } = 10;
|
|
|
|
/// <summary>Earlier runs needed before a job is compared with its own history.</summary>
|
|
public int BatchHistoryMinimumRuns { get; set; } = 5;
|
|
|
|
/// <summary>A run that failed within this many hours is reported (SQLI-AX-001).</summary>
|
|
public int BatchFailedLookbackHours { get; set; } = 24;
|
|
|
|
/// <summary>A number sequence used more than this (percent) is a finding (SQLI-AX-005).</summary>
|
|
public double NumberSequenceWarnPercent { get; set; } = 80;
|
|
|
|
/// <summary>…and High from this.</summary>
|
|
public double NumberSequenceCriticalPercent { get; set; } = 95;
|
|
|
|
/// <summary>Forecast to run out within this many days is a finding (number sequences and 64-bit RecId).</summary>
|
|
public int ForecastWarnDays { get; set; } = 90;
|
|
|
|
/// <summary>…and High within this many days.</summary>
|
|
public int ForecastCriticalDays { get; set; } = 30;
|
|
|
|
/// <summary>AX 3.0's 32-bit RecId: a finding from this share of the int range (SQLI-AX-009).</summary>
|
|
public double RecId32WarnPercent { get; set; } = 50;
|
|
|
|
/// <summary>…and Critical from this.</summary>
|
|
public double RecId32CriticalPercent { get; set; } = 75;
|
|
|
|
/// <summary>AX 3.0: forecast to run out within this many days is a finding, Critical within half.</summary>
|
|
public int RecId32ForecastDays { get; set; } = 730;
|
|
|
|
/// <summary>64-bit RecId (or a lowered MAXVAL): a finding from this share.</summary>
|
|
public double RecId64WarnPercent { get; set; } = 80;
|
|
|
|
/// <summary>An AX session blocking others this many seconds is reported with its AX user (SQLI-AX-010).</summary>
|
|
public int BlockingSeconds { get; set; } = 30;
|
|
|
|
/// <summary>How many days back the forecasts look.</summary>
|
|
public int ForecastBasisDays { get; set; } = 30;
|
|
|
|
/// <summary>The AOS's time zone (Windows or IANA id) for Axapta 3.0/4.0's local times; empty: the agent's own zone when it matches the SQL Server.</summary>
|
|
public string? LocalTimeZone { get; set; }
|
|
|
|
public Core.DynamicsAx.AxThresholds ToThresholds() => new()
|
|
{
|
|
BatchLateMinutes = Math.Max(1, BatchLateMinutes),
|
|
BatchNeverStartedHours = Math.Max(1, BatchNeverStartedHours),
|
|
BatchStuckHours = Math.Max(1, BatchStuckHours),
|
|
BatchLongRunningFactor = Math.Max(1.1, BatchLongRunningFactor),
|
|
BatchLongRunningMinimumMinutes = Math.Max(1, BatchLongRunningMinimumMinutes),
|
|
BatchHistoryMinimumRuns = Math.Clamp(BatchHistoryMinimumRuns, 1, Core.DynamicsAx.AxBatchHistory.RunsKept),
|
|
BatchFailedLookbackHours = Math.Max(1, BatchFailedLookbackHours),
|
|
NumberSequenceWarnPercent = Math.Clamp(NumberSequenceWarnPercent, 1, 100),
|
|
NumberSequenceCriticalPercent = Math.Clamp(NumberSequenceCriticalPercent, 1, 100),
|
|
ForecastWarnDays = Math.Max(1, ForecastWarnDays),
|
|
ForecastCriticalDays = Math.Max(1, ForecastCriticalDays),
|
|
RecId32WarnPercent = Math.Clamp(RecId32WarnPercent, 1, 100),
|
|
RecId32CriticalPercent = Math.Clamp(RecId32CriticalPercent, 1, 100),
|
|
RecId32ForecastDays = Math.Max(1, RecId32ForecastDays),
|
|
RecId64WarnPercent = Math.Clamp(RecId64WarnPercent, 1, 100),
|
|
BlockingSeconds = Math.Max(1, BlockingSeconds),
|
|
ForecastBasisDays = Math.Clamp(ForecastBasisDays, 2, 365),
|
|
LocalTimeZone = string.IsNullOrWhiteSpace(LocalTimeZone) ? null : LocalTimeZone.Trim(),
|
|
};
|
|
}
|