using Serilog; using Serilog.Core; using Serilog.Events; namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// /// Serilog sink that mirrors script log events at /// or higher to a companion logger (typically the main opcua-*.log) at /// . Lets operators see script errors in the /// primary server log without drowning it in Debug/Info/Warning noise from scripts. /// /// /// /// Registered alongside the dedicated scripts-*.log rolling file sink in /// the root script-logger configuration — events below Error land only in the /// scripts file; Error/Fatal events land in both the scripts file (at original /// level) and the main log (downgraded to Warning since the main log's audience /// is server operators, not script authors). /// /// /// The forwarded message preserves the ScriptName property so operators /// reading the main log can tell which script raised the error at a glance. /// Original exception (if any) is attached so the main log's diagnostics keep /// the full stack trace. /// /// /// Rate-limited per script. A predicate that throws on every push would otherwise mirror /// one Warning per push into the main log; a keyed by script /// name admits 20 per minute and mirrors one summary line for the rest when the window rolls. The /// scripts-*.log file sink is unaffected and keeps the full stream. /// /// public sealed class ScriptLogCompanionSink : ILogEventSink { private readonly ILogger _mainLogger; private readonly LogEventLevel _minMirrorLevel; private readonly ScriptLogRateLimiter _limiter; /// Initializes a new instance of the ScriptLogCompanionSink. /// The companion logger to mirror events to. /// The minimum log level to mirror (default: Error). /// Per-script rate limiter; null ⇒ the default 20 per minute. public ScriptLogCompanionSink(ILogger mainLogger, LogEventLevel minMirrorLevel = LogEventLevel.Error, ScriptLogRateLimiter? limiter = null) { _mainLogger = mainLogger ?? throw new ArgumentNullException(nameof(mainLogger)); _minMirrorLevel = minMirrorLevel; _limiter = limiter ?? new ScriptLogRateLimiter(); } /// Emits a log event to the companion logger if it meets the minimum level and the script is within its per-minute budget. /// The log event to emit. public void Emit(LogEvent logEvent) { if (logEvent is null) return; if (logEvent.Level < _minMirrorLevel) return; var scriptName = "unknown"; if ((logEvent.Properties.TryGetValue(ScriptLoggerFactory.ScriptNameProperty, out var prop) || logEvent.Properties.TryGetValue(ScriptLoggerFactory.ScriptIdProperty, out prop)) && prop is ScalarValue sv && sv.Value is string s) { scriptName = s; } var admission = _limiter.Admit(scriptName); if (admission.SuppressedInPreviousWindow > 0) { _mainLogger.Warning( "[Script] {ScriptName}: {SuppressedCount} further script error(s) were suppressed in the previous {WindowSeconds:F0} s; see scripts-*.log", scriptName, admission.SuppressedInPreviousWindow, _limiter.Window.TotalSeconds); } if (!admission.Admitted) return; var rendered = logEvent.RenderMessage(); if (logEvent.Exception is not null) { _mainLogger.Warning(logEvent.Exception, "[Script] {ScriptName} emitted {OriginalLevel}: {ScriptMessage}", scriptName, logEvent.Level, rendered); } else { _mainLogger.Warning( "[Script] {ScriptName} emitted {OriginalLevel}: {ScriptMessage}", scriptName, logEvent.Level, rendered); } } }