feat(scripting): root script logger + DPS publisher wired in Host
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Composes the root script <see cref="Serilog.ILogger"/> — the single pipeline every
|
||||
/// per-script logger derives from. Fans each script log event out to three destinations:
|
||||
/// a dedicated rolling <c>scripts-*.log</c> file, the main server log (via
|
||||
/// <see cref="ScriptLogCompanionSink"/>, mirroring Error-or-higher events at Warning), and
|
||||
/// the cluster <c>script-logs</c> DPS topic (via <see cref="ScriptLogTopicSink"/>) so the
|
||||
/// live Script-log Admin UI page can tail script output.
|
||||
/// </summary>
|
||||
public static class ScriptRootLoggerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the composed root script logger.
|
||||
/// </summary>
|
||||
/// <param name="publisher">
|
||||
/// The publisher the topic sink forwards entries to (the DPS publisher in production).
|
||||
/// </param>
|
||||
/// <param name="filePath">
|
||||
/// Path template for the rolling <c>scripts-*.log</c> file sink (e.g. <c>logs/scripts-.log</c>).
|
||||
/// </param>
|
||||
/// <param name="topicMinLevel">
|
||||
/// Minimum level for events forwarded to the <c>script-logs</c> topic; lower-level events
|
||||
/// still land in the file sink but are not pushed onto the cluster bus.
|
||||
/// </param>
|
||||
/// <returns>The composed root script logger.</returns>
|
||||
public static Serilog.ILogger Build(IScriptLogPublisher publisher, string filePath, LogEventLevel topicMinLevel)
|
||||
=> new LoggerConfiguration()
|
||||
.MinimumLevel.Verbose()
|
||||
.WriteTo.File(filePath, rollingInterval: RollingInterval.Day)
|
||||
.WriteTo.Sink(new ScriptLogCompanionSink(Serilog.Log.Logger))
|
||||
.WriteTo.Sink(new ScriptLogTopicSink(publisher, topicMinLevel))
|
||||
.CreateLogger();
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using Akka.Actor;
|
||||
using Akka.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Clients;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components;
|
||||
@@ -11,14 +13,17 @@ using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.ControlPlane;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
||||
using ZB.MOM.WW.OtOpcUa.Host;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Engines;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Health;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Observability;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Scripting;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime;
|
||||
using ZB.MOM.WW.Auth.Abstractions.Roles;
|
||||
@@ -113,6 +118,21 @@ if (hasDriver)
|
||||
new RoslynScriptedAlarmEvaluator(sp.GetRequiredService<ILoggerFactory>().CreateLogger<RoslynScriptedAlarmEvaluator>()));
|
||||
builder.Services.AddSingleton<IScriptedAlarmEvaluator>(sp => sp.GetRequiredService<RoslynScriptedAlarmEvaluator>());
|
||||
|
||||
// Script-log fan-out (Layer 0). The DPS publisher resolves the ActorSystem lazily so it never
|
||||
// races Akka startup. ScriptRootLogger wraps the composed pipeline (rolling scripts-*.log +
|
||||
// error mirror to the main log + script-logs DPS topic) for unambiguous DI resolution; Task 3
|
||||
// injects it into the Roslyn evaluators above.
|
||||
var scriptLogFilePath = builder.Configuration["Scripting:LogFilePath"] ?? "logs/scripts-.log";
|
||||
var scriptLogTopicMinLevel = Enum.TryParse<LogEventLevel>(
|
||||
builder.Configuration["Scripting:LogTopicMinLevel"], ignoreCase: true, out var parsedLevel)
|
||||
? parsedLevel
|
||||
: LogEventLevel.Information;
|
||||
builder.Services.AddSingleton<IScriptLogPublisher>(sp =>
|
||||
new DpsScriptLogPublisher(() => sp.GetRequiredService<ActorSystem>()));
|
||||
builder.Services.AddSingleton(sp => new ScriptRootLogger(
|
||||
ScriptRootLoggerFactory.Build(
|
||||
sp.GetRequiredService<IScriptLogPublisher>(), scriptLogFilePath, scriptLogTopicMinLevel)));
|
||||
|
||||
builder.Services.AddValidatedOptions<LdapOptions, LdapOptionsValidator>(builder.Configuration, LdapOptions.SectionName);
|
||||
// TryAdd so a fused admin+driver node (where AddOtOpcUaAuth also registers these) ends up
|
||||
// with exactly one descriptor; on a driver-only node these are the sole registrations.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using Akka.Actor;
|
||||
using Akka.Cluster.Tools.PublishSubscribe;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Scripting;
|
||||
|
||||
/// <summary>
|
||||
/// Concrete <see cref="IScriptLogPublisher"/> that routes each <see cref="ScriptLogEntry"/>
|
||||
/// onto the Akka DistributedPubSub <c>script-logs</c> topic
|
||||
/// (<see cref="VirtualTagActor.ScriptLogsTopic"/>). <c>ScriptLogSignalRBridge</c> subscribes
|
||||
/// to that topic so entries reach the live Script-log Admin UI page.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="ActorSystem"/> is resolved lazily through a <see cref="Func{ActorSystem}"/>
|
||||
/// so constructing the publisher never races Akka startup — the system only needs to exist
|
||||
/// by the time the first script logs, not at DI-registration time.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This type sits behind a Serilog sink (<see cref="ScriptLogTopicSink"/>); a sink must
|
||||
/// never throw back into the logging pipeline, so every failure path here is swallowed
|
||||
/// (best-effort logged at Debug to the main Serilog logger).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class DpsScriptLogPublisher : IScriptLogPublisher
|
||||
{
|
||||
private readonly Func<ActorSystem> _system;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="DpsScriptLogPublisher"/> class.</summary>
|
||||
/// <param name="system">
|
||||
/// Lazy accessor for the running <see cref="ActorSystem"/>. Invoked on each
|
||||
/// <see cref="Publish"/> so registration does not depend on Akka having started yet.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="system"/> is <c>null</c>.</exception>
|
||||
public DpsScriptLogPublisher(Func<ActorSystem> system) =>
|
||||
_system = system ?? throw new ArgumentNullException(nameof(system));
|
||||
|
||||
/// <summary>
|
||||
/// Publishes <paramref name="entry"/> onto the DPS <c>script-logs</c> topic. Any failure
|
||||
/// (system not yet ready, mediator unavailable) is swallowed so the logging pipeline is
|
||||
/// never disrupted by a transient cluster condition.
|
||||
/// </summary>
|
||||
/// <param name="entry">The entry to publish.</param>
|
||||
public void Publish(ScriptLogEntry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
var mediator = DistributedPubSub.Get(_system()).Mediator;
|
||||
mediator.Tell(new Publish(VirtualTagActor.ScriptLogsTopic, entry));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// A logging sink must never throw into the logging pipeline. Best-effort note to
|
||||
// the main log at Debug; nothing actionable for the script author here.
|
||||
Serilog.Log.Debug(ex, "DpsScriptLogPublisher could not route a script log entry to the cluster bus.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@
|
||||
-->
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian\ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj"/>
|
||||
<!-- IScriptLogPublisher lives in Core.Scripting; DpsScriptLogPublisher implements it
|
||||
here so the concrete Akka DPS routing stays out of the Core layer. -->
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Scripting\ZB.MOM.WW.OtOpcUa.Core.Scripting.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user