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)));
}
}