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. /// /// public sealed class ScriptLogCompanionSink : ILogEventSink { private readonly ILogger _mainLogger; private readonly LogEventLevel _minMirrorLevel; public ScriptLogCompanionSink(ILogger mainLogger, LogEventLevel minMirrorLevel = LogEventLevel.Error) { _mainLogger = mainLogger ?? throw new ArgumentNullException(nameof(mainLogger)); _minMirrorLevel = minMirrorLevel; } 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) && prop is ScalarValue sv && sv.Value is string s) { scriptName = s; } 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); } } }