using Microsoft.Extensions.Logging; using NSubstitute; using Serilog; using ZB.MOM.WW.Audit; using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; 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; namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests; /// /// ScadaBridge#56 (central half): while a method request is traced, other central node-log lines /// tagged with its ExecutionId become node rows of the request; the script's own /// Log.* line and the executor's outcome line are not copied, and an untraced request copies /// nothing. /// public class InboundNodeLogCaptureTests { private readonly Serilog.ILogger _node = new LoggerConfiguration() .MinimumLevel.Information() .Enrich.FromLogContext() .WriteTo.Sink(new ExecutionLogCaptureSink()) .CreateLogger(); private readonly InboundScriptExecutor _executor; private readonly ApiMethod _method = new("node", "x") { Id = 1, TimeoutSeconds = 10 }; /// Builds an executor whose own logger writes to the node pipeline, like the host's. public InboundNodeLogCaptureTests() => _executor = new InboundScriptExecutor(new SerilogMel(_node), Substitute.For()); private sealed class SerilogMel(Serilog.ILogger inner) : ILogger { private readonly Serilog.ILogger _inner = inner.ForContext("SourceContext", typeof(InboundScriptExecutor).FullName); 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)); } } private async Task> RunAsync( Func> handler, bool trace) { _executor.RegisterHandler("node", handler); var sink = new List(); var route = new RouteHelper(Substitute.For(), Substitute.For()) .WithTraceSink(e => { lock (sink) sink.Add(e); }); await _executor.ExecuteAsync(_method, new Dictionary(), route, TimeSpan.FromSeconds(10), parentExecutionId: Guid.NewGuid(), trace: trace); lock (sink) { return sink .Select(e => AuditDetailsCodec.Deserialize(e.DetailsJson)) .Where(d => d.Kind == nameof(AuditKind.ScriptLog)) .Select(d => (d, ScriptTraceExtra.TryParse(d.Extra)!)) .ToList(); } } /// A callee's line on the request's flow becomes a node row with its category. /// A task that represents the asynchronous operation. [Fact] public async Task TracedRequest_CalleeLine_IsANodeRow() { var rows = await RunAsync(async _ => { await Task.Yield(); _node.ForContext("SourceContext", "Acme.InboundDatabaseHelper").Warning("query retried {Count}", 2); return null; }, trace: true); var row = Assert.Single(rows); Assert.Equal("node", row.Extra.LogSource); Assert.Equal("warn", row.Extra.LogLevel); Assert.Equal("Acme.InboundDatabaseHelper", row.Extra.LogCategory); Assert.Equal("query retried 2", row.Details.RequestSummary); } /// An untraced request registers nothing, so the same line is not captured. /// A task that represents the asynchronous operation. [Fact] public async Task UntracedRequest_CapturesNothing() { var rows = await RunAsync(_ => { _node.Warning("query retried"); return Task.FromResult(null); }, trace: false); Assert.Empty(rows); } /// /// The script's own Log line is one script row (its node forward is suppressed), and the /// executor's failure line is the inbound row's error, so it is not copied. /// /// A task that represents the asynchronous operation. [Fact] public async Task ScriptLine_AndExecutorFailureLine_AreNotCopied() { var rows = await RunAsync(ctx => { ctx.Log.Info("from the script"); throw new InvalidOperationException("boom"); }, trace: true); var row = Assert.Single(rows); Assert.Equal("script", row.Extra.LogSource); Assert.Equal("from the script", row.Details.RequestSummary); } }