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
106 lines
5.2 KiB
C#
106 lines
5.2 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// ScadaBridge#56: which messages name the execution they serve, and what pushing one does to a
|
|
/// LogContext-enriched logger.
|
|
/// </summary>
|
|
public class ExecutionLogContextTests
|
|
{
|
|
private static readonly DateTimeOffset Now = DateTimeOffset.UtcNow;
|
|
private static readonly Guid Id = Guid.NewGuid();
|
|
|
|
private sealed class CaptureSink : ILogEventSink
|
|
{
|
|
public ConcurrentQueue<LogEvent> Events { get; } = new();
|
|
public void Emit(LogEvent logEvent) => Events.Enqueue(logEvent);
|
|
}
|
|
|
|
/// <summary>Every correlated message, built with <see cref="Id"/> on its execution field.</summary>
|
|
/// <returns>The messages.</returns>
|
|
public static TheoryData<object> 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<string, string>(), 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<string, string?>(), null, null, Now, SourceExecutionId: Id),
|
|
new WriteTagRequest("c", "conn", "tag", 1, Now, SourceExecutionId: Id),
|
|
new WriteTagBatchRequest("c", "conn", new Dictionary<string, object?>(), null, null, Now, SourceExecutionId: Id),
|
|
};
|
|
|
|
/// <summary>Each correlated message reports the execution id it was built with.</summary>
|
|
/// <param name="message">The message.</param>
|
|
[Theory]
|
|
[MemberData(nameof(CorrelatedMessages))]
|
|
public void CorrelatedMessage_ReportsItsExecutionId(object message)
|
|
=> Assert.Equal(Id, Assert.IsAssignableFrom<IExecutionCorrelated>(message).CorrelatedExecutionId);
|
|
|
|
/// <summary>
|
|
/// The correlation member is an explicit interface implementation: it adds nothing to the
|
|
/// serialized shape, so the wire is unchanged.
|
|
/// </summary>
|
|
/// <param name="message">The message.</param>
|
|
[Theory]
|
|
[MemberData(nameof(CorrelatedMessages))]
|
|
public void CorrelatedMessage_SerializesNoExtraMember(object message)
|
|
=> Assert.DoesNotContain(nameof(IExecutionCorrelated.CorrelatedExecutionId), JsonConvert.SerializeObject(message));
|
|
|
|
/// <summary>An inferred writer (the trace arm's device-echo heuristic) is not stamped onto log lines.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>A message that names no execution - or is not correlated at all - pushes nothing.</summary>
|
|
[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));
|
|
}
|
|
|
|
/// <summary>Lines inside the scope carry the id, lines after it do not, and a nested push wins.</summary>
|
|
[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));
|
|
}
|
|
}
|