using Akka.Actor; using Akka.Event; using Akka.TestKit.Xunit2; using NSubstitute; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection; using ZB.MOM.WW.ScadaBridge.Commons.Observability; using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors; using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Logging; using ZB.MOM.WW.ScadaBridge.HealthMonitoring; namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Actors; /// /// ScadaBridge#120: the DCL actors' own Akka _log lines record the execution id of the /// message being served, so the host's Serilog logger actor can tag them although it writes them /// on another thread. /// public class ExecutionAwareLoggingTests : TestKit { /// Debug level so the DCL's write-path Debug line is published. public ExecutionAwareLoggingTests() : base("akka.loglevel = DEBUG") { } private sealed record LogUnder(Guid? Id); private sealed class Probe : ReceiveActor { private readonly ILoggingAdapter _log = ExecutionAwareLogging.GetLogger(Context); public Probe() { Receive(m => { var id = m.Id; using (ExecutionLogContext.Push(id)) _log.Info("probe {0}", id?.ToString() ?? "none"); }); Receive(_ => _log.Debug("debug {0}", "x")); } } /// A line logged inside an execution's push is published wrapped with that id; its text is unchanged. [Fact] public void LineInsideAPush_IsTaggedWithTheExecutionId_TextUnchanged() { var events = CreateTestProbe(); Sys.EventStream.Subscribe(events.Ref, typeof(Info)); var id = Guid.NewGuid(); Sys.ActorOf(Props.Create(() => new Probe())).Tell(new LogUnder(id)); var info = events.FishForMessage(i => i.Message.ToString()!.StartsWith("probe", StringComparison.Ordinal)); var tagged = Assert.IsType(info.Message); Assert.Equal(id, tagged.ExecutionId); Assert.Equal($"probe {id}", info.Message.ToString()); Assert.IsAssignableFrom(tagged.Message); } /// With no execution current, the line is published exactly as Context.GetLogger() would publish it. [Fact] public void LineOutsideAPush_IsPublishedUnwrapped() { var events = CreateTestProbe(); Sys.EventStream.Subscribe(events.Ref, typeof(Info)); Sys.ActorOf(Props.Create(() => new Probe())).Tell(new LogUnder(null)); var info = events.FishForMessage(i => i.Message.ToString()!.StartsWith("probe", StringComparison.Ordinal)); Assert.IsAssignableFrom(info.Message); Assert.Equal("probe none", info.Message.ToString()); } /// The adapter honours akka.loglevel: a Debug line is not published once the bus is at Info. [Fact] public void DisabledLevel_PublishesNothing() { Sys.EventStream.SetLogLevel(Akka.Event.LogLevel.InfoLevel); var events = CreateTestProbe(); Sys.EventStream.Subscribe(events.Ref, typeof(Debug)); Sys.ActorOf(Props.Create(() => new Probe())).Tell("debug"); events.ExpectNoMsg(TimeSpan.FromMilliseconds(300)); } /// /// End to end on the DCL: the DataConnectionActor's own write-path Debug line, logged while it /// serves a correlated WriteTagRequest, is tagged with the request's execution id; an /// uncorrelated write's line is not (control). /// /// A task that represents the asynchronous operation. [Fact] public async Task DataConnectionActor_WritePathLine_IsTagged_UncorrelatedIsNot() { var adapter = Substitute.For(); adapter.ConnectAsync(Arg.Any>(), Arg.Any()) .Returns(Task.CompletedTask); adapter.WriteAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new WriteResult(true, null))); var options = new DataConnectionOptions { ReconnectInterval = TimeSpan.FromMilliseconds(100), WriteTimeout = TimeSpan.FromSeconds(5), SeedReadMaxAttempts = 1, SeedReadRetryDelay = TimeSpan.FromMilliseconds(10), }; var actor = Sys.ActorOf(Props.Create(() => new DataConnectionActor( "akka-tag", adapter, options, Substitute.For(), Substitute.For(), "OpcUa")), "akka-tag"); AwaitCondition(() => adapter.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ConnectAsync"), TimeSpan.FromSeconds(3)); await Task.Delay(200); var events = CreateTestProbe(); Sys.EventStream.Subscribe(events.Ref, typeof(Debug)); var id = Guid.NewGuid(); actor.Tell(new WriteTagRequest("c1", "akka-tag", "tagT", 1, DateTimeOffset.UtcNow, id)); Assert.True(ExpectMsg(TimeSpan.FromSeconds(5)).Success); var tagged = events.FishForMessage(d => d.Message.ToString()!.Contains("Writing to tag tagT", StringComparison.Ordinal)); Assert.Equal(id, Assert.IsType(tagged.Message).ExecutionId); actor.Tell(new WriteTagRequest("c2", "akka-tag", "tagU", 1, DateTimeOffset.UtcNow)); Assert.True(ExpectMsg(TimeSpan.FromSeconds(5)).Success); var plain = events.FishForMessage(d => d.Message.ToString()!.Contains("Writing to tag tagU", StringComparison.Ordinal)); Assert.IsNotType(plain.Message); } }