feat(site-runtime): outbound API/DB/notification audit rows ride the flow-trace collector
ScriptRuntimeContext caches a trace sink (null on an untraced run, so the production path is byte-identical) and threads it into the ExternalSystem / Database / Notify helpers and AuditingDbConnection→AuditingDbCommand (onAudit). All eight outbound emit sites hand the SAME AuditEvent instance to the sink first, then to the writer/forwarder. FlowTraceCollector gains Completed + Complete(): ScriptRunLauncher closes the collector where it builds the reply snapshot, so un-awaited calls finishing after the run returned never mutate a list the reply was built from. Cap (500) unchanged. Plan 2026-09-06 D5. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
@@ -179,6 +179,16 @@ public class ScriptRuntimeContext
|
||||
/// </summary>
|
||||
private readonly FlowTraceCollector? _traceCollector;
|
||||
|
||||
/// <summary>
|
||||
/// Outbound-calls addendum (plan 2026-09-06 D5): <see cref="Collect"/> as a delegate the
|
||||
/// <c>ExternalSystem</c> / <c>Database</c> / <c>Notify</c> helpers hand every
|
||||
/// <c>ApiOutbound</c> / <c>DbOutbound</c> / <c>Notification</c> row to — the same
|
||||
/// <see cref="AuditEvent"/> instance the audit writer / telemetry forwarder receives, so
|
||||
/// a Live run's inline reply carries them. Null when untraced so the helpers' emit paths
|
||||
/// stay byte-identical to the pre-feature behaviour.
|
||||
/// </summary>
|
||||
private readonly Action<AuditEvent>? _traceSink;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new script runtime context with all dependencies for script
|
||||
/// execution, external system calls, database access, and notification delivery.
|
||||
@@ -287,8 +297,17 @@ public class ScriptRuntimeContext
|
||||
_scriptTimeoutToken = scriptTimeoutToken;
|
||||
_isTraced = isTraced;
|
||||
_traceCollector = traceCollector;
|
||||
_traceSink = traceCollector is null ? null : Collect;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flow Debugger: the single feeder of the inline collector. Every row this run's
|
||||
/// reply should carry — the <c>ScriptTrace</c> rows from <see cref="EmitTrace"/> and the
|
||||
/// outbound rows the helpers emit — passes through here; a no-op when untraced or once
|
||||
/// the launcher has <see cref="FlowTraceCollector.Complete"/>d the run.
|
||||
/// </summary>
|
||||
private void Collect(AuditEvent evt) => _traceCollector?.Add(evt);
|
||||
|
||||
/// <summary>Flow Debugger (design 2026-09-05 §5.2): whether this run emits <c>ScriptTrace</c> rows.</summary>
|
||||
public bool IsTraced => _isTraced;
|
||||
|
||||
@@ -385,7 +404,7 @@ public class ScriptRuntimeContext
|
||||
return;
|
||||
}
|
||||
|
||||
_traceCollector?.Add(evt);
|
||||
Collect(evt);
|
||||
if (_auditWriter is null) return;
|
||||
try
|
||||
{
|
||||
@@ -818,7 +837,9 @@ public class ScriptRuntimeContext
|
||||
// SourceNode-stamping: the local node name (node-a/node-b),
|
||||
// threaded so the cached-call telemetry construction sites can stamp
|
||||
// it onto SiteCallOperational.SourceNode.
|
||||
_sourceNode);
|
||||
_sourceNode,
|
||||
// Flow Debugger: the outbound rows ride the inline collector (null when untraced).
|
||||
traceSink: _traceSink);
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to database operations.
|
||||
@@ -846,7 +867,9 @@ public class ScriptRuntimeContext
|
||||
// SourceNode-stamping: the local node name (node-a/node-b),
|
||||
// threaded so Database.CachedWrite's CachedSubmit telemetry can
|
||||
// stamp it onto SiteCallOperational.SourceNode.
|
||||
_sourceNode);
|
||||
_sourceNode,
|
||||
// Flow Debugger: the outbound rows ride the inline collector (null when untraced).
|
||||
traceSink: _traceSink);
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to the Notification Outbox API.
|
||||
@@ -869,7 +892,9 @@ public class ScriptRuntimeContext
|
||||
_parentExecutionId,
|
||||
// SourceNode-stamping: the local node name (node-a/node-b),
|
||||
// threaded so NotifyTarget.Send can stamp it onto NotificationSubmit.
|
||||
_sourceNode);
|
||||
_sourceNode,
|
||||
// Flow Debugger: the NotifySend row rides the inline collector (null when untraced).
|
||||
traceSink: _traceSink);
|
||||
|
||||
/// <summary>
|
||||
/// Read-only access to the instance's CURRENT alarm conditions (MES alarm-status API
|
||||
@@ -1065,6 +1090,13 @@ public class ScriptRuntimeContext
|
||||
/// </summary>
|
||||
private readonly string? _sourceNode;
|
||||
|
||||
/// <summary>
|
||||
/// Flow Debugger (plan 2026-09-06 D5): the run's inline collector. Each emit
|
||||
/// site hands it the SAME <see cref="AuditEvent"/> it gives the writer /
|
||||
/// forwarder, collector first. Null when untraced — the production default.
|
||||
/// </summary>
|
||||
private readonly Action<AuditEvent>? _traceSink;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new external system helper for script API calls.
|
||||
/// </summary>
|
||||
@@ -1078,6 +1110,7 @@ public class ScriptRuntimeContext
|
||||
/// <param name="cachedForwarder">Optional forwarder for cached call telemetry.</param>
|
||||
/// <param name="parentExecutionId">Optional identifier of the parent execution (for routed calls).</param>
|
||||
/// <param name="sourceNode">Optional cluster node identifier (node-a/node-b) for audit stamping.</param>
|
||||
/// <param name="traceSink">Flow Debugger inline collector — receives the same <see cref="AuditEvent"/> each emit hands to the writer/forwarder; null when untraced.</param>
|
||||
// Internal constructor for tests living in ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests
|
||||
// (via InternalsVisibleTo). Production sites resolve the helper through
|
||||
// ScriptRuntimeContext.ExternalSystem.
|
||||
@@ -1100,7 +1133,8 @@ public class ScriptRuntimeContext
|
||||
string? sourceScript = null,
|
||||
ICachedCallTelemetryForwarder? cachedForwarder = null,
|
||||
Guid? parentExecutionId = null,
|
||||
string? sourceNode = null)
|
||||
string? sourceNode = null,
|
||||
Action<AuditEvent>? traceSink = null)
|
||||
{
|
||||
_client = client;
|
||||
_instanceName = instanceName;
|
||||
@@ -1112,6 +1146,7 @@ public class ScriptRuntimeContext
|
||||
_cachedForwarder = cachedForwarder;
|
||||
_parentExecutionId = parentExecutionId;
|
||||
_sourceNode = sourceNode;
|
||||
_traceSink = traceSink;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1329,6 +1364,8 @@ public class ScriptRuntimeContext
|
||||
|
||||
try
|
||||
{
|
||||
// Flow Debugger: collector first, then the forwarder.
|
||||
_traceSink?.Invoke(telemetry.Audit);
|
||||
await _cachedForwarder.ForwardAsync(telemetry, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
@@ -1452,6 +1489,9 @@ public class ScriptRuntimeContext
|
||||
{
|
||||
try
|
||||
{
|
||||
// Flow Debugger: the immediate-completion rows are emitted inline while
|
||||
// the run is still open, so they legitimately ride the collector.
|
||||
_traceSink?.Invoke(attempted.Audit);
|
||||
await _cachedForwarder.ForwardAsync(attempted, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
@@ -1519,6 +1559,7 @@ public class ScriptRuntimeContext
|
||||
|
||||
try
|
||||
{
|
||||
_traceSink?.Invoke(resolve.Audit);
|
||||
await _cachedForwarder.ForwardAsync(resolve, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
@@ -1569,6 +1610,8 @@ public class ScriptRuntimeContext
|
||||
|
||||
try
|
||||
{
|
||||
// Flow Debugger: collector first (same instance), then the writer.
|
||||
_traceSink?.Invoke(evt);
|
||||
// Fire-and-forget so we never block the script on the audit
|
||||
// writer; the writer itself is responsible for fast, durable
|
||||
// enqueue (site SQLite hot-path). We DO observe failures via
|
||||
@@ -1758,6 +1801,14 @@ public class ScriptRuntimeContext
|
||||
/// </summary>
|
||||
private readonly string? _sourceNode;
|
||||
|
||||
/// <summary>
|
||||
/// Flow Debugger (plan 2026-09-06 D5): the run's inline collector, handed the
|
||||
/// same <see cref="AuditEvent"/> each emit gives the writer / forwarder and
|
||||
/// threaded into <see cref="AuditingDbConnection"/> for the sync <c>DbWrite</c>
|
||||
/// rows. Null when untraced.
|
||||
/// </summary>
|
||||
private readonly Action<AuditEvent>? _traceSink;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new database helper for script database access.
|
||||
/// </summary>
|
||||
@@ -1771,6 +1822,7 @@ public class ScriptRuntimeContext
|
||||
/// <param name="cachedForwarder">Optional forwarder for cached call telemetry.</param>
|
||||
/// <param name="parentExecutionId">Optional identifier of the parent execution (for routed calls).</param>
|
||||
/// <param name="sourceNode">Optional cluster node identifier (node-a/node-b) for audit stamping.</param>
|
||||
/// <param name="traceSink">Flow Debugger inline collector — receives the same <see cref="AuditEvent"/> each emit hands to the writer/forwarder; null when untraced.</param>
|
||||
// Parameter ordering: executionId sits immediately after the
|
||||
// ILogger — see the note on ExternalSystemHelper's ctor for why the
|
||||
// post-logger slot is the one consistent position across all four
|
||||
@@ -1785,7 +1837,8 @@ public class ScriptRuntimeContext
|
||||
string? sourceScript = null,
|
||||
ICachedCallTelemetryForwarder? cachedForwarder = null,
|
||||
Guid? parentExecutionId = null,
|
||||
string? sourceNode = null)
|
||||
string? sourceNode = null,
|
||||
Action<AuditEvent>? traceSink = null)
|
||||
{
|
||||
_gateway = gateway;
|
||||
_instanceName = instanceName;
|
||||
@@ -1797,6 +1850,7 @@ public class ScriptRuntimeContext
|
||||
_cachedForwarder = cachedForwarder;
|
||||
_parentExecutionId = parentExecutionId;
|
||||
_sourceNode = sourceNode;
|
||||
_traceSink = traceSink;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1837,7 +1891,9 @@ public class ScriptRuntimeContext
|
||||
executionId: _executionId,
|
||||
// (ParentExecutionId): the spawning execution's
|
||||
// id, threaded alongside _executionId. Null for non-routed runs.
|
||||
parentExecutionId: _parentExecutionId);
|
||||
parentExecutionId: _parentExecutionId,
|
||||
// Flow Debugger: the sync DbWrite rows ride the inline collector.
|
||||
onAudit: _traceSink);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1959,6 +2015,8 @@ public class ScriptRuntimeContext
|
||||
{
|
||||
try
|
||||
{
|
||||
// Flow Debugger: emitted inline while the run is open — rides the collector.
|
||||
_traceSink?.Invoke(attempted.Audit);
|
||||
await _cachedForwarder.ForwardAsync(attempted, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
@@ -1980,6 +2038,7 @@ public class ScriptRuntimeContext
|
||||
{
|
||||
try
|
||||
{
|
||||
_traceSink?.Invoke(resolve.Audit);
|
||||
await _cachedForwarder.ForwardAsync(resolve, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
@@ -2107,6 +2166,8 @@ public class ScriptRuntimeContext
|
||||
|
||||
try
|
||||
{
|
||||
// Flow Debugger: collector first, then the forwarder.
|
||||
_traceSink?.Invoke(telemetry.Audit);
|
||||
await _cachedForwarder.ForwardAsync(telemetry, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
@@ -2169,6 +2230,12 @@ public class ScriptRuntimeContext
|
||||
/// </summary>
|
||||
private readonly string? _sourceNode;
|
||||
|
||||
/// <summary>
|
||||
/// Flow Debugger (plan 2026-09-06 D5): the run's inline collector, forwarded to
|
||||
/// each <see cref="NotifyTarget"/>. Null when untraced.
|
||||
/// </summary>
|
||||
private readonly Action<AuditEvent>? _traceSink;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new notification helper for script notification delivery.
|
||||
/// </summary>
|
||||
@@ -2183,6 +2250,7 @@ public class ScriptRuntimeContext
|
||||
/// <param name="auditWriter">Optional writer for audit log entries.</param>
|
||||
/// <param name="parentExecutionId">Optional identifier of the parent execution (for routed calls).</param>
|
||||
/// <param name="sourceNode">Optional cluster node identifier (node-a/node-b) for audit stamping.</param>
|
||||
/// <param name="traceSink">Flow Debugger inline collector — receives the same <see cref="AuditEvent"/> <c>Send</c> hands to the writer; null when untraced.</param>
|
||||
// Parameter ordering: executionId sits immediately after the ILogger,
|
||||
// consistent with the other audit-threaded ctors. parentExecutionId is
|
||||
// a trailing optional param.
|
||||
@@ -2197,7 +2265,8 @@ public class ScriptRuntimeContext
|
||||
Guid executionId,
|
||||
IAuditWriter? auditWriter = null,
|
||||
Guid? parentExecutionId = null,
|
||||
string? sourceNode = null)
|
||||
string? sourceNode = null,
|
||||
Action<AuditEvent>? traceSink = null)
|
||||
{
|
||||
_storeAndForward = storeAndForward;
|
||||
_siteCommunicationActor = siteCommunicationActor;
|
||||
@@ -2210,6 +2279,7 @@ public class ScriptRuntimeContext
|
||||
_auditWriter = auditWriter;
|
||||
_parentExecutionId = parentExecutionId;
|
||||
_sourceNode = sourceNode;
|
||||
_traceSink = traceSink;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2232,7 +2302,9 @@ public class ScriptRuntimeContext
|
||||
_parentExecutionId,
|
||||
// SourceNode-stamping: the local node name, stamped
|
||||
// onto NotificationSubmit.SourceNode in Send().
|
||||
_sourceNode);
|
||||
_sourceNode,
|
||||
// Flow Debugger: the NotifySend row rides the inline collector.
|
||||
traceSink: _traceSink);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2339,6 +2411,13 @@ public class ScriptRuntimeContext
|
||||
/// </summary>
|
||||
private readonly string? _sourceNode;
|
||||
|
||||
/// <summary>
|
||||
/// Flow Debugger (plan 2026-09-06 D5): the run's inline collector — handed the
|
||||
/// same <c>NotifySend</c> <see cref="AuditEvent"/> the writer receives. Null when
|
||||
/// untraced.
|
||||
/// </summary>
|
||||
private readonly Action<AuditEvent>? _traceSink;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new notification target for a specific notification list.
|
||||
/// </summary>
|
||||
@@ -2352,6 +2431,7 @@ public class ScriptRuntimeContext
|
||||
/// <param name="auditWriter">Optional writer for audit log entries.</param>
|
||||
/// <param name="parentExecutionId">Optional identifier of the parent execution (for routed calls).</param>
|
||||
/// <param name="sourceNode">Optional cluster node identifier (node-a/node-b) for audit stamping.</param>
|
||||
/// <param name="traceSink">Flow Debugger inline collector; null when untraced.</param>
|
||||
internal NotifyTarget(
|
||||
string listName,
|
||||
StoreAndForwardService? storeAndForward,
|
||||
@@ -2362,7 +2442,8 @@ public class ScriptRuntimeContext
|
||||
Guid executionId,
|
||||
IAuditWriter? auditWriter = null,
|
||||
Guid? parentExecutionId = null,
|
||||
string? sourceNode = null)
|
||||
string? sourceNode = null,
|
||||
Action<AuditEvent>? traceSink = null)
|
||||
{
|
||||
_listName = listName;
|
||||
_storeAndForward = storeAndForward;
|
||||
@@ -2374,6 +2455,7 @@ public class ScriptRuntimeContext
|
||||
_auditWriter = auditWriter;
|
||||
_parentExecutionId = parentExecutionId;
|
||||
_sourceNode = sourceNode;
|
||||
_traceSink = traceSink;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2558,6 +2640,8 @@ public class ScriptRuntimeContext
|
||||
|
||||
try
|
||||
{
|
||||
// Flow Debugger: collector first (same instance), then the writer.
|
||||
_traceSink?.Invoke(evt);
|
||||
// Fire-and-forget (mirrors ExternalSystemHelper.EmitCallAudit)
|
||||
// so the script is never blocked on the audit writer; we observe
|
||||
// failures via ContinueWith so a thrown writer is logged rather
|
||||
|
||||
Reference in New Issue
Block a user