using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.ScriptExecution;
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
///
/// ScadaBridge#56: while a run is traced, node-log lines that OTHER actors write for it (tagged with
/// its ExecutionId) are copied into its ScriptLog rows as source node — under the
/// run's line cap, through the same collector (and so the same redaction) as every other row — and
/// an untraced run copies nothing.
///
public class NodeLogCaptureTests : TestKit, IDisposable
{
private readonly ScriptExecutionScheduler _scheduler = new(4);
private readonly SharedScriptLibrary _sharedLibrary = new(
new ScriptCompilationService(NullLogger.Instance),
NullLogger.Instance);
/// The node's Serilog pipeline as the host builds it: LogContext enrichment plus the capture sink.
private readonly Serilog.ILogger _node = new Serilog.LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.FromLogContext()
.WriteTo.Sink(new ExecutionLogCaptureSink())
.CreateLogger();
///
void IDisposable.Dispose()
{
Shutdown();
_scheduler.Dispose();
}
/// A Microsoft logger over a Serilog logger (the host's UseSerilog shape), with a category.
private sealed class SerilogMel(Serilog.ILogger inner, string category) : ILogger
{
private readonly Serilog.ILogger _inner = inner.ForContext("SourceContext", category);
public IDisposable? BeginScope(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Information;
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func formatter)
{
if (!IsEnabled(logLevel)) return;
var level = logLevel switch
{
LogLevel.Information => Serilog.Events.LogEventLevel.Information,
LogLevel.Warning => Serilog.Events.LogEventLevel.Warning,
_ => Serilog.Events.LogEventLevel.Error,
};
_inner.Write(level, exception, "{Message:l}", formatter(state, exception));
}
}
///
/// Stands in for the Instance Actor: re-pushes the message's execution id (as the real one does in
/// AroundReceive), logs a line on ITS thread, and acknowledges the write.
///
private sealed class LoggingInstanceActor : ReceiveActor
{
public LoggingInstanceActor(Serilog.ILogger node)
{
var log = node.ForContext("SourceContext", "ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors.InstanceActor");
Receive(cmd =>
{
using (ExecutionLogContext.PushFor(cmd))
{
log.Warning("instance actor applied {Attribute} token=hunter2", cmd.AttributeName);
}
Sender.Tell(new SetStaticAttributeResponse(
cmd.CorrelationId, cmd.InstanceUniqueName, cmd.AttributeName, true, null, DateTimeOffset.UtcNow));
});
}
}
private static Script