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);
}
}