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