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:
Joseph Doherty
2026-11-10 05:28:20 -05:00
parent 1ccd6cfe33
commit edd0ca672f
28 changed files with 905 additions and 33 deletions
@@ -1,3 +1,5 @@
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
/// <summary>
@@ -16,13 +18,21 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
/// <param name="TriggerTagPath">Optional device tag path of a flag written AFTER the batch succeeds; null to skip.</param>
/// <param name="TriggerValue">Value to write to <paramref name="TriggerTagPath"/> (ignored when it is null/empty).</param>
/// <param name="Timestamp">When the request was issued (UTC).</param>
/// <param name="SourceExecutionId">ScadaBridge#56: the execution issuing this request (a site script run), so the
/// actors that serve it stamp the id onto their log lines. Additive and nullable; never serialized as anything
/// but an id.</param>
public record WriteTagBatchRequest(
string CorrelationId,
string ConnectionName,
IReadOnlyDictionary<string, object?> Values, // device tagPath -> value
string? TriggerTagPath, // optional flag written AFTER the batch
object? TriggerValue,
DateTimeOffset Timestamp);
DateTimeOffset Timestamp,
Guid? SourceExecutionId = null) : IExecutionCorrelated
{
/// <summary>ScadaBridge#56: the execution this message serves, for log enrichment.</summary>
Guid? IExecutionCorrelated.CorrelatedExecutionId => SourceExecutionId;
}
/// <summary>
/// Response for a <see cref="WriteTagBatchRequest"/>. <see cref="Success"/> is true only
@@ -1,3 +1,5 @@
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
/// <summary>
@@ -9,7 +11,12 @@ public record WriteTagRequest(
string ConnectionName,
string TagPath,
object? Value,
DateTimeOffset Timestamp);
DateTimeOffset Timestamp,
Guid? SourceExecutionId = null) : IExecutionCorrelated
{
/// <summary>ScadaBridge#56: the execution this message serves, for log enrichment.</summary>
Guid? IExecutionCorrelated.CorrelatedExecutionId => SourceExecutionId;
}
/// <summary>
/// Response for a device tag write operation.
@@ -1,4 +1,5 @@
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi;
@@ -22,7 +23,11 @@ public record RouteToCallRequest(
IReadOnlyDictionary<string, object?>? Parameters,
DateTimeOffset Timestamp,
Guid? ParentExecutionId = null,
bool Trace = false);
bool Trace = false) : IExecutionCorrelated
{
/// <summary>ScadaBridge#56: the execution this message serves, for log enrichment (the inbound request).</summary>
Guid? IExecutionCorrelated.CorrelatedExecutionId => ParentExecutionId;
}
/// <summary>
/// Response from a Route.To() call.
@@ -57,7 +62,11 @@ public record RouteToGetAttributesRequest(
string InstanceUniqueName,
IReadOnlyList<string> AttributeNames,
DateTimeOffset Timestamp,
Guid? ParentExecutionId = null);
Guid? ParentExecutionId = null) : IExecutionCorrelated
{
/// <summary>ScadaBridge#56: the execution this message serves, for log enrichment (the inbound request).</summary>
Guid? IExecutionCorrelated.CorrelatedExecutionId => ParentExecutionId;
}
/// <summary>
/// Response containing attribute values from a remote instance.
@@ -87,7 +96,11 @@ public record RouteToSetAttributesRequest(
IReadOnlyDictionary<string, string> AttributeValues,
DateTimeOffset Timestamp,
Guid? ParentExecutionId = null,
bool Trace = false);
bool Trace = false) : IExecutionCorrelated
{
/// <summary>ScadaBridge#56: the execution this message serves, for log enrichment (the inbound request).</summary>
Guid? IExecutionCorrelated.CorrelatedExecutionId => ParentExecutionId;
}
/// <summary>
/// Response confirming attribute writes on a remote instance.
@@ -128,7 +141,11 @@ public record RouteToWaitForAttributeRequest(
TimeSpan Timeout,
DateTimeOffset Timestamp,
Guid? ParentExecutionId = null,
bool RequireGoodQuality = false);
bool RequireGoodQuality = false) : IExecutionCorrelated
{
/// <summary>ScadaBridge#56: the execution this message serves, for log enrichment (the inbound request).</summary>
Guid? IExecutionCorrelated.CorrelatedExecutionId => ParentExecutionId;
}
/// <summary>
/// Response from a remote attribute wait. <see cref="Success"/>/<see cref="ErrorMessage"/>
@@ -1,14 +1,23 @@
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
/// <summary>
/// Request to get the current value of an attribute from an Instance Actor.
/// Uses the Ask pattern for system boundaries; Tell pattern is preferred for hot-path.
/// </summary>
/// <remarks><c>SourceExecutionId</c> (ScadaBridge#56): the site script run issuing the read, so the Instance Actor
/// stamps it onto any line it logs while serving it. Additive and nullable.</remarks>
public record GetAttributeRequest(
string CorrelationId,
string InstanceUniqueName,
string AttributeName,
DateTimeOffset Timestamp);
DateTimeOffset Timestamp,
Guid? SourceExecutionId = null) : IExecutionCorrelated
{
/// <summary>ScadaBridge#56: the execution this message serves, for log enrichment.</summary>
Guid? IExecutionCorrelated.CorrelatedExecutionId => SourceExecutionId;
}
/// <summary>
/// Response containing the current value and quality of an attribute.
@@ -1,3 +1,5 @@
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
/// <summary>
@@ -27,7 +29,11 @@ public record SetStaticAttributeCommand(
string Value,
DateTimeOffset Timestamp,
Guid? SourceExecutionId = null,
bool Trace = false);
bool Trace = false) : IExecutionCorrelated
{
/// <summary>ScadaBridge#56: the execution this message serves, for log enrichment.</summary>
Guid? IExecutionCorrelated.CorrelatedExecutionId => SourceExecutionId;
}
/// <summary>
/// Response confirming that a static attribute was set.
@@ -1,3 +1,5 @@
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
/// <summary>
@@ -37,6 +39,9 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
/// the match tests the value only). Trailing/defaulted so existing positional
/// constructions compile unchanged.
/// </param>
/// <param name="SourceExecutionId">ScadaBridge#56: the execution issuing this request (a site script run), so the
/// actors that serve it stamp the id onto their log lines. Additive and nullable; never serialized as anything
/// but an id.</param>
public record WaitForAttributeRequest(
string CorrelationId,
string InstanceName,
@@ -45,7 +50,12 @@ public record WaitForAttributeRequest(
Func<object?, bool>? Predicate,
TimeSpan Timeout,
DateTimeOffset OccurredAtUtc,
bool RequireGoodQuality = false);
bool RequireGoodQuality = false,
Guid? SourceExecutionId = null) : IExecutionCorrelated
{
/// <summary>ScadaBridge#56: the execution this message serves, for log enrichment.</summary>
Guid? IExecutionCorrelated.CorrelatedExecutionId => SourceExecutionId;
}
/// <summary>
/// Reply to a <see cref="WaitForAttributeRequest"/>. Exactly one of
@@ -1,3 +1,5 @@
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
/// <summary>
@@ -22,13 +24,21 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
/// <param name="TriggerAttribute">Optional canonical attribute name of a flag written AFTER the batch; null to skip.</param>
/// <param name="TriggerEncodedValue">Codec-encoded value for <paramref name="TriggerAttribute"/>.</param>
/// <param name="OccurredAtUtc">When the request was issued (UTC).</param>
/// <param name="SourceExecutionId">ScadaBridge#56: the execution issuing this request (a site script run), so the
/// actors that serve it stamp the id onto their log lines. Additive and nullable; never serialized as anything
/// but an id.</param>
public record WriteAttributeBatchRequest(
string CorrelationId,
string InstanceName,
IReadOnlyDictionary<string, string?> AttributeEncodedValues, // canonical attr name -> codec-encoded value
string? TriggerAttribute,
string? TriggerEncodedValue,
DateTimeOffset OccurredAtUtc);
DateTimeOffset OccurredAtUtc,
Guid? SourceExecutionId = null) : IExecutionCorrelated
{
/// <summary>ScadaBridge#56: the execution this message serves, for log enrichment.</summary>
Guid? IExecutionCorrelated.CorrelatedExecutionId => SourceExecutionId;
}
/// <summary>
/// Reply to a <see cref="WriteAttributeBatchRequest"/>. <see cref="Success"/> is true only
@@ -1,3 +1,5 @@
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.ScriptExecution;
/// <param name="ParentExecutionId">
@@ -16,4 +18,8 @@ public record ScriptCallRequest(
int CurrentCallDepth,
string CorrelationId,
Guid? ParentExecutionId = null,
bool Trace = false);
bool Trace = false) : IExecutionCorrelated
{
/// <summary>ScadaBridge#56: the execution this message serves, for log enrichment (the calling run).</summary>
Guid? IExecutionCorrelated.CorrelatedExecutionId => ParentExecutionId;
}
@@ -1,3 +1,5 @@
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
/// <summary>
@@ -41,4 +43,12 @@ public record AttributeValueChanged(
DateTimeOffset Timestamp,
Guid? SourceExecutionId = null,
bool Trace = false,
bool SourceExecutionInferred = false) : ISiteStreamEvent;
bool SourceExecutionInferred = false) : ISiteStreamEvent, IExecutionCorrelated
{
/// <summary>
/// ScadaBridge#56: the writing execution, for log enrichment of the actors that handle the change.
/// An INFERRED writer (a device echo matched to a traced write by the trace arm) is a heuristic
/// linkage, so it is not stamped onto log lines.
/// </summary>
Guid? IExecutionCorrelated.CorrelatedExecutionId => SourceExecutionInferred ? null : SourceExecutionId;
}
@@ -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;
}
@@ -0,0 +1,17 @@
namespace ZB.MOM.WW.ScadaBridge.Commons.Observability;
/// <summary>
/// A message sent on behalf of one execution (a site script/alarm run, or an inbound API
/// method request) that names that execution, so the actor handling it can stamp the id onto
/// its log lines (ScadaBridge#56). See <see cref="ExecutionLogContext"/>.
/// </summary>
/// <remarks>
/// Implemented explicitly on the message records, mapped onto the field the record already
/// carries (<c>SourceExecutionId</c> / <c>ParentExecutionId</c>), so it adds no serialized
/// member and no wire change. Only an identifier ever rides here — never a value or a secret.
/// </remarks>
public interface IExecutionCorrelated
{
/// <summary>The execution this message serves, or null when it serves none.</summary>
Guid? CorrelatedExecutionId { get; }
}
@@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" />
<PackageReference Include="ZB.MOM.WW.Audit" />
</ItemGroup>
@@ -328,6 +328,26 @@ public class DataConnectionActor : UntypedActor, IWithTimers
_tagResolutionInterval = _options.TagResolutionRetryInterval;
}
/// <summary>
/// ScadaBridge#56: while handling a message sent on behalf of an execution
/// (<see cref="IExecutionCorrelated"/>), every line this actor logs - and every line logged by
/// a continuation it starts - carries that execution's <c>ExecutionId</c>. A message that names
/// no execution costs one type test.
/// </summary>
/// <remarks>
/// Enrichment reaches lines written through a Serilog-backed <c>ILogger</c> - here, the protocol
/// adapter's own logging on the write path, whose <c>WriteAsync</c> starts inside this scope. This
/// actor's own Akka <c>_log</c> lines go through Akka's logger, not Serilog, and are not enriched.
/// </remarks>
/// <param name="receive">The behaviour that handles the message.</param>
/// <param name="message">The message.</param>
/// <returns>Whether the message was handled.</returns>
protected override bool AroundReceive(Receive receive, object message)
{
using (ExecutionLogContext.PushFor(message))
return base.AroundReceive(receive, message);
}
/// <inheritdoc />
protected override void PreStart()
{
@@ -233,6 +233,26 @@ public class DataConnectionManagerActor : ReceiveActor
_log.Warning("No connection actor for {0} during alarm unsubscribe", request.ConnectionName);
}
/// <summary>
/// ScadaBridge#56: while handling a message sent on behalf of an execution
/// (<see cref="IExecutionCorrelated"/>), every line this actor logs - and every line logged by
/// a continuation it starts - carries that execution's <c>ExecutionId</c>. A message that names
/// no execution costs one type test.
/// </summary>
/// <remarks>
/// Enrichment reaches lines written through a Serilog-backed <c>ILogger</c> - here, the protocol
/// adapter's own logging on the write path, whose <c>WriteAsync</c> starts inside this scope. This
/// actor's own Akka <c>_log</c> lines go through Akka's logger, not Serilog, and are not enriched.
/// </remarks>
/// <param name="receive">The behaviour that handles the message.</param>
/// <param name="message">The message.</param>
/// <returns>Whether the message was handled.</returns>
protected override bool AroundReceive(Receive receive, object message)
{
using (ExecutionLogContext.PushFor(message))
return base.AroundReceive(receive, message);
}
private void HandleRouteWrite(WriteTagRequest request)
{
if (_connectionActors.TryGetValue(request.ConnectionName, out var actor))
@@ -442,6 +442,11 @@ public class InboundScriptExecutor
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, timeoutCts.Token);
// ScadaBridge#56: every line logged on this request's async flow (the executor, the
// Database helper, route/communication callees) carries the request's ExecutionId; the
// site actors that serve a routed call re-stamp it from RouteTo*Request.ParentExecutionId.
using var executionLogScope = ExecutionLogContext.Push(parentExecutionId);
// IpsenMES MoveIn: expose the scoped, parameterized Database helper to the script
// (reads + writes). Resolve the gateway from a fresh DI scope
// (declared outside the try so it lives until after the script handler runs and is
@@ -1,3 +1,4 @@
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using Akka.Actor;
using Microsoft.CodeAnalysis.Scripting;
@@ -262,6 +263,21 @@ public class AlarmActor : ReceiveActor
Receive<ExpressionEvalFailed>(HandleExpressionEvalFailed);
}
/// <summary>
/// ScadaBridge#56: while handling a message sent on behalf of an execution
/// (<see cref="IExecutionCorrelated"/>), every line this actor logs - and every line logged by
/// a continuation it starts - carries that execution's <c>ExecutionId</c>. A message that names
/// no execution costs one type test.
/// </summary>
/// <param name="receive">The behaviour that handles the message.</param>
/// <param name="message">The message.</param>
/// <returns>Whether the message was handled.</returns>
protected override bool AroundReceive(Receive receive, object message)
{
using (ExecutionLogContext.PushFor(message))
return base.AroundReceive(receive, message);
}
/// <inheritdoc />
protected override void PreStart()
{
@@ -1,3 +1,4 @@
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using Akka.Actor;
using Akka.Cluster;
@@ -571,6 +572,21 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
});
}
/// <summary>
/// ScadaBridge#56: while handling a message sent on behalf of an execution
/// (<see cref="IExecutionCorrelated"/>), every line this actor logs - and every line logged by
/// a continuation it starts - carries that execution's <c>ExecutionId</c>. A message that names
/// no execution costs one type test.
/// </summary>
/// <param name="receive">The behaviour that handles the message.</param>
/// <param name="message">The message.</param>
/// <returns>Whether the message was handled.</returns>
protected override bool AroundReceive(Receive receive, object message)
{
using (ExecutionLogContext.PushFor(message))
return base.AroundReceive(receive, message);
}
/// <inheritdoc />
protected override void PreStart()
{
@@ -3509,7 +3525,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
var inner = new WaitForAttributeRequest(
request.CorrelationId, request.InstanceUniqueName, request.AttributeName,
request.TargetValueEncoded, null, request.Timeout, DateTimeOffset.UtcNow,
request.RequireGoodQuality);
request.RequireGoodQuality,
// ScadaBridge#56: the Instance Actor logs under the inbound request's id.
SourceExecutionId: request.ParentExecutionId);
// Ask bounded by the WAIT timeout + slack — NOT a fixed 30s (the wait legitimately blocks up to request.Timeout).
instanceActor.Ask<WaitForAttributeResponse>(inner, request.Timeout + TimeSpan.FromSeconds(5))
@@ -1,3 +1,4 @@
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using Akka.Actor;
using Microsoft.CodeAnalysis.Scripting;
@@ -450,6 +451,21 @@ public class InstanceActor : ReceiveActor
Receive<FlushStaticOverrides>(_ => FlushDirtyStaticOverrides());
}
/// <summary>
/// ScadaBridge#56: while handling a message sent on behalf of an execution
/// (<see cref="IExecutionCorrelated"/>), every line this actor logs - and every line logged by
/// a continuation it starts - carries that execution's <c>ExecutionId</c>. A message that names
/// no execution costs one type test.
/// </summary>
/// <param name="receive">The behaviour that handles the message.</param>
/// <param name="message">The message.</param>
/// <returns>Whether the message was handled.</returns>
protected override bool AroundReceive(Receive receive, object message)
{
using (ExecutionLogContext.PushFor(message))
return base.AroundReceive(receive, message);
}
/// <inheritdoc />
protected override void PreStart()
{
@@ -608,7 +624,11 @@ public class InstanceActor : ReceiveActor
var caller = Sender;
var outcomes = new Task<SetStaticAttributeResponse>[request.Writes.Count];
for (var i = 0; i < outcomes.Length; i++)
outcomes[i] = ApplySetAttribute(request.Writes[i]);
{
// ScadaBridge#56: the batch envelope names no execution; each write names its own.
using (ExecutionLogContext.PushFor(request.Writes[i]))
outcomes[i] = ApplySetAttribute(request.Writes[i]);
}
if (outcomes.All(t => t.IsCompleted))
{
@@ -897,7 +917,9 @@ public class InstanceActor : ReceiveActor
resolved.Attribute.BoundDataConnectionName!,
resolved.Attribute.DataSourceReference!,
writeValue,
DateTimeOffset.UtcNow);
DateTimeOffset.UtcNow,
// ScadaBridge#56: the DCL (and its adapter) log under the writing execution's id.
SourceExecutionId: command.SourceExecutionId);
// Flow Debugger trace arm (design §5.3): only a TRACED write with a known writer arms;
// the next device update is marked as inferred in HandleTagValueUpdate. Untraced writes
@@ -1036,7 +1058,9 @@ public class InstanceActor : ReceiveActor
}
var dclReq = new WriteTagBatchRequest(
cid, connName!, values, triggerPath, triggerVal, DateTimeOffset.UtcNow);
cid, connName!, values, triggerPath, triggerVal, DateTimeOffset.UtcNow,
// ScadaBridge#56: the DCL (and its adapter) log under the writing execution's id.
SourceExecutionId: request.SourceExecutionId);
// Ask the DCL and pipe the batch outcome back to the original caller. The DCL
// bounds its own write with WriteTimeout; this Ask is bounded a bit longer so a
@@ -1,3 +1,4 @@
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using Akka.Actor;
using Microsoft.CodeAnalysis.Scripting;
@@ -228,6 +229,21 @@ public class ScriptActor : ReceiveActor, IWithTimers
Receive<ExpressionEvalFailed>(HandleExpressionEvalFailed);
}
/// <summary>
/// ScadaBridge#56: while handling a message sent on behalf of an execution
/// (<see cref="IExecutionCorrelated"/>), every line this actor logs - and every line logged by
/// a continuation it starts - carries that execution's <c>ExecutionId</c>. A message that names
/// no execution costs one type test.
/// </summary>
/// <param name="receive">The behaviour that handles the message.</param>
/// <param name="message">The message.</param>
/// <returns>Whether the message was handled.</returns>
protected override bool AroundReceive(Receive receive, object message)
{
using (ExecutionLogContext.PushFor(message))
return base.AroundReceive(receive, message);
}
/// <inheritdoc />
protected override void PreStart()
{
@@ -1,3 +1,4 @@
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
using Serilog.Context;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
@@ -10,17 +11,15 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
/// <c>Enrich.FromLogContext()</c> (<c>LoggerConfigurationFactory</c>).
/// </summary>
/// <remarks>
/// <b>Bounded by design.</b> <c>LogContext</c> is an <c>AsyncLocal</c>: it follows the run's own
/// call chain but NOT an Akka message hop. A line logged by the Instance Actor, DCL or alarm
/// actors while handling a message sent BY the run runs on that actor's own context and does not
/// carry the id. Closing that needs the id on the message envelope plus a per-message push in each
/// actor, and a queryable sink (an execution-id column on the replicated <c>site_events</c> table);
/// both are recorded as the remaining work on the issue.
/// <c>LogContext</c> is an <c>AsyncLocal</c>: it follows the run's own call chain but NOT an Akka
/// message hop. The messages the run sends carry its id instead (<see cref="IExecutionCorrelated"/>)
/// and the Instance, Script, Alarm, Deployment Manager and DCL actors re-push it per message
/// (<see cref="ExecutionLogContext.PushFor"/>).
/// </remarks>
internal static class ScriptRunLogScope
{
/// <summary>Property name carrying the run's <c>ExecutionId</c>.</summary>
internal const string ExecutionIdProperty = "ExecutionId";
internal const string ExecutionIdProperty = ExecutionLogContext.ExecutionIdProperty;
/// <summary>Property name carrying the instance's unique name.</summary>
internal const string InstanceNameProperty = "InstanceName";
@@ -719,7 +719,9 @@ public class ScriptRuntimeContext
{
var correlationId = Guid.NewGuid().ToString();
var request = new GetAttributeRequest(
correlationId, _instanceName, attributeName, DateTimeOffset.UtcNow);
correlationId, _instanceName, attributeName, DateTimeOffset.UtcNow,
// ScadaBridge#56: the Instance Actor logs under this run's id while serving it.
SourceExecutionId: _executionId);
var response = await _instanceActor.Ask<GetAttributeResponse>(request, _askTimeout).ConfigureAwait(false);
@@ -823,7 +825,9 @@ public class ScriptRuntimeContext
var cid = Guid.NewGuid().ToString();
var req = new WaitForAttributeRequest(
cid, _instanceName, name, targetValueEncoded, predicate, timeout, DateTimeOffset.UtcNow,
requireGoodQuality);
requireGoodQuality,
// ScadaBridge#56: the Instance Actor logs under this run's id while serving it.
SourceExecutionId: _executionId);
var startedUtc = DateTime.UtcNow;
var sw = _isTraced ? Stopwatch.StartNew() : null;
@@ -1126,7 +1130,9 @@ public class ScriptRuntimeContext
{
var cid = Guid.NewGuid().ToString();
var batchReq = new WriteAttributeBatchRequest(
cid, _instanceName, encodedValues, triggerAttr, triggerEncoded, DateTimeOffset.UtcNow);
cid, _instanceName, encodedValues, triggerAttr, triggerEncoded, DateTimeOffset.UtcNow,
// ScadaBridge#56: rides on to the DCL batch write so both actors log under this run's id.
SourceExecutionId: _executionId);
// 35s: the InstanceActor's DCL Ask is internally bounded at 30s, so allow a small
// margin so the DCL's own typed failure/timeout reply is the one we observe rather