When the distribution database could not be read, SQL Server's message (which names the agent's login) was appended to Unavailable. That text is the rules' "not measurable" reason and is returned unredacted by /rule-states and /replication. It is now UnavailableDetail: shown on the local instance page and [udeladt] in the API unless ApiIncludesSensitiveEvidence. The agent list is also capped (TOP 500). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
71 lines
3.9 KiB
C#
71 lines
3.9 KiB
C#
using System.Data.Common;
|
|
using SQLInsight.Core.Data;
|
|
using SQLInsight.Core.Instances;
|
|
|
|
namespace SQLInsight.Collectors.Instance;
|
|
|
|
/// <summary>
|
|
/// On a distributor: each distribution agent's latest run (status, latency, message) and replication errors the last
|
|
/// day, as SELECTs in the distribution database. Reading it needs SELECT on those three tables (not on the replicated
|
|
/// commands); without it the snapshot says why. SQL Server 2005+.
|
|
/// </summary>
|
|
public sealed class ReplicationCollector : Collector<ReplicationSnapshot>
|
|
{
|
|
internal const string DistributionDatabaseSql = "SELECT TOP 1 name FROM sys.databases WHERE is_distributor = 1 AND state = 0";
|
|
|
|
internal const string AgentsSql = """
|
|
SELECT TOP 500 a.name, a.publisher_db, h.runstatus, DATEADD(minute, DATEDIFF(minute, GETDATE(), GETUTCDATE()), h.time), h.delivery_latency,
|
|
CAST(h.comments AS nvarchar(500))
|
|
FROM dbo.MSdistribution_agents a
|
|
OUTER APPLY (SELECT TOP 1 x.runstatus, x.time, x.delivery_latency, x.comments FROM dbo.MSdistribution_history x
|
|
WHERE x.agent_id = a.id ORDER BY x.timestamp DESC) h
|
|
""";
|
|
|
|
internal const string ErrorsSql = "SELECT COUNT(*) FROM dbo.MSrepl_errors WHERE time > DATEADD(hour, -24, GETDATE())";
|
|
|
|
public const string NoRightsText =
|
|
"Distributionsdatabasen kan ikke læses med agentens rettigheder. Giv agenten læseadgang til netop agenternes status (ikke de replikerede data): "
|
|
+ "USE distribution; CREATE USER [agentens login]; GRANT SELECT ON dbo.MSdistribution_agents TO [agentens login]; "
|
|
+ "GRANT SELECT ON dbo.MSdistribution_history TO [agentens login]; GRANT SELECT ON dbo.MSrepl_errors TO [agentens login].";
|
|
|
|
public override string Name => "replication";
|
|
public override TimeSpan DefaultInterval => TimeSpan.FromMinutes(15);
|
|
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.DynamicManagementViews];
|
|
|
|
protected override async Task<ReplicationSnapshot> CollectAsync(CollectorContext context, ReplicationSnapshot? previous)
|
|
{
|
|
var ct = context.CancellationToken;
|
|
var connection = context.Connection;
|
|
var distribution = (await Db.QueryAsync(connection, DistributionDatabaseSql, r => r.String(0), ct).ConfigureAwait(false)).FirstOrDefault();
|
|
if (distribution is null)
|
|
{
|
|
return new ReplicationSnapshot { CapturedAt = context.Now };
|
|
}
|
|
|
|
var original = (await Db.QueryAsync(connection, "SELECT DB_NAME()", r => r.String(0), ct).ConfigureAwait(false))[0];
|
|
try
|
|
{
|
|
var use = "USE " + Db.Identifier(distribution) + ";\n";
|
|
var agents = await Db.QueryAsync(connection, use + AgentsSql, r => new DistributionAgent(
|
|
r.String(0), r.NullableString(1), r.NullableInt(2),
|
|
r.NullableDateTime(3) is { } at ? new DateTimeOffset(DateTime.SpecifyKind(at, DateTimeKind.Utc)) : null,
|
|
r.NullableLong(4), r.NullableString(5)), ct).ConfigureAwait(false);
|
|
var errors = (await Db.QueryAsync(connection, use + ErrorsSql, r => r.Int(0), ct).ConfigureAwait(false))[0];
|
|
return new ReplicationSnapshot { CapturedAt = context.Now, IsDistributor = true, DistributionDatabase = distribution, Agents = agents, ErrorsLastDay = errors };
|
|
}
|
|
catch (DbException ex)
|
|
{
|
|
return new ReplicationSnapshot
|
|
{
|
|
CapturedAt = context.Now, IsDistributor = true, DistributionDatabase = distribution,
|
|
// The error message names the agent's login: kept apart from the reason, which rules and the API show as is.
|
|
Unavailable = NoRightsText,
|
|
UnavailableDetail = ex.Message.Split('\n')[0].Trim(),
|
|
};
|
|
}
|
|
finally
|
|
{
|
|
await Db.ExecuteAsync(connection, "USE " + Db.Identifier(original), ct).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|