using System.Collections.Concurrent; using Newtonsoft.Json; using Serilog; using Serilog.Core; using Serilog.Events; using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection; using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance; using ZB.MOM.WW.ScadaBridge.Commons.Messages.ScriptExecution; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; using ZB.MOM.WW.ScadaBridge.Commons.Observability; namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Observability; /// /// ScadaBridge#56: which messages name the execution they serve, and what pushing one does to a /// LogContext-enriched logger. /// public class ExecutionLogContextTests { private static readonly DateTimeOffset Now = DateTimeOffset.UtcNow; private static readonly Guid Id = Guid.NewGuid(); private sealed class CaptureSink : ILogEventSink { public ConcurrentQueue Events { get; } = new(); public void Emit(LogEvent logEvent) => Events.Enqueue(logEvent); } /// Every correlated message, built with on its execution field. /// The messages. public static TheoryData CorrelatedMessages() => new() { new SetStaticAttributeCommand("c", "i", "a", "v", Now, SourceExecutionId: Id), new AttributeValueChanged("i", "p", "a", 1, "Good", Now, SourceExecutionId: Id), new ScriptCallRequest("s", null, 0, "c", ParentExecutionId: Id), new RouteToCallRequest("c", "i", "s", null, Now, ParentExecutionId: Id), new RouteToGetAttributesRequest("c", "i", ["a"], Now, ParentExecutionId: Id), new RouteToSetAttributesRequest("c", "i", new Dictionary(), Now, ParentExecutionId: Id), new RouteToWaitForAttributeRequest("c", "i", "a", null, TimeSpan.FromSeconds(1), Now, ParentExecutionId: Id), new GetAttributeRequest("c", "i", "a", Now, SourceExecutionId: Id), new WaitForAttributeRequest("c", "i", "a", null, null, TimeSpan.FromSeconds(1), Now, SourceExecutionId: Id), new WriteAttributeBatchRequest("c", "i", new Dictionary(), null, null, Now, SourceExecutionId: Id), new WriteTagRequest("c", "conn", "tag", 1, Now, SourceExecutionId: Id), new WriteTagBatchRequest("c", "conn", new Dictionary(), null, null, Now, SourceExecutionId: Id), }; /// Each correlated message reports the execution id it was built with. /// The message. [Theory] [MemberData(nameof(CorrelatedMessages))] public void CorrelatedMessage_ReportsItsExecutionId(object message) => Assert.Equal(Id, Assert.IsAssignableFrom(message).CorrelatedExecutionId); /// /// The correlation member is an explicit interface implementation: it adds nothing to the /// serialized shape, so the wire is unchanged. /// /// The message. [Theory] [MemberData(nameof(CorrelatedMessages))] public void CorrelatedMessage_SerializesNoExtraMember(object message) => Assert.DoesNotContain(nameof(IExecutionCorrelated.CorrelatedExecutionId), JsonConvert.SerializeObject(message)); /// An inferred writer (the trace arm's device-echo heuristic) is not stamped onto log lines. [Fact] public void InferredValueChange_NamesNoExecution() { IExecutionCorrelated inferred = new AttributeValueChanged( "i", "p", "a", 1, "Good", Now, SourceExecutionId: Id, Trace: true, SourceExecutionInferred: true); Assert.Null(inferred.CorrelatedExecutionId); } /// A message that names no execution - or is not correlated at all - pushes nothing. [Fact] public void PushFor_NothingToStamp_ReturnsNull() { Assert.Null(ExecutionLogContext.PushFor(new WriteTagRequest("c", "conn", "tag", 1, Now))); Assert.Null(ExecutionLogContext.PushFor("not a message")); Assert.Null(ExecutionLogContext.PushFor(null)); Assert.Null(ExecutionLogContext.Push(null)); } /// Lines inside the scope carry the id, lines after it do not, and a nested push wins. [Fact] public void PushFor_StampsLinesInsideTheScope_AndTheInnermostWins() { var sink = new CaptureSink(); var log = new LoggerConfiguration().Enrich.FromLogContext().WriteTo.Sink(sink).CreateLogger(); var child = Guid.NewGuid(); using (ExecutionLogContext.PushFor(new ScriptCallRequest("s", null, 0, "c", ParentExecutionId: Id))) { log.Information("parent"); using (ExecutionLogContext.Push(child)) log.Information("child"); } log.Information("outside"); var events = sink.Events.ToList(); Assert.Equal(Id.ToString(), events[0].Properties[ExecutionLogContext.ExecutionIdProperty].ToString().Trim('"')); Assert.Equal(child.ToString(), events[1].Properties[ExecutionLogContext.ExecutionIdProperty].ToString().Trim('"')); Assert.False(events[2].Properties.ContainsKey(ExecutionLogContext.ExecutionIdProperty)); } }