feat(commons): FlowTraceCollector low-priority lane for log lines (D7)
This commit is contained in:
@@ -27,12 +27,33 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
|
|||||||
/// protobuf framing, and 1 048 576 chars is 1 MB of UTF-8 for ASCII payloads but up to ~3 MB for
|
/// protobuf framing, and 1 048 576 chars is 1 MB of UTF-8 for ASCII payloads but up to ~3 MB for
|
||||||
/// non-ASCII ones. That worst case still fits the 4 MB limit with the rest of the reply, which is
|
/// non-ASCII ones. That worst case still fits the 4 MB limit with the rest of the reply, which is
|
||||||
/// what the budget exists to guarantee — raise it only with a real serialized-size measurement.
|
/// what the budget exists to guarantee — raise it only with a real serialized-size measurement.
|
||||||
|
/// Step-logs addendum (D7, 2026-09-09): a <c>ScriptLog</c> row per script log line rides the same
|
||||||
|
/// sink via <see cref="AddLowPriority"/>, a lower-priority lane that shares the same
|
||||||
|
/// <see cref="_events"/>/<see cref="_bytes"/> accounting but stops <see cref="LowPriorityReserveEvents"/>
|
||||||
|
/// events short of the count cap and at <see cref="LowPriorityByteShare"/> of the byte budget — so
|
||||||
|
/// the step rows a run emits at the very end (the <c>ScriptRun</c> anchor is written last) always
|
||||||
|
/// have room, even on a chatty run whose log lines alone would otherwise fill the collector. A
|
||||||
|
/// dropped low-priority row sets <see cref="LogsTruncated"/> instead of <see cref="Truncated"/>,
|
||||||
|
/// because losing inline log lines does not affect step-row fidelity the way losing a step row
|
||||||
|
/// would — the page reports it separately ("log lines truncated inline — Replay is complete").
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class FlowTraceCollector
|
public sealed class FlowTraceCollector
|
||||||
{
|
{
|
||||||
/// <summary>Default inline cap per run (design §5.4 <c>MaxTraceEventsPerRun</c>).</summary>
|
/// <summary>Default inline cap per run (design §5.4 <c>MaxTraceEventsPerRun</c>).</summary>
|
||||||
public const int DefaultMaxEvents = 500;
|
public const int DefaultMaxEvents = 500;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Events reserved for <see cref="Add"/> (step rows) — <see cref="AddLowPriority"/> (log
|
||||||
|
/// lines) stops this many events short of the event cap (D7, 2026-09-09).
|
||||||
|
/// </summary>
|
||||||
|
public const int LowPriorityReserveEvents = 50;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Share of the byte budget <see cref="AddLowPriority"/> (log lines) may consume — the
|
||||||
|
/// remainder is reserved for <see cref="Add"/> (step rows) (D7, 2026-09-09).
|
||||||
|
/// </summary>
|
||||||
|
public const double LowPriorityByteShare = 0.75;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default inline byte budget per run — the summed redacted <see cref="AuditEvent.DetailsJson"/>
|
/// Default inline byte budget per run — the summed redacted <see cref="AuditEvent.DetailsJson"/>
|
||||||
/// length in UTF-16 chars (see the type remarks: ~1 MB of UTF-8 for ASCII payloads, ~3 MB worst
|
/// length in UTF-16 chars (see the type remarks: ~1 MB of UTF-8 for ASCII payloads, ~3 MB worst
|
||||||
@@ -53,6 +74,13 @@ public sealed class FlowTraceCollector
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Truncated { get; private set; }
|
public bool Truncated { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True once an <see cref="AddLowPriority"/> (log line) was dropped because the low-priority
|
||||||
|
/// event or byte share was reached — reported separately from <see cref="Truncated"/> because
|
||||||
|
/// step-row fidelity is unaffected (D7, 2026-09-09).
|
||||||
|
/// </summary>
|
||||||
|
public bool LogsTruncated { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True once <see cref="Complete"/> ran — the run's reply snapshot has been taken and
|
/// True once <see cref="Complete"/> ran — the run's reply snapshot has been taken and
|
||||||
/// later <see cref="Add"/> calls are ignored (they never count as <see cref="Truncated"/>).
|
/// later <see cref="Add"/> calls are ignored (they never count as <see cref="Truncated"/>).
|
||||||
@@ -77,10 +105,22 @@ public sealed class FlowTraceCollector
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Appends the REDACTED copy of an event, or marks the collector truncated once the event
|
/// Appends the REDACTED copy of a step event, or marks the collector truncated once the
|
||||||
/// cap or the byte budget is reached. A no-op after <see cref="Complete"/>.
|
/// event cap or the byte budget is reached. A no-op after <see cref="Complete"/>. Full
|
||||||
|
/// priority — never yields to a log line queued via <see cref="AddLowPriority"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Add(AuditEvent evt)
|
public void Add(AuditEvent evt) => AddCore(evt, lowPriority: false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Appends the REDACTED copy of a <c>ScriptLog</c> event at LOW priority (D7, 2026-09-09):
|
||||||
|
/// it stops <see cref="LowPriorityReserveEvents"/> events short of the event cap and at
|
||||||
|
/// <see cref="LowPriorityByteShare"/> of the byte budget, so log lines never crowd out the
|
||||||
|
/// step rows a run emits at the end. A dropped row sets <see cref="LogsTruncated"/>, not
|
||||||
|
/// <see cref="Truncated"/>. A no-op after <see cref="Complete"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void AddLowPriority(AuditEvent evt) => AddCore(evt, lowPriority: true);
|
||||||
|
|
||||||
|
private void AddCore(AuditEvent evt, bool lowPriority)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(evt);
|
ArgumentNullException.ThrowIfNull(evt);
|
||||||
// Redaction runs OUTSIDE the lock (arch-review F1): IAuditRedactor is a pure function
|
// Redaction runs OUTSIDE the lock (arch-review F1): IAuditRedactor is a pure function
|
||||||
@@ -91,7 +131,13 @@ public sealed class FlowTraceCollector
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (Completed) return;
|
if (Completed) return;
|
||||||
if (_events.Count >= _max || _bytes + size > _maxBytes) { Truncated = true; return; }
|
var maxCount = lowPriority ? Math.Max(1, _max - LowPriorityReserveEvents) : _max;
|
||||||
|
var maxBytes = lowPriority ? (int)(_maxBytes * LowPriorityByteShare) : _maxBytes;
|
||||||
|
if (_events.Count >= maxCount || _bytes + size > maxBytes)
|
||||||
|
{
|
||||||
|
if (lowPriority) LogsTruncated = true; else Truncated = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
_events.Add(filtered);
|
_events.Add(filtered);
|
||||||
_bytes += size;
|
_bytes += size;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using ZB.MOM.WW.Audit;
|
using ZB.MOM.WW.Audit;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Debugging;
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Debugging;
|
||||||
|
|
||||||
@@ -16,6 +18,12 @@ public class FlowTraceCollectorTests
|
|||||||
Target = i.ToString(),
|
Target = i.ToString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>Low-priority-lane helper (D7, 2026-09-09): a canonical row built through the
|
||||||
|
/// factory, so <c>DetailsJson</c> carries a real envelope around <paramref name="details"/>.</summary>
|
||||||
|
private static AuditEvent Evt(string? details = null) => ScadaBridgeAuditEventFactory.Create(
|
||||||
|
AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered,
|
||||||
|
target: "S", requestSummary: details);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Add_BeyondCap_KeepsFirstN_AndFlagsTruncated()
|
public void Add_BeyondCap_KeepsFirstN_AndFlagsTruncated()
|
||||||
{
|
{
|
||||||
@@ -128,6 +136,65 @@ public class FlowTraceCollectorTests
|
|||||||
Assert.Throws<ArgumentOutOfRangeException>(() => new FlowTraceCollector(maxBytes: 0));
|
Assert.Throws<ArgumentOutOfRangeException>(() => new FlowTraceCollector(maxBytes: 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>D7 (2026-09-09): the low-priority lane stops short of the event cap so the
|
||||||
|
/// step rows a run emits at the end always have room.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void AddLowPriority_StopsBeforeTheReserve_AndSetsLogsTruncated()
|
||||||
|
{
|
||||||
|
var c = new FlowTraceCollector(maxEvents: 60, maxBytes: 1_000_000); // reserve = 50 → low-priority stops at 10
|
||||||
|
for (var i = 0; i < 12; i++) c.AddLowPriority(Evt());
|
||||||
|
|
||||||
|
Assert.Equal(10, c.Count);
|
||||||
|
Assert.True(c.LogsTruncated);
|
||||||
|
Assert.False(c.Truncated);
|
||||||
|
|
||||||
|
c.Add(Evt()); // a step row still fits
|
||||||
|
|
||||||
|
Assert.Equal(11, c.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>D7 (2026-09-09): the low-priority lane also stops short of the byte budget.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void AddLowPriority_RespectsByteShare()
|
||||||
|
{
|
||||||
|
var c = new FlowTraceCollector(maxEvents: 500, maxBytes: 1_000); // 75% = 750 chars
|
||||||
|
c.AddLowPriority(Evt(details: new string('x', 600)));
|
||||||
|
c.AddLowPriority(Evt(details: new string('x', 200)));
|
||||||
|
|
||||||
|
Assert.Equal(1, c.Count);
|
||||||
|
Assert.True(c.LogsTruncated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>D7 (2026-09-09): the reserve floors at 1, so a tiny cap still admits one log line.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void AddLowPriority_WithTinyCap_StillAllowsOne()
|
||||||
|
{
|
||||||
|
var c = new FlowTraceCollector(maxEvents: 20, maxBytes: 1_000_000); // max(1, 20-50) = 1
|
||||||
|
|
||||||
|
c.AddLowPriority(Evt());
|
||||||
|
Assert.Equal(1, c.Count);
|
||||||
|
Assert.False(c.LogsTruncated);
|
||||||
|
|
||||||
|
c.AddLowPriority(Evt());
|
||||||
|
Assert.Equal(1, c.Count);
|
||||||
|
Assert.True(c.LogsTruncated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>D7 (2026-09-09): <see cref="FlowTraceCollector.Complete"/> closes the low-priority
|
||||||
|
/// lane too — a late log line is dropped silently, not counted as truncated.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void AddLowPriority_IsNoOpAfterComplete()
|
||||||
|
{
|
||||||
|
var c = new FlowTraceCollector();
|
||||||
|
c.AddLowPriority(Evt());
|
||||||
|
c.Complete();
|
||||||
|
|
||||||
|
c.AddLowPriority(Evt());
|
||||||
|
|
||||||
|
Assert.Equal(1, c.Count);
|
||||||
|
Assert.False(c.LogsTruncated);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Upper-cases <c>DetailsJson</c> so a test can see the filtered copy land.</summary>
|
/// <summary>Upper-cases <c>DetailsJson</c> so a test can see the filtered copy land.</summary>
|
||||||
private sealed class UpperRedactor : IAuditRedactor
|
private sealed class UpperRedactor : IAuditRedactor
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user