Merge wt/step-logs-t6: Task 6
This commit is contained in:
@@ -400,7 +400,12 @@ internal static class ScriptRunLauncher
|
||||
scriptTimeoutToken: cts.Token,
|
||||
// Flow Debugger (design §5.2/§5.4): the trace flag and the inline sink.
|
||||
isTraced: trace,
|
||||
traceCollector: collector);
|
||||
traceCollector: collector,
|
||||
// Step logs (design 2026-09-09 D7): the per-run ScriptLog row cap. Read off
|
||||
// effectiveOptions (the IOptionsMonitor snapshot taken at the top of this run),
|
||||
// so an operator's live edit applies from the NEXT run, like the event-policy
|
||||
// flags above.
|
||||
maxLogLinesPerRun: effectiveOptions.MaxLogLinesPerRun);
|
||||
|
||||
var globals = new ScriptGlobals
|
||||
{
|
||||
@@ -426,13 +431,17 @@ internal static class ScriptRunLauncher
|
||||
|
||||
var state = await compiledScript.RunAsync(globals, cts.Token);
|
||||
|
||||
// Step logs (design D7): close the run's log BEFORE the anchor row, so the closing
|
||||
// "N line(s) dropped" row precedes the ScriptRun in emission order on every path.
|
||||
context.Log.FlushDropped();
|
||||
|
||||
// Flow Debugger: the ScriptRun row goes into the collector BEFORE the reply so the
|
||||
// inline list the caller receives includes the run itself. Complete() (not Snapshot)
|
||||
// closes the sink at the same instant — outbound rows finishing after this point
|
||||
// (plan 2026-09-06 D5) go to the audit writer only.
|
||||
EmitScriptRun(trace, collector, auditWriter, logger, scriptName, instanceName, siteId, sourceNode,
|
||||
context.ExecutionId, parentExecutionId, startedUtc, runStopwatch.ElapsedMilliseconds,
|
||||
parameters, state.ReturnValue, error: null, triggerExtra);
|
||||
parameters, state.ReturnValue, error: null, errorDetail: null, triggerExtra);
|
||||
|
||||
// Send result to requester if this was an Ask-based call
|
||||
if (!replyTo.IsNobody())
|
||||
@@ -467,9 +476,14 @@ internal static class ScriptRunLauncher
|
||||
_ = siteEventLogger?.LogEventAsync(
|
||||
"script", "Error", instanceName, $"ScriptActor:{scriptName}", errorMsg);
|
||||
|
||||
// Step logs (design D7): see the success path — the closing row comes first.
|
||||
context?.Log.FlushDropped();
|
||||
|
||||
// A timeout carries NO errorDetail (design D3): the OperationCanceledException's stack
|
||||
// is the launcher's cancellation plumbing, not the script's, so it would mislead.
|
||||
EmitScriptRun(trace, collector, auditWriter, logger, scriptName, instanceName, siteId, sourceNode,
|
||||
context?.ExecutionId, parentExecutionId, startedUtc, runStopwatch.ElapsedMilliseconds,
|
||||
parameters, null, errorMsg, triggerExtra);
|
||||
parameters, null, errorMsg, errorDetail: null, triggerExtra);
|
||||
|
||||
if (!replyTo.IsNobody())
|
||||
{
|
||||
@@ -492,9 +506,14 @@ internal static class ScriptRunLauncher
|
||||
_ = siteEventLogger?.LogEventAsync(
|
||||
"script", "Error", instanceName, $"ScriptActor:{scriptName}", errorMsg, ex.ToString());
|
||||
|
||||
// Step logs (design D7): see the success path — the closing row comes first.
|
||||
context?.Log.FlushDropped();
|
||||
|
||||
// Design D3: the failing run's full exception — type, message and stack — so the Flow
|
||||
// Debugger's red node can show the stack, not just the one-line message.
|
||||
EmitScriptRun(trace, collector, auditWriter, logger, scriptName, instanceName, siteId, sourceNode,
|
||||
context?.ExecutionId, parentExecutionId, startedUtc, runStopwatch.ElapsedMilliseconds,
|
||||
parameters, null, errorMsg, triggerExtra);
|
||||
parameters, null, errorMsg, errorDetail: ex.ToString(), triggerExtra);
|
||||
|
||||
if (!replyTo.IsNobody())
|
||||
{
|
||||
@@ -551,7 +570,7 @@ internal static class ScriptRunLauncher
|
||||
EmitScriptRun(
|
||||
trace: true, collector: null, writer, logger, scriptName, instanceName, siteId, sourceNode,
|
||||
executionId: Guid.NewGuid(), parentExecutionId, DateTime.UtcNow, elapsedMs: 0,
|
||||
parameters: null, returnValue: null, error: reason, triggerExtra);
|
||||
parameters: null, returnValue: null, error: reason, errorDetail: null, triggerExtra);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -564,13 +583,19 @@ internal static class ScriptRunLauncher
|
||||
/// traced run — collector first (so the inline reply carries it), then the best-effort
|
||||
/// audit writer. <c>OccurredAtUtc</c> is the run's START so the row sorts before the
|
||||
/// writes/waits the run made. No-op when untraced; never throws.
|
||||
///
|
||||
/// <para>Step logs (design 2026-09-09 D3): <paramref name="errorDetail"/> carries the failing
|
||||
/// run's full <c>Exception.ToString()</c> — type, message and stack — so the Flow Debugger can
|
||||
/// show the stack behind a red node instead of only the one-line message. A TIMED-OUT run
|
||||
/// passes null on purpose: its <c>OperationCanceledException</c> stack is the launcher's own
|
||||
/// cancellation plumbing, not the script's, and would mislead.</para>
|
||||
/// </summary>
|
||||
private static void EmitScriptRun(
|
||||
bool trace, FlowTraceCollector? collector, IAuditWriter? auditWriter, ILogger logger,
|
||||
string scriptName, string instanceName, string siteId, string? sourceNode,
|
||||
Guid? executionId, Guid? parentExecutionId, DateTime startedUtc, long elapsedMs,
|
||||
IReadOnlyDictionary<string, object?>? parameters, object? returnValue, string? error,
|
||||
ScriptTraceExtra? triggerExtra)
|
||||
string? errorDetail, ScriptTraceExtra? triggerExtra)
|
||||
{
|
||||
if (!trace) return;
|
||||
try
|
||||
@@ -586,6 +611,7 @@ internal static class ScriptRunLauncher
|
||||
sourceInstanceId: instanceName, sourceScript: sourceScript,
|
||||
durationMs: (int)Math.Min(elapsedMs, int.MaxValue),
|
||||
errorMessage: error,
|
||||
errorDetail: errorDetail,
|
||||
requestSummary: TraceJson(parameters),
|
||||
responseSummary: TraceJson(returnValue),
|
||||
extra: (triggerExtra ?? new ScriptTraceExtra { TriggerKind = "call" }).ToJson());
|
||||
|
||||
@@ -243,4 +243,18 @@ public class SiteRuntimeOptions
|
||||
/// Default: 30.
|
||||
/// </summary>
|
||||
public int TraceArmWindowSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Flow Debugger step logs (design 2026-09-09 D7): the maximum number of <c>ScriptLog</c>
|
||||
/// trace rows ONE script run may produce — the author's own <c>Log.*</c> lines plus the
|
||||
/// runtime lines captured while the run is traced. Lines past the cap are counted and
|
||||
/// dropped, and the run emits one closing <c>warn</c> row naming the dropped count, so a
|
||||
/// chatty script degrades to a truncated log instead of an unbounded row stream.
|
||||
///
|
||||
/// <para>The cap is per RUN, not per script: an inline shared script shares its caller's
|
||||
/// logger and therefore its counter. It only bounds trace rows — every line still reaches
|
||||
/// the node's <c>ILogger</c>, traced or not — and an untraced run produces no rows at all.
|
||||
/// Must be greater than 0. Default: 200.</para>
|
||||
/// </summary>
|
||||
public int MaxLogLinesPerRun { get; set; } = 200;
|
||||
}
|
||||
|
||||
@@ -113,5 +113,11 @@ public sealed class SiteRuntimeOptionsValidator : OptionsValidatorBase<SiteRunti
|
||||
$"ScadaBridge:SiteRuntime:TraceArmWindowSeconds must be greater than 0 " +
|
||||
$"(was {options.TraceArmWindowSeconds}); it is how long the Instance Actor waits for the " +
|
||||
"device echo of a Flow Debugger traced write before the arm expires (design 2026-09-05 §5.3).");
|
||||
|
||||
builder.RequireThat(options.MaxLogLinesPerRun > 0,
|
||||
$"ScadaBridge:SiteRuntime:MaxLogLinesPerRun must be greater than 0 " +
|
||||
$"(was {options.MaxLogLinesPerRun}); it is the per-run cap on Flow Debugger ScriptLog " +
|
||||
"rows a script's Log.* calls may produce (design 2026-09-09 D7), and a run that may " +
|
||||
"emit no lines at all cannot report its own truncation.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user