feat(scripting): evaluators log through root script logger → script-log page (F8)

This commit is contained in:
Joseph Doherty
2026-06-10 12:03:51 -04:00
parent bf86b3def6
commit bd2dd05a0c
7 changed files with 245 additions and 36 deletions
@@ -5,7 +5,6 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
using SerilogLogger = Serilog.ILogger;
using SerilogLog = Serilog.Log;
namespace ZB.MOM.WW.OtOpcUa.Host.Engines;
@@ -22,20 +21,24 @@ namespace ZB.MOM.WW.OtOpcUa.Host.Engines;
/// </summary>
public sealed class RoslynScriptedAlarmEvaluator : IScriptedAlarmEvaluator, IDisposable
{
private static readonly SerilogLogger ScriptLogger = SerilogLog.ForContext<RoslynScriptedAlarmEvaluator>();
private readonly ConcurrentDictionary<string, ScriptEvaluator<AlarmPredicateContext, bool>> _cache
= new(StringComparer.Ordinal);
private readonly ILogger<RoslynScriptedAlarmEvaluator> _logger;
private readonly SerilogLogger _scriptRoot;
private readonly TimeSpan _runTimeout;
private bool _disposed;
/// <summary>Initializes a new instance of the Roslyn scripted alarm evaluator.</summary>
/// <param name="logger">Logger for diagnostic messages.</param>
/// <param name="logger">Logger for diagnostic messages (host diagnostics).</param>
/// <param name="scriptRoot">Root script logger; user <c>ctx.Logger.*</c> output flows through this to the Script-log page.</param>
/// <param name="runTimeout">Optional timeout for script evaluation; defaults to 2 seconds.</param>
public RoslynScriptedAlarmEvaluator(ILogger<RoslynScriptedAlarmEvaluator> logger, TimeSpan? runTimeout = null)
public RoslynScriptedAlarmEvaluator(
ILogger<RoslynScriptedAlarmEvaluator> logger,
ScriptRootLogger scriptRoot,
TimeSpan? runTimeout = null)
{
_logger = logger;
_scriptRoot = (scriptRoot ?? throw new ArgumentNullException(nameof(scriptRoot))).Logger;
_runTimeout = runTimeout ?? TimeSpan.FromSeconds(2);
}
@@ -71,7 +74,12 @@ public sealed class RoslynScriptedAlarmEvaluator : IScriptedAlarmEvaluator, IDis
}
var readCache = BuildReadCache(dependencies);
var context = new AlarmPredicateContext(readCache, ScriptLogger);
// Per-evaluation script logger: bind both ScriptId and AlarmId from the alarm id so the
// Script-log page can attribute each line to the owning scripted alarm.
var scriptLog = _scriptRoot
.ForContext(ScriptLoggerFactory.ScriptIdProperty, alarmId)
.ForContext(ScriptLoggerFactory.AlarmIdProperty, alarmId);
var context = new AlarmPredicateContext(readCache, scriptLog);
try
{
@@ -5,7 +5,6 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;
using SerilogLogger = Serilog.ILogger;
using SerilogLog = Serilog.Log;
namespace ZB.MOM.WW.OtOpcUa.Host.Engines;
@@ -23,20 +22,24 @@ namespace ZB.MOM.WW.OtOpcUa.Host.Engines;
/// </summary>
public sealed class RoslynVirtualTagEvaluator : IVirtualTagEvaluator, IDisposable
{
private static readonly SerilogLogger ScriptLogger = SerilogLog.ForContext<RoslynVirtualTagEvaluator>();
private readonly ConcurrentDictionary<string, ScriptEvaluator<VirtualTagContext, object?>> _cache
= new(StringComparer.Ordinal);
private readonly ILogger<RoslynVirtualTagEvaluator> _logger;
private readonly SerilogLogger _scriptRoot;
private readonly TimeSpan _runTimeout;
private bool _disposed;
/// <summary>Initializes a new RoslynVirtualTagEvaluator with the given logger and optional timeout.</summary>
/// <param name="logger">Logger for recording compilation and execution errors.</param>
/// <summary>Initializes a new RoslynVirtualTagEvaluator with the given loggers and optional timeout.</summary>
/// <param name="logger">Logger for recording compilation and execution errors (host diagnostics).</param>
/// <param name="scriptRoot">Root script logger; user <c>ctx.Logger.*</c> output flows through this to the Script-log page.</param>
/// <param name="runTimeout">Maximum execution time for each script; defaults to 2 seconds if not specified.</param>
public RoslynVirtualTagEvaluator(ILogger<RoslynVirtualTagEvaluator> logger, TimeSpan? runTimeout = null)
public RoslynVirtualTagEvaluator(
ILogger<RoslynVirtualTagEvaluator> logger,
ScriptRootLogger scriptRoot,
TimeSpan? runTimeout = null)
{
_logger = logger;
_scriptRoot = (scriptRoot ?? throw new ArgumentNullException(nameof(scriptRoot))).Logger;
_runTimeout = runTimeout ?? TimeSpan.FromSeconds(2);
}
@@ -84,12 +87,18 @@ public sealed class RoslynVirtualTagEvaluator : IVirtualTagEvaluator, IDisposabl
}
var readCache = BuildReadCache(dependencies);
// Per-evaluation script logger: bind both ScriptId and VirtualTagId from the virtual-tag id
// (in the live path the script id equals the virtual-tag id) so the Script-log page can
// attribute each line. EquipmentId stays unbound for now.
var scriptLog = _scriptRoot
.ForContext(ScriptLoggerFactory.ScriptIdProperty, virtualTagId)
.ForContext(ScriptLoggerFactory.VirtualTagIdProperty, virtualTagId);
var context = new VirtualTagContext(
readCache,
setVirtualTag: (path, _) =>
_logger.LogDebug("VirtualTag {Id}: cross-tag write to {Path} dropped (single-tag adapter)",
virtualTagId, path),
logger: ScriptLogger);
logger: scriptLog);
try
{
+6 -2
View File
@@ -109,13 +109,17 @@ if (hasDriver)
// Replaces the F8-default NullVirtualTagEvaluator so VirtualTagActor evaluates real user
// scripts at runtime.
builder.Services.AddSingleton<RoslynVirtualTagEvaluator>(sp =>
new RoslynVirtualTagEvaluator(sp.GetRequiredService<ILoggerFactory>().CreateLogger<RoslynVirtualTagEvaluator>()));
new RoslynVirtualTagEvaluator(
sp.GetRequiredService<ILoggerFactory>().CreateLogger<RoslynVirtualTagEvaluator>(),
sp.GetRequiredService<ScriptRootLogger>()));
builder.Services.AddSingleton<IVirtualTagEvaluator>(sp => sp.GetRequiredService<RoslynVirtualTagEvaluator>());
// F9b — same pattern for scripted-alarm predicates. The actor preserves prior state on
// any Failure result, so a misbehaving script can't flip Active/Inactive spuriously.
builder.Services.AddSingleton<RoslynScriptedAlarmEvaluator>(sp =>
new RoslynScriptedAlarmEvaluator(sp.GetRequiredService<ILoggerFactory>().CreateLogger<RoslynScriptedAlarmEvaluator>()));
new RoslynScriptedAlarmEvaluator(
sp.GetRequiredService<ILoggerFactory>().CreateLogger<RoslynScriptedAlarmEvaluator>(),
sp.GetRequiredService<ScriptRootLogger>()));
builder.Services.AddSingleton<IScriptedAlarmEvaluator>(sp => sp.GetRequiredService<RoslynScriptedAlarmEvaluator>());
// Script-log fan-out (Layer 0). The DPS publisher resolves the ActorSystem lazily so it never