sqlinsight-pro/src/SQLInsight.Core/Data/HighAvailability.cs
Søren Eggert Lundsteen Olsen d27323a1a4 Availability groups: cluster node or witness down (D)
The availability group collector also reads sys.dm_hadr_cluster_members
(empty or unavailable without a WSFC), and SQLI-AG-003 (High) reports a
node or witness that is not up, with how many members are still up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 10:56:43 +02:00

95 lines
3.4 KiB
C#

namespace SQLInsight.Core.Data;
/// <summary>
/// Always On availability group state as seen from this instance. A primary sees all replicas; a secondary only itself.
/// </summary>
public sealed record AvailabilityGroupSnapshot
{
public required DateTimeOffset CapturedAt { get; init; }
public required IReadOnlyList<ReplicaState> Replicas { get; init; }
public required IReadOnlyList<DatabaseReplicaState> Databases { get; init; }
/// <summary>The Windows cluster's nodes and witness (sys.dm_hadr_cluster_members); empty without a WSFC.</summary>
public IReadOnlyList<ClusterMember> ClusterMembers { get; init; } = [];
}
public sealed record ReplicaState
{
public required string Group { get; init; }
public required string Replica { get; init; }
public bool IsLocal { get; init; }
/// <summary>PRIMARY, SECONDARY or RESOLVING; null when this instance cannot see the replica's state.</summary>
public string? Role { get; init; }
public string? ConnectedState { get; init; }
/// <summary>HEALTHY, PARTIALLY_HEALTHY or NOT_HEALTHY.</summary>
public string? SynchronizationHealth { get; init; }
public string? AvailabilityMode { get; init; }
public string? FailoverMode { get; init; }
}
public sealed record DatabaseReplicaState
{
public required string Group { get; init; }
public required string Database { get; init; }
public required string Replica { get; init; }
public bool IsLocal { get; init; }
public bool IsPrimaryReplica { get; init; }
public string? SynchronizationState { get; init; }
public string? SynchronizationHealth { get; init; }
public bool IsSuspended { get; init; }
public string? SuspendReason { get; init; }
/// <summary>Log not yet sent to this secondary, in KB.</summary>
public long? LogSendQueueKb { get; init; }
public long? LogSendRateKbPerSecond { get; init; }
/// <summary>Log received but not yet redone on this secondary, in KB.</summary>
public long? RedoQueueKb { get; init; }
public long? RedoRateKbPerSecond { get; init; }
public DateTime? LastCommitTime { get; init; }
}
/// <summary>
/// SQL Server Agent: whether it runs, and the latest outcome of each job.
/// </summary>
public sealed record AgentJobSnapshot
{
public required DateTimeOffset CapturedAt { get; init; }
/// <summary>False when no SQL Agent session exists (the service is stopped).</summary>
public required bool IsAgentRunning { get; init; }
public required IReadOnlyList<AgentJob> Jobs { get; init; }
}
public sealed record AgentJob
{
public required string Name { get; init; }
public bool IsEnabled { get; init; }
/// <summary>0 failed, 1 succeeded, 2 retry, 3 canceled, 4 in progress; null if never run.</summary>
public int? LastRunStatus { get; init; }
public DateTime? LastRunAt { get; init; }
/// <summary>The message of the most recent failed step, which says why (the job-level message does not).</summary>
public string? LastFailedStepMessage { get; init; }
public string? LastFailedStepName { get; init; }
public int FailuresLastDay { get; init; }
public bool LastRunFailed => LastRunStatus == 0;
}
/// <param name="Type">CLUSTER_NODE, DISK_WITNESS, FILE_SHARE_WITNESS, CLOUD_WITNESS.</param>
/// <param name="State">0 offline, 1 up.</param>
public sealed record ClusterMember(string Name, string Type, int State, string StateText, int? QuorumVotes)
{
public bool IsUp => State == 1;
}