Merge wt/step-logs-t3: Task 3 FlowGraphBuilder log-line attachment
This commit is contained in:
@@ -43,16 +43,43 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
|
||||
/// <item>When an <c>InboundRequest</c> row carries a response summary, a <c>Response</c> step is
|
||||
/// synthesized as the final Central node, linked from the last non-async step of the <i>inbound</i>
|
||||
/// execution (the route call, or the inbound row itself), never from a cascaded site step.</item>
|
||||
/// <item><b>Log lines are never steps</b> (step-logs design 2026-09-09 D2). <c>ScriptLog</c> rows are
|
||||
/// partitioned out before anything else — before the input-order index — and folded onto a step's
|
||||
/// <see cref="FlowStep.Logs"/>: the step its <c>StepEventId</c> names, else the <i>anchor</i> of its
|
||||
/// execution (the <c>InboundRequest</c>, <c>RouteCall</c> or <c>ScriptRun</c> step, in that order),
|
||||
/// else that execution's first non-<c>Response</c> step. A line whose execution has no step at all is
|
||||
/// dropped and counted in <see cref="FlowGraph.UnattachedLogLines"/>. Within a step, lines are ordered
|
||||
/// by <c>OccurredAtUtc</c> then <c>Seq</c>, with a null <c>Seq</c> sorting last, so same-millisecond
|
||||
/// lines keep their emission order.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public static class FlowGraphBuilder
|
||||
{
|
||||
/// <summary>Builds the graph. <paramref name="truncated"/> is passed through to <see cref="FlowGraph.Truncated"/>.</summary>
|
||||
public static FlowGraph Build(IReadOnlyList<AuditEvent> events, bool truncated = false)
|
||||
/// <summary>
|
||||
/// Builds the graph. <paramref name="truncated"/> and <paramref name="logsTruncated"/> are passed
|
||||
/// through to <see cref="FlowGraph.Truncated"/> / <see cref="FlowGraph.LogsTruncated"/>.
|
||||
/// </summary>
|
||||
public static FlowGraph Build(IReadOnlyList<AuditEvent> events, bool truncated = false, bool logsTruncated = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(events);
|
||||
|
||||
// D2: a ScriptLog row is a line, never a node. Partition BEFORE the input-order index below, so
|
||||
// `seq` indexes only real step rows and a line can never shift an execution's display position.
|
||||
var lines = new List<FlowLogLine>();
|
||||
var stepEvents = new List<AuditEvent>(events.Count);
|
||||
foreach (var e in events)
|
||||
{
|
||||
if (FlowStepProjector.IsLogLine(e))
|
||||
lines.Add(FlowStepProjector.ToLogLine(e));
|
||||
else
|
||||
stepEvents.Add(e);
|
||||
}
|
||||
|
||||
events = stepEvents;
|
||||
|
||||
if (events.Count == 0)
|
||||
return new FlowGraph(Array.Empty<FlowStep>(), Array.Empty<FlowEdge>(), Predicted: false, Truncated: truncated);
|
||||
return new FlowGraph(Array.Empty<FlowStep>(), Array.Empty<FlowEdge>(), Predicted: false, Truncated: truncated,
|
||||
LogsTruncated: logsTruncated, UnattachedLogLines: lines.Count);
|
||||
|
||||
// Arch-review F5: a step's position in the input is the only cross-node ordering we can trust —
|
||||
// central rows carry central's clock, site rows the site's, and a 1-2 s skew used to sort a whole
|
||||
@@ -230,7 +257,65 @@ public static class FlowGraphBuilder
|
||||
edges.Add(new FlowEdge(origin.EventId, response.EventId, CrossesBoundary(origin, response), External: false));
|
||||
}
|
||||
|
||||
return new FlowGraph(steps, edges, Predicted: false, Truncated: truncated);
|
||||
// 5. Fold the log lines onto their owning steps (D2) — after the Response node joined `steps`,
|
||||
// so a line naming it (none should) still resolves rather than counting as unattached.
|
||||
AttachLogLines(steps, lines, out var unattached);
|
||||
|
||||
return new FlowGraph(steps, edges, Predicted: false, Truncated: truncated,
|
||||
LogsTruncated: logsTruncated, UnattachedLogLines: unattached);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches each log line to its step (D2): the step named by <see cref="FlowLogLine.StepEventId"/>,
|
||||
/// else the anchor of the line's execution (<c>InboundRequest</c> before <c>RouteCall</c> before
|
||||
/// <c>ScriptRun</c> — an inbound row and its route call share one central execution), else that
|
||||
/// execution's first non-<c>Response</c> step. Lines that resolve to nothing are counted in
|
||||
/// <paramref name="unattached"/>. Rewrites <paramref name="steps"/> in place.
|
||||
/// </summary>
|
||||
private static void AttachLogLines(List<FlowStep> steps, List<FlowLogLine> lines, out int unattached)
|
||||
{
|
||||
unattached = 0;
|
||||
if (lines.Count == 0) return;
|
||||
|
||||
var byEventId = new Dictionary<Guid, FlowStep>(steps.Count);
|
||||
foreach (var step in steps)
|
||||
byEventId[step.EventId] = step;
|
||||
|
||||
var anchorByExecution = steps
|
||||
.Where(s => s.ExecutionId is not null
|
||||
&& s.Kind is FlowStepKind.ScriptRun or FlowStepKind.InboundRequest or FlowStepKind.RouteCall)
|
||||
.GroupBy(s => s.ExecutionId!.Value)
|
||||
.ToDictionary(g => g.Key, g => g.OrderBy(s => KindRank(s.Kind)).First());
|
||||
var firstByExecution = steps
|
||||
.Where(s => s.ExecutionId is not null && s.Kind != FlowStepKind.Response)
|
||||
.GroupBy(s => s.ExecutionId!.Value)
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
|
||||
var logsByStep = new Dictionary<Guid, List<FlowLogLine>>();
|
||||
foreach (var line in lines)
|
||||
{
|
||||
FlowStep? owner = null;
|
||||
if (line.StepEventId is { } stepId && byEventId.TryGetValue(stepId, out var named))
|
||||
owner = named;
|
||||
else if (line.ExecutionId is { } execution
|
||||
&& (anchorByExecution.TryGetValue(execution, out var fallback)
|
||||
|| firstByExecution.TryGetValue(execution, out fallback)))
|
||||
owner = fallback;
|
||||
|
||||
if (owner is null)
|
||||
{
|
||||
unattached++;
|
||||
continue;
|
||||
}
|
||||
|
||||
(logsByStep.TryGetValue(owner.EventId, out var l) ? l : logsByStep[owner.EventId] = []).Add(line);
|
||||
}
|
||||
|
||||
for (var i = 0; i < steps.Count; i++)
|
||||
{
|
||||
if (logsByStep.TryGetValue(steps[i].EventId, out var l))
|
||||
steps[i] = steps[i] with { Logs = l.OrderBy(x => x.OccurredAtUtc).ThenBy(x => x.Seq ?? int.MaxValue).ToList() };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user