a07ff28f10
1. Request headers in Extra JSON (AuditWriteMiddleware): adds a `requestHeaders` object to the existing Extra JSON alongside remoteIp/userAgent; headers whose names appear in AuditLogOptions.HeaderRedactList (Authorization, X-Api-Key, Cookie, Set-Cookie by default) are replaced with "<redacted>" using OrdinalIgnoreCase matching — same policy as ScadaBridgeAuditRedactor. 2. AuditInboundCeilingHits counter: new IAuditInboundCeilingHitsCounter interface + NoOpAuditInboundCeilingHitsCounter default; AuditCentralHealthSnapshot implements the interface (Interlocked field, thread-safe) and exposes AuditInboundCeilingHits on IAuditCentralHealthSnapshot; AddAuditLog registers the NoOp default, AddAuditLogCentralMaintenance forwards to the snapshot; AuditWriteMiddleware accepts the counter as an optional ctor arg and increments it once per request where either the request or response body hit the cap. 3. Per-method SkipBodyCapture opt-out: adds SkipBodyCapture bool to PerTargetRedactionOverride; AuditWriteMiddleware consults the per-target override map at the start of InvokeAsync (before EnableBuffering) and, when set, skips body read + capture entirely — the audit row still emits with headers/metadata but null RequestSummary/ResponseSummary; truncation flags are also cleared so the ceiling-hits counter is not bumped for opted-out methods.
92 lines
3.9 KiB
C#
92 lines
3.9 KiB
C#
using System.Collections.Concurrent;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Payload;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
|
|
|
|
/// <summary>
|
|
/// Audit Log (#23) M6 Bundle E (T8, T9) — central singleton implementation of
|
|
/// <see cref="IAuditCentralHealthSnapshot"/>. Owns thread-safe
|
|
/// <see cref="System.Threading.Interlocked"/> counters for
|
|
/// <c>CentralAuditWriteFailures</c> + <c>AuditRedactionFailure</c> and a
|
|
/// per-site latched stalled-state map fed by the
|
|
/// <see cref="SiteAuditTelemetryStalledTracker"/>. Also implements the
|
|
/// writer surfaces (<see cref="ICentralAuditWriteFailureCounter"/> +
|
|
/// <see cref="IAuditRedactionFailureCounter"/>) so a single concrete object
|
|
/// is the source of truth — DI binds those two interfaces to this same
|
|
/// singleton instance on the central composition root.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Why one type for read + write.</b> The writer interfaces are tiny
|
|
/// (<c>Increment()</c>) and the read surface needs visibility of those
|
|
/// counters anyway — having a single class own both means the
|
|
/// <c>Interlocked</c> field IS the snapshot value, no extra plumbing needed.
|
|
/// Mirrors the
|
|
/// <see cref="ZB.MOM.WW.ScadaBridge.HealthMonitoring.SiteHealthCollector"/> pattern where
|
|
/// the collector both receives and exposes the metric.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Stalled-state plumbing.</b> The per-site stalled latch lives directly
|
|
/// on this snapshot. <see cref="SiteAuditTelemetryStalledTracker"/> is the
|
|
/// EventStream subscriber that pushes
|
|
/// <see cref="SiteAuditTelemetryStalledChanged"/> publications in via
|
|
/// <see cref="ApplyStalled"/>. Keeping the dictionary on this type (rather
|
|
/// than reading the tracker on every access) lets the snapshot be constructed
|
|
/// without an <see cref="Akka.Actor.ActorSystem"/> dependency — the tracker
|
|
/// is wired up later from the Akka bootstrap, once the system is built.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class AuditCentralHealthSnapshot
|
|
: IAuditCentralHealthSnapshot,
|
|
ICentralAuditWriteFailureCounter,
|
|
IAuditRedactionFailureCounter,
|
|
IAuditInboundCeilingHitsCounter
|
|
{
|
|
private int _centralAuditWriteFailures;
|
|
private int _auditRedactionFailure;
|
|
private int _auditInboundCeilingHits;
|
|
private readonly ConcurrentDictionary<string, bool> _stalled = new();
|
|
|
|
/// <inheritdoc/>
|
|
public int CentralAuditWriteFailures =>
|
|
Interlocked.CompareExchange(ref _centralAuditWriteFailures, 0, 0);
|
|
|
|
/// <inheritdoc/>
|
|
public int AuditRedactionFailure =>
|
|
Interlocked.CompareExchange(ref _auditRedactionFailure, 0, 0);
|
|
|
|
/// <inheritdoc/>
|
|
public int AuditInboundCeilingHits =>
|
|
Interlocked.CompareExchange(ref _auditInboundCeilingHits, 0, 0);
|
|
|
|
/// <inheritdoc/>
|
|
public IReadOnlyDictionary<string, bool> SiteAuditTelemetryStalled =>
|
|
new Dictionary<string, bool>(_stalled);
|
|
|
|
/// <summary>
|
|
/// Apply a <see cref="SiteAuditTelemetryStalledChanged"/> publication
|
|
/// observed by <see cref="SiteAuditTelemetryStalledTracker"/>. Public
|
|
/// so the tracker (which lives in the same assembly but is constructed
|
|
/// later from the Akka host) can push without a friend reference;
|
|
/// readers should call <see cref="SiteAuditTelemetryStalled"/>.
|
|
/// </summary>
|
|
/// <param name="evt">The event carrying the site ID and new stalled state.</param>
|
|
public void ApplyStalled(SiteAuditTelemetryStalledChanged evt)
|
|
{
|
|
if (evt is null) return;
|
|
_stalled[evt.SiteId] = evt.Stalled;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
void ICentralAuditWriteFailureCounter.Increment() =>
|
|
Interlocked.Increment(ref _centralAuditWriteFailures);
|
|
|
|
/// <inheritdoc/>
|
|
void IAuditRedactionFailureCounter.Increment() =>
|
|
Interlocked.Increment(ref _auditRedactionFailure);
|
|
|
|
/// <inheritdoc/>
|
|
void IAuditInboundCeilingHitsCounter.Increment() =>
|
|
Interlocked.Increment(ref _auditInboundCeilingHits);
|
|
}
|