From bf86b3def68cb28960c455686541cbeec3cd8cf4 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 10 Jun 2026 11:56:51 -0400 Subject: [PATCH] fix(scripting): explicit companion logger + disposable ScriptRootLogger (T2 review) --- .../ScriptRootLogger.cs | 24 ++++++++++++------- .../Logging/ScriptRootLoggerFactory.cs | 11 +++++++-- src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs | 2 +- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptRootLogger.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptRootLogger.cs index 07f000c0..76db74aa 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptRootLogger.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptRootLogger.cs @@ -11,15 +11,23 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// file sink, the mirroring errors to the main log, /// and the fanning entries onto the cluster /// script-logs topic for the live Script-log Admin UI page. +/// +/// Implements so the DI container flushes the rolling-file +/// sink on graceful host shutdown. +/// /// -public sealed class ScriptRootLogger +public sealed class ScriptRootLogger : IDisposable { - /// Gets the composed root script logger that evaluators derive per-script loggers from. - public Serilog.ILogger Logger { get; } + private readonly Serilog.Core.Logger _logger; - /// Initializes a new instance of the class. - /// The composed root script logger. Must not be null. - /// Thrown when is null. - public ScriptRootLogger(Serilog.ILogger logger) => - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + /// The composed root script logger (file + companion mirror + DPS topic sink). + public Serilog.ILogger Logger => _logger; + + /// Initializes a new . + /// The composed Serilog logger; owned + disposed by this wrapper. + public ScriptRootLogger(Serilog.Core.Logger logger) => + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + /// Disposes the underlying logger, flushing the rolling-file sink. + public void Dispose() => _logger.Dispose(); } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Logging/ScriptRootLoggerFactory.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Logging/ScriptRootLoggerFactory.cs index 1796417a..810d43b0 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Logging/ScriptRootLoggerFactory.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Logging/ScriptRootLoggerFactory.cs @@ -27,12 +27,19 @@ public static class ScriptRootLoggerFactory /// Minimum level for events forwarded to the script-logs topic; lower-level events /// still land in the file sink but are not pushed onto the cluster bus. /// + /// + /// The application's main logger that receives mirrored Error-or-higher script events at + /// Warning level. Passed explicitly so callers control the source rather than reading the + /// global static inside the factory. + /// /// The composed root script logger. - public static Serilog.ILogger Build(IScriptLogPublisher publisher, string filePath, LogEventLevel topicMinLevel) + public static Serilog.Core.Logger Build( + IScriptLogPublisher publisher, string filePath, LogEventLevel topicMinLevel, + Serilog.ILogger companionLogger) => new LoggerConfiguration() .MinimumLevel.Verbose() .WriteTo.File(filePath, rollingInterval: RollingInterval.Day) - .WriteTo.Sink(new ScriptLogCompanionSink(Serilog.Log.Logger)) + .WriteTo.Sink(new ScriptLogCompanionSink(companionLogger)) .WriteTo.Sink(new ScriptLogTopicSink(publisher, topicMinLevel)) .CreateLogger(); } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs index fb7cf8b5..66a02c3b 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs @@ -131,7 +131,7 @@ if (hasDriver) new DpsScriptLogPublisher(() => sp.GetRequiredService())); builder.Services.AddSingleton(sp => new ScriptRootLogger( ScriptRootLoggerFactory.Build( - sp.GetRequiredService(), scriptLogFilePath, scriptLogTopicMinLevel))); + sp.GetRequiredService(), scriptLogFilePath, scriptLogTopicMinLevel, Serilog.Log.Logger))); builder.Services.AddValidatedOptions(builder.Configuration, LdapOptions.SectionName); // TryAdd so a fused admin+driver node (where AddOtOpcUaAuth also registers these) ends up