using Microsoft.Extensions.Logging; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// /// Flow Debugger step logs (design 2026-09-09 D6): decorates the script context's /// while a run is traced. /// /// Forwards EVERYTHING to the inner logger unchanged (the node's log file is unaffected by /// tracing), and additionally copies and above into /// ScriptLog rows with source runtime, attributed to the ambient /// . This is how the warnings the runtime logs on the script's behalf — /// inside SetAttribute, WriteBatchAndWait, WaitInternal, the sync /// ExternalSystem.Call — reach the Flow Debugger without editing each call site. /// /// Debug and Trace are deliberately NOT captured (internal chatter, design D6), and the /// per-run line cap of the sink applies to what is. /// /// Installed only when the run is traced, so an untraced run keeps the bare inner logger. /// public sealed class TraceCapturingLogger : ILogger { private readonly ILogger _inner; private readonly ScriptLogger _sink; /// Wraps so that Information and above also reach . /// The node logger every entry is forwarded to. /// The run's , which owns the caps and the row emitter. /// or is null. /// is itself a . public TraceCapturingLogger(ILogger inner, ScriptLogger sink) { ArgumentNullException.ThrowIfNull(inner); ArgumentNullException.ThrowIfNull(sink); if (inner is TraceCapturingLogger) { throw new ArgumentException("inner is already a TraceCapturingLogger — wrapping twice would emit two rows per script line", nameof(inner)); } _inner = inner; _sink = sink; } /// public IDisposable? BeginScope(TState state) where TState : notnull => _inner.BeginScope(state); /// /// 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. /// /// The level being tested. /// when the entry should be produced. public bool IsEnabled(LogLevel level) => level >= LogLevel.Information || _inner.IsEnabled(level); /// public void Log(LogLevel level, EventId eventId, TState state, Exception? exception, Func 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", }; string message; try { message = formatter(state, exception); } catch { // Deliberate swallow: formatting is the CALLER's code, and a great many of the entries // captured here are the runtime's own best-effort "audit write failed" warnings logged // from inside a catch block. A throwing formatter escaping this decorator would replace // the original failure with a formatting exception on a path whose whole contract is // that it never throws. The line is dropped; the inner logger already saw (or refused) // it above, unchanged. return; } _sink.Runtime(name, message, exception); } }