feat(logging): carry ExecutionId across actor hops onto other actors' log lines (ScadaBridge#56)
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
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
using Serilog.Context;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Observability;
|
||||
|
||||
/// <summary>
|
||||
/// Stamps an execution id onto the Serilog <c>LogContext</c> (ScadaBridge#56), so every line
|
||||
/// logged inside the scope - through Serilog directly or through a Microsoft
|
||||
/// <c>ILogger</c> backed by Serilog (<c>UseSerilog</c>) - carries an <c>ExecutionId</c>
|
||||
/// property once the host enables <c>Enrich.FromLogContext()</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Why per message.</b> <c>LogContext</c> is an <c>AsyncLocal</c>: it follows one
|
||||
/// async call chain but does not cross an Akka message hop (the dispatcher does not flow the
|
||||
/// sender's execution context into the receiver). An actor that serves an execution therefore
|
||||
/// re-establishes the id from the message itself, for the duration of that one message:
|
||||
/// <code>
|
||||
/// protected override bool AroundReceive(Receive receive, object message)
|
||||
/// {
|
||||
/// using (ExecutionLogContext.PushFor(message))
|
||||
/// return base.AroundReceive(receive, message);
|
||||
/// }
|
||||
/// </code>
|
||||
/// Continuations the handler starts (<c>ContinueWith</c>, <c>PipeTo</c>, an adapter call)
|
||||
/// capture the context at creation, so their lines carry the id too.</para>
|
||||
/// <para><b>Cost.</b> A message that is not <see cref="IExecutionCorrelated"/>, or names no
|
||||
/// execution, costs one type test and allocates nothing.</para>
|
||||
/// <para><b>Nesting.</b> A later push of the same property wins, so a child run started while
|
||||
/// serving its parent's message logs under its OWN id (the launcher pushes it).</para>
|
||||
/// </remarks>
|
||||
public static class ExecutionLogContext
|
||||
{
|
||||
/// <summary>The log-event property carrying the execution id.</summary>
|
||||
public const string ExecutionIdProperty = "ExecutionId";
|
||||
|
||||
/// <summary>Pushes <paramref name="executionId"/> as <see cref="ExecutionIdProperty"/>.</summary>
|
||||
/// <param name="executionId">The execution id; null pushes nothing.</param>
|
||||
/// <returns>A disposable that pops the property, or null when nothing was pushed.</returns>
|
||||
public static IDisposable? Push(Guid? executionId)
|
||||
=> executionId is { } id ? LogContext.PushProperty(ExecutionIdProperty, id) : null;
|
||||
|
||||
/// <summary>Pushes the execution a message serves, when it names one.</summary>
|
||||
/// <param name="message">Any actor message.</param>
|
||||
/// <returns>A disposable that pops the property, or null when the message names no execution.</returns>
|
||||
public static IDisposable? PushFor(object? message)
|
||||
=> message is IExecutionCorrelated correlated ? Push(correlated.CorrelatedExecutionId) : null;
|
||||
}
|
||||
Reference in New Issue
Block a user