feat(site-runtime): ScriptLogger (Log global), TraceStepScope, TraceCapturingLogger (D4-D7)

This commit is contained in:
Joseph Doherty
2026-09-09 11:34:17 -04:00
parent 25b127b3e3
commit 14fb8c889b
5 changed files with 689 additions and 0 deletions
@@ -0,0 +1,60 @@
using Microsoft.Extensions.Logging;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
/// <summary>
/// Flow Debugger step logs (design 2026-09-09 D6): decorates the script context's
/// <see cref="ILogger"/> while a run is traced.
///
/// <para>Forwards EVERYTHING to the inner logger unchanged (the node's log file is unaffected by
/// tracing), and additionally copies <see cref="LogLevel.Information"/> and above into
/// <c>ScriptLog</c> rows with source <c>runtime</c>, attributed to the ambient
/// <see cref="TraceStepScope"/>. This is how the warnings the runtime logs on the script's behalf —
/// inside <c>SetAttribute</c>, <c>WriteBatchAndWait</c>, <c>WaitInternal</c>, the sync
/// <c>ExternalSystem.Call</c> — reach the Flow Debugger without editing each call site.</para>
///
/// <para>Debug and Trace are deliberately NOT captured (internal chatter, design D6), and the
/// per-run line cap of the sink applies to what is.</para>
///
/// <para>Installed only when the run is traced, so an untraced run keeps the bare inner logger.</para>
/// </summary>
/// <param name="inner">The node logger every entry is forwarded to.</param>
/// <param name="sink">The run's <see cref="ScriptLogger"/>, which owns the caps and the row emitter.</param>
public sealed class TraceCapturingLogger(ILogger inner, ScriptLogger sink) : ILogger
{
/// <inheritdoc />
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => inner.BeginScope(state);
/// <summary>
/// Information and above are always enabled — they are trace data even when the node's minimum
/// level is Warning. Below Information the inner logger decides.
/// </summary>
/// <param name="level">The level being tested.</param>
/// <returns><see langword="true"/> when the entry should be produced.</returns>
public bool IsEnabled(LogLevel level) => level >= LogLevel.Information || inner.IsEnabled(level);
/// <inheritdoc />
public void Log<TState>(LogLevel level, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
// Forward first, and only if the inner logger actually wants it: IsEnabled above lies
// upward for trace capture, so the node's own minimum level must be re-checked here.
if (inner.IsEnabled(level))
{
inner.Log(level, eventId, state, exception, formatter);
}
if (level < LogLevel.Information || level == LogLevel.None)
{
return;
}
var name = level switch
{
LogLevel.Information => "info",
LogLevel.Warning => "warn",
_ => "error",
};
sink.Runtime(name, formatter(state, exception), exception);
}
}