using System.Collections.Concurrent; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using Serilog; using Serilog.Core; using Serilog.Events; using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Observability; namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests; /// /// ScadaBridge#56: an inbound API method request stamps its ExecutionId on the Serilog /// LogContext for the whole execution, so a line logged anywhere on the request's async flow /// (the method script's callees - route, database, communication) carries it. /// public class InboundScriptExecutorLogContextTests { private sealed class CaptureSink : ILogEventSink { public ConcurrentQueue Events { get; } = new(); public void Emit(LogEvent logEvent) => Events.Enqueue(logEvent); } private readonly CaptureSink _sink = new(); private readonly Serilog.ILogger _log; private readonly InboundScriptExecutor _executor = new(NullLogger.Instance, Substitute.For()); private readonly RouteHelper _route = new(Substitute.For(), Substitute.For()); private readonly ApiMethod _method = new("logs", "x") { Id = 1, TimeoutSeconds = 10 }; /// Registers a handler that logs one line from inside the method execution. public InboundScriptExecutorLogContextTests() { _log = new LoggerConfiguration().Enrich.FromLogContext().WriteTo.Sink(_sink).CreateLogger(); _executor.RegisterHandler("logs", async _ => { await Task.Yield(); // the line is written after an await, on a continuation _log.Information("inside the method"); return null; }); } private static Guid? ExecutionIdOf(LogEvent evt) => evt.Properties.TryGetValue(ExecutionLogContext.ExecutionIdProperty, out var v) ? Guid.Parse(v.ToString().Trim('"')) : null; /// A line written during the execution carries the request's id; one written after does not. /// A task that represents the asynchronous operation. [Fact] public async Task LinesDuringTheExecution_CarryTheRequestsExecutionId() { var executionId = Guid.NewGuid(); var result = await _executor.ExecuteAsync(_method, new Dictionary(), _route, TimeSpan.FromSeconds(10), parentExecutionId: executionId); _log.Information("after the method"); Assert.True(result.Success, result.ErrorMessage); var events = _sink.Events.ToList(); Assert.Equal(2, events.Count); Assert.Equal(executionId, ExecutionIdOf(events[0])); Assert.Null(ExecutionIdOf(events[1])); } /// A request with no execution id (outside the inbound flow) logs untagged. /// A task that represents the asynchronous operation. [Fact] public async Task NoExecutionId_LinesAreUntagged() { var result = await _executor.ExecuteAsync(_method, new Dictionary(), _route, TimeSpan.FromSeconds(10)); Assert.True(result.Success, result.ErrorMessage); Assert.Null(ExecutionIdOf(Assert.Single(_sink.Events))); } }