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:
+9
-1
@@ -31,6 +31,7 @@ public sealed class AuditChainHeadExportService : IHostedService, IDisposable
|
||||
private readonly string? _node;
|
||||
private readonly TimeProvider _time;
|
||||
private readonly ILogger<AuditChainHeadExportService> _logger;
|
||||
private readonly AuditChainHeadExportStatus _status;
|
||||
private readonly Dictionary<DateTime, (long Seq, long? Purged, long Links)> _lastExported = new();
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _loop;
|
||||
@@ -42,14 +43,17 @@ public sealed class AuditChainHeadExportService : IHostedService, IDisposable
|
||||
/// <param name="nodeName">This node's name for the records.</param>
|
||||
/// <param name="logger">Logger.</param>
|
||||
/// <param name="time">Clock.</param>
|
||||
/// <param name="status">Health status the tick outcome is recorded on (ScadaBridge#115).</param>
|
||||
public AuditChainHeadExportService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<AuditLogHashChainOptions> options,
|
||||
IAuditChainHeadSink? sink,
|
||||
string? nodeName,
|
||||
ILogger<AuditChainHeadExportService> logger,
|
||||
TimeProvider? time = null)
|
||||
TimeProvider? time = null,
|
||||
AuditChainHeadExportStatus? status = null)
|
||||
{
|
||||
_status = status ?? new AuditChainHeadExportStatus();
|
||||
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
||||
_options = (options ?? throw new ArgumentNullException(nameof(options))).Value.HeadExport;
|
||||
_sink = sink;
|
||||
@@ -67,6 +71,7 @@ public sealed class AuditChainHeadExportService : IHostedService, IDisposable
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_status.MarkStarted(_options.IntervalMinutes, _time.GetUtcNow().UtcDateTime);
|
||||
_logger.LogInformation("AuditLog chain-head export every {Minutes} min to {Sink}.", _options.IntervalMinutes, _sink.Describe);
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_cts = cts;
|
||||
@@ -118,6 +123,7 @@ public sealed class AuditChainHeadExportService : IHostedService, IDisposable
|
||||
.ToList();
|
||||
if (changed.Count == 0)
|
||||
{
|
||||
_status.RecordSuccess(now);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -128,6 +134,7 @@ public sealed class AuditChainHeadExportService : IHostedService, IDisposable
|
||||
_lastExported[h.Month] = (h.LastSeq, h.PurgedThroughSeq, h.ChainedRows + h.Tombstones);
|
||||
}
|
||||
|
||||
_status.RecordSuccess(now);
|
||||
_logger.LogDebug("Exported {Count} AuditLog chain head(s) to {Sink}.", records.Count, _sink.Describe);
|
||||
return records.Count;
|
||||
}
|
||||
@@ -139,6 +146,7 @@ public sealed class AuditChainHeadExportService : IHostedService, IDisposable
|
||||
{
|
||||
// Contained: the next tick retries the same heads (nothing was marked exported).
|
||||
_logger.LogError(ex, "AuditLog chain-head export to {Sink} failed; retrying next tick.", _sink.Describe);
|
||||
_status.RecordFailure(_time.GetUtcNow().UtcDateTime, ex.Message);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user