fix(audit): maintenance-login roll-forward/backfill, archive orphan cleanup, head-export health (ScadaBridge#113 #114 #115)

#113: AuditLogPartitionMaintenance (monthly SPLIT RANGE) and the
backfill-source-node endpoint now use the AuditLog:MaintenanceConnectionString
login when set (unset = unchanged). Migration AuditMaintenanceGrants gives
scadabridge_audit_purger ALTER ANY DATASPACE and column-level UPDATE (SourceNode).
#114: archive cleanup deletes the writer's own *.tmp files and manifest-less
data files older than 24 h inside {Directory}/{YYYY} (DeletedOrphan, dry-run aware).
#115: head-export failures bump scadabridge.audit.chain_head_export.failures and
Degrade the /health/ready entry audit-chain-head-export (failing or stale > 2 intervals).

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 23:47:47 -05:00
parent b95ee5a658
commit 533414a16d
19 changed files with 2943 additions and 16 deletions
@@ -0,0 +1,63 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using ZB.MOM.WW.ScadaBridge.AuditLog.Central.HeadExport;
namespace ZB.MOM.WW.ScadaBridge.Host.Health;
/// <summary>
/// /health/ready entry (ScadaBridge#115): Degraded while the off-box AuditLog chain-head export
/// (<c>AuditLog:HashChain:HeadExport</c>) is failing, or has had no success for more than two
/// intervals. Degraded maps to HTTP 200, so it shows on the Overview dashboard without taking
/// the node out of rotation. Healthy when the export is disabled.
/// </summary>
public sealed class AuditChainHeadExportHealthCheck : IHealthCheck
{
private readonly AuditChainHeadExportStatus _status;
private readonly TimeProvider _time;
/// <summary>Initializes the check.</summary>
/// <param name="status">The export status singleton.</param>
/// <param name="time">Clock (defaults to the system clock).</param>
public AuditChainHeadExportHealthCheck(AuditChainHeadExportStatus status, TimeProvider? time = null)
{
_status = status ?? throw new ArgumentNullException(nameof(status));
_time = time ?? TimeProvider.System;
}
/// <inheritdoc />
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
if (!_status.Enabled)
{
return Task.FromResult(HealthCheckResult.Healthy("Chain-head export disabled."));
}
var now = _time.GetUtcNow().UtcDateTime;
var data = new Dictionary<string, object>(StringComparer.Ordinal)
{
["totalFailures"] = _status.TotalFailures,
["consecutiveFailures"] = _status.ConsecutiveFailures,
["intervalMinutes"] = _status.IntervalMinutes,
};
if (_status.LastSuccessUtc is { } ok)
{
data["lastSuccessUtc"] = ok;
}
if (_status.ConsecutiveFailures > 0)
{
return Task.FromResult(HealthCheckResult.Degraded(
$"Chain-head export failing ({_status.ConsecutiveFailures} consecutive): {_status.LastError}", data: data));
}
var limit = TimeSpan.FromMinutes(2L * _status.IntervalMinutes);
var reference = _status.LastSuccessUtc ?? _status.StartedUtc;
if (reference is { } r && now - r > limit)
{
return Task.FromResult(HealthCheckResult.Degraded(
$"No successful chain-head export for more than 2 intervals (last {(_status.LastSuccessUtc is null ? "never" : _status.LastSuccessUtc.Value.ToString("O"))}).", data: data));
}
return Task.FromResult(HealthCheckResult.Healthy("Chain-head export OK.", data));
}
}
@@ -579,6 +579,12 @@ try
"bundle-signing-policy",
failureStatus: null,
tags: new[] { ZbHealthTags.Ready })
// ScadaBridge#115: Degraded (HTTP 200) while the off-box chain-head export is failing
// or stale; healthy when the export is disabled.
.AddTypeActivatedCheck<AuditChainHeadExportHealthCheck>(
"audit-chain-head-export",
failureStatus: null,
tags: new[] { ZbHealthTags.Ready })
// Oldest-Up-member check (review 01 [High]), unscoped: every central member competes for
// the one active slot. Since ZB.MOM.WW.Health 0.3.0 this IS the shared check — it selects
// by age, so the host-local OldestNodeActiveHealthCheck that existed only to avoid the old