edd0ca672f
LogContext is an AsyncLocal and does not cross an Akka message hop, so the run's id now rides the message: Commons.Observability.IExecutionCorrelated (explicit interface member, no wire change) on SetStaticAttributeCommand, AttributeValueChanged (not for an inferred writer), ScriptCallRequest and the four RouteTo*Requests, plus a new additive SourceExecutionId on GetAttributeRequest, WaitForAttributeRequest, WriteAttributeBatchRequest, WriteTagRequest and WriteTagBatchRequest. ScriptRuntimeContext stamps its id on the requests it sends, the Instance Actor hands it on to the DCL, and the Instance, Script, Alarm, Deployment Manager and both DCL actors re-push it per message in AroundReceive (ExecutionLogContext.PushFor: one type test when the message names no execution). The inbound executor pushes the request's ExecutionId for the method execution. Tests (TestKit): Instance Actor line carries the message's id, no leak from the sender's ambient context, scope ends with the message, per-write ids inside a batched SetAttributes, ids forwarded on DCL writes, ScriptRuntimeContext stamps; DCL adapter lines on single/batch writes carry the id; inbound executor lines carry the request id; Commons mapping + no serialized member. Each mutation- proved. Still open on #56: a queryable per-execution sink and a Flow Debugger section. The DCL actors' own Akka _log lines bypass Serilog (filed #120). Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
58 lines
2.6 KiB
C#
58 lines
2.6 KiB
C#
using System.Collections.Concurrent;
|
|
using Microsoft.Extensions.Logging;
|
|
using Serilog;
|
|
using Serilog.Core;
|
|
using Serilog.Events;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
|
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Logging;
|
|
|
|
/// <summary>
|
|
/// A Microsoft <see cref="ILogger"/> that writes each call straight to a Serilog logger enriched
|
|
/// from <c>LogContext</c> - the shape the host gets from <c>UseSerilog</c> - and keeps the events,
|
|
/// so a test can read the <c>ExecutionId</c> a line carried at the moment it was logged.
|
|
/// </summary>
|
|
internal sealed class LogContextCapture : ILogger, ILogEventSink
|
|
{
|
|
private readonly Serilog.ILogger _serilog;
|
|
|
|
/// <summary>Creates the capture with its own Serilog pipeline.</summary>
|
|
public LogContextCapture()
|
|
=> _serilog = new LoggerConfiguration().MinimumLevel.Verbose()
|
|
.Enrich.FromLogContext().WriteTo.Sink(this).CreateLogger();
|
|
|
|
/// <summary>Every event logged so far.</summary>
|
|
public ConcurrentQueue<LogEvent> Events { get; } = new();
|
|
|
|
/// <summary>The <c>ExecutionId</c> the first line containing <paramref name="fragment"/> carried (null when none).</summary>
|
|
/// <param name="fragment">Text the rendered message contains.</param>
|
|
/// <returns>The id, or null when the line carried none.</returns>
|
|
public Guid? ExecutionIdOf(string fragment)
|
|
{
|
|
var evt = Events.First(e => e.RenderMessage().Contains(fragment, StringComparison.Ordinal));
|
|
return evt.Properties.TryGetValue(ExecutionLogContext.ExecutionIdProperty, out var value)
|
|
? Guid.Parse(value.ToString().Trim('"'))
|
|
: null;
|
|
}
|
|
|
|
/// <summary>Whether a line containing <paramref name="fragment"/> was logged.</summary>
|
|
/// <param name="fragment">Text the rendered message contains.</param>
|
|
/// <returns>True once such a line exists.</returns>
|
|
public bool Has(string fragment) => Events.Any(e => e.RenderMessage().Contains(fragment, StringComparison.Ordinal));
|
|
|
|
/// <inheritdoc />
|
|
public void Emit(LogEvent logEvent) => Events.Enqueue(logEvent);
|
|
|
|
/// <inheritdoc />
|
|
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
|
|
|
/// <inheritdoc />
|
|
public bool IsEnabled(LogLevel logLevel) => true;
|
|
|
|
/// <inheritdoc />
|
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
|
Func<TState, Exception?, string> formatter)
|
|
=> _serilog.Write(LogEventLevel.Information, exception, "{Line}", formatter(state, exception));
|
|
}
|