feat(logging): bridge Akka logging to Serilog and copy traced runs' node-log lines into the Flow Debugger (ScadaBridge#120, #56)
#120: Akka ILoggingAdapter lines used to bypass Serilog and go to stdout only. - BuildHocon sets akka.loggers = [Host.Actors.SerilogAkkaLogger] and keeps akka.loglevel = INFO, so Akka Debug chatter is still filtered at the adapter. The INFO+ lines that used to reach stdout now go through the Serilog pipeline instead, with no extra volume. - Lines reach the file sinks with the node enrichers. SourceContext is the logging class's full name, AkkaSource is the actor path, Akka's own timestamp is kept, and format arguments become structured properties. - The Akka logger actor writes on its own thread, after the logging actor's LogContext is gone. To keep the ExecutionId, the two DCL actors log through ExecutionAwareLogging.GetLogger(Context), which records ExecutionLogContext.Current (a new AsyncLocal mirror of the pushed id) in an ExecutionTaggedLogMessage. The logger actor pushes that id again while it writes the line. #56 remainder (coordinator decision: no replicated schema change): - The host pipeline gains ExecutionLogCaptureSink. - While a run is traced, its step logger registers its ExecutionId: the site ScriptLogger from the launcher (plus each inline CallShared child id), and the central InboundScriptLogger from the executor. - Each tagged line becomes a ScriptLog row with LogSource "node" and the new ScriptTraceExtra.LogCategory. - The rows count against the existing MaxLogLinesPerRun cap, are clipped at 2 000 chars, ride the low-priority collector lane and pass the same redactor. Untraced runs cost one volatile read. - Lines are never copied twice. The step loggers' own forwards run inside ExecutionLogCapture.Suppress(), and the sink suppresses itself while it calls a target. The registration is declared inside the launcher's and executor's try, so it ends before the catch blocks log the outcome, and FlushDropped closes it. - FlowGraphBuilder lists node lines in FlowGraph.NodeLogs and never folds them onto a step. - The Flow Debugger shows them in a Node log card (FlowNodeLogPanel): grouped per execution under its run step, with an All | Warn + filter. Live and Replay both read the same rows, so site scope and role rules apply unchanged. - CLI --show-logs prints them as a trailing block. Tests: Commons ExecutionLogCaptureTests, ExecutionLogContext.Current, FlowGraphBuilder node lines. SiteRuntime NodeLogCaptureTests covers cross-actor capture via the launcher, untraced runs capturing nothing, no double capture of script and runtime lines or of the launcher failure line, the shared cap, collector redaction, shared-script child ids and FlushDropped closing capture. InboundNodeLogCaptureTests covers the central path. DCL ExecutionAwareLoggingTests covers the DataConnectionActor write-path Debug line being tagged and an uncorrelated line not being tagged. Host SerilogAkkaLoggerTests covers mapping, the ExecutionId crossing the logger hop, HOCON, ActorSystem boot and the Akka line reaching a traced run's step log end to end. Host LoggerConfigurationTests checks that the sink is wired. bUnit FlowNodeLogPanelTests and a CLI projection test cover the UI and CLI. Every new assertion was mutation-proven. Docs: step-logs design section 12, Component-SiteRuntime, CentralUI, InboundAPI, Host, CLI README, CLAUDE.md. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -138,10 +138,10 @@ writer is *inferred* by the trace arm), `ScriptCallRequest`, the four `RouteTo*R
|
||||
`SourceExecutionId` — `GetAttributeRequest`, `WaitForAttributeRequest`, `WriteAttributeBatchRequest`, `WriteTagRequest`,
|
||||
`WriteTagBatchRequest`; the Instance, Script, Alarm, Deployment Manager and both DCL actors re-push it per message in
|
||||
`AroundReceive` (`ExecutionLogContext.PushFor`; one type test for an uncorrelated message). So a node-log line the Instance
|
||||
Actor or a protocol adapter writes while serving a run's write carries the run's id. **Still open:** a queryable per-execution
|
||||
sink — the `site_events` execution-id column (replicated table, both-nodes-together schema change) or a sink that copies tagged
|
||||
lines of a *traced* execution into `ScriptLog` rows — and a Flow Debugger section that shows them; and the DCL actors' own Akka
|
||||
`ILoggingAdapter` lines, which bypass Serilog entirely (Gitea #120); a `Log` sink to the site event log; capturing `Console.Out` on a site run — `System` is imported so
|
||||
Actor or a protocol adapter writes while serving a run's write carries the run's id. **Capture done too — see §12** (no
|
||||
`site_events` schema change: a Serilog sink copies the tagged lines of a *traced* execution into its `ScriptLog` rows, and the
|
||||
Flow Debugger lists them in a *Node log* section); the DCL actors' own Akka `ILoggingAdapter` lines now reach Serilog with the
|
||||
id (Gitea #120, §12). Still out of scope: a `Log` sink to the site event log; capturing `Console.Out` on a site run — `System` is imported so
|
||||
`Console.WriteLine` compiles today, and the Test Run sandbox captures it (`SandboxConsoleCapture`), but on a
|
||||
site node `Console.Out` is process-global and shared by every concurrent run, so per-run capture is unsound;
|
||||
`Log.*` is the leveled, per-run replacement and the sandbox mirror writes to the same console pane.
|
||||
@@ -165,3 +165,48 @@ logger), `Component-InboundAPI.md` (`Log`, `ErrorDetail`), `Component-AuditLog.m
|
||||
`Component-ScriptAnalysis.md` (compile surface), CLI README (`--show-logs`), `CLAUDE.md` Flow Debugger bullet
|
||||
(v1.4), deferred-work register row 110 (close the console half; keep Direction B), plus a new row for the
|
||||
node-logger enrichment follow-up (§9).
|
||||
|
||||
## 12. Other actors' lines per execution (Gitea #120 + #56, 2026-11-10)
|
||||
|
||||
**Akka logging is bridged to Serilog (#120).** `AkkaHostedService.BuildHocon` sets
|
||||
`akka.loggers = [SerilogAkkaLogger]` (Host) and keeps `akka.loglevel = INFO`, replacing Akka's stdout-only
|
||||
`DefaultLogger`: every `ILoggingAdapter` line now reaches the file sinks with the node enrichers, as
|
||||
`SourceContext` = the logging class's full name plus `AkkaSource` = the actor path, at the timestamp Akka
|
||||
recorded, with the adapter's format arguments as structured properties. Akka's Debug chatter is still
|
||||
filtered at the adapter, so the bridge adds no volume: the INFO+ lines that used to go to stdout only now go to
|
||||
the Serilog sinks instead. Akka writes each line later on its logger actor's thread, where the logging actor's
|
||||
`LogContext` no longer applies. The two DCL actors therefore log through
|
||||
`DataConnectionLayer.Logging.ExecutionAwareLogging.GetLogger(Context)`, which records
|
||||
`ExecutionLogContext.Current` (a new `AsyncLocal` mirror of the pushed id) by wrapping the message in an
|
||||
`ExecutionTaggedLogMessage` (`ToString()` unchanged, so stdout and TestKit filters see the same text). The logger
|
||||
actor re-pushes that id while it writes the line.
|
||||
|
||||
**Per-execution capture without a schema change (#56).** The decision was to add no replicated column. Instead,
|
||||
the host pipeline gains `Commons.Observability.ExecutionLogCaptureSink`. While a run is traced, its step logger
|
||||
registers its `ExecutionId` in `ExecutionLogCapture`: the site `ScriptLogger` registers from the launcher,
|
||||
plus each inline `CallShared` child's id, and the central `InboundScriptLogger` registers from the executor. The
|
||||
sink hands each line tagged with a registered id to that logger, which emits it as a `ScriptLog` row with
|
||||
`LogSource = "node"` and `ScriptTraceExtra.LogCategory` = the logger category. Consequences:
|
||||
|
||||
- **Untraced runs cost ~nothing.** The sink reads one volatile counter and returns. An untraced step logger never
|
||||
registers.
|
||||
- **Same bounds as every other line.** Node lines count against `MaxLogLinesPerRun` together with the script's
|
||||
own lines and are clipped at 2 000 chars. They ride the collector's low-priority lane and pass the same
|
||||
redactor inline and in the store.
|
||||
- **No double capture.** The step loggers' own node forwards (`ScriptLogger`, `TraceCapturingLogger`,
|
||||
`InboundScriptLogger`) run inside `ExecutionLogCapture.Suppress()`, and the sink suppresses itself while it calls
|
||||
a target. The registration is declared inside the launcher's and executor's `try`, so it ends before any catch
|
||||
block logs the run's outcome, which is already the `ScriptRun` / inbound row. `FlushDropped` also closes it, so
|
||||
no node line lands after the closing row.
|
||||
- **What it sees** is what the node logs: its minimum level and per-source overrides apply first, so a node at
|
||||
Information never yields Debug lines. A line logged after the run ends (for example a late Akka logger-actor
|
||||
write) is not captured; it still reaches the node log.
|
||||
- **Graph and UI.** `FlowGraphBuilder` lists node lines in `FlowGraph.NodeLogs` in time order and never folds
|
||||
them onto a step, because no step can honestly claim another actor's line. The Flow Debugger page renders them
|
||||
in a **Node log** card under the detail panel (`FlowNodeLogPanel`, `data-test="flow-nodelog-panel"`), grouped
|
||||
per execution under that execution's run step (the title selects it), with an `All | Warn +` filter and the
|
||||
short logger name. Live and Replay build the graph from the same rows, so site scope and the `RequireFlowDebugger`
|
||||
policy govern them as they govern every other row. CLI `--show-logs` prints them as a trailing `node log (n lines):` block.
|
||||
- **Mixed versions.** An old central projects `node` rows like any `ScriptLog` row and folds them onto the anchor
|
||||
step. An old site sends none.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user