feat(logging): stamp ExecutionId/InstanceName/ScriptName on the Serilog LogContext per run (ScadaBridge#56)
The site script and alarm on-trigger launchers push the run identity onto LogContext for the run's lifetime and the host now enables Enrich.FromLogContext(), so log lines written on the run's own async call chain carry the id. Bounded part only: LogContext does not cross an Akka message hop, so Instance Actor/DCL/S&F lines and a site_events execution-id column remain open (documented in the step-logs design doc section 9). Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -92,6 +92,10 @@ public static class LoggerConfigurationFactory
|
||||
.Enrich.WithProperty("SiteId", siteId)
|
||||
.Enrich.WithProperty("NodeHostname", nodeHostname)
|
||||
.Enrich.WithProperty("NodeRole", nodeRole)
|
||||
// ScadaBridge#56: surface the ExecutionId/InstanceName/ScriptName the site script
|
||||
// launcher pushes onto LogContext, so lines written on a run's own call chain carry
|
||||
// the run's identity. See SiteRuntime ScriptRunLogScope for the (bounded) scope.
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.With(new TraceContextEnricher());
|
||||
}
|
||||
|
||||
|
||||
@@ -288,6 +288,7 @@ internal static class ScriptRunLauncher
|
||||
var workerSlot = ScriptExecutionScheduler.CurrentWorkerSlot;
|
||||
var workerRunStamp = workerSlot is { } slot ? executionScheduler.CurrentRunStamp(slot) : 0L;
|
||||
|
||||
IDisposable? logScope = null;
|
||||
var completed = 0;
|
||||
ArmStuckScriptWatchdog(
|
||||
cts, options, logger, siteEventLogger, executionScheduler,
|
||||
@@ -412,6 +413,9 @@ internal static class ScriptRunLauncher
|
||||
// Script file access (design 2026-09-28): the Files global's host reader.
|
||||
fileReader: fileReader);
|
||||
|
||||
// ScadaBridge#56: stamp the run identity on the LogContext for the rest of the run.
|
||||
logScope = ScriptRunLogScope.Push(context.ExecutionId, instanceName, scriptName);
|
||||
|
||||
var globals = new ScriptGlobals
|
||||
{
|
||||
Instance = context,
|
||||
@@ -533,6 +537,7 @@ internal static class ScriptRunLauncher
|
||||
// Mark the body finished so the stuck-script watchdog (registered on
|
||||
// cts.Token above) treats a timely cancellation as NOT stuck.
|
||||
Interlocked.Exchange(ref completed, 1);
|
||||
logScope?.Dispose();
|
||||
// Dispose the DI scope (and scoped services) after script execution completes
|
||||
serviceScope?.Dispose();
|
||||
cts.Dispose();
|
||||
@@ -709,6 +714,7 @@ internal static class ScriptRunLauncher
|
||||
var workerSlot = ScriptExecutionScheduler.CurrentWorkerSlot;
|
||||
var workerRunStamp = workerSlot is { } slot ? executionScheduler.CurrentRunStamp(slot) : 0L;
|
||||
|
||||
IDisposable? logScope = null;
|
||||
var completed = 0;
|
||||
ArmStuckScriptWatchdog(
|
||||
cts, options, logger, siteEventLogger, executionScheduler,
|
||||
@@ -766,6 +772,8 @@ internal static class ScriptRunLauncher
|
||||
// Script file access (design 2026-09-28): the Files global's host reader.
|
||||
fileReader: services?.GetService<IScriptFileReader>());
|
||||
|
||||
logScope = ScriptRunLogScope.Push(context.ExecutionId, instanceName, scriptName);
|
||||
|
||||
var globals = new ScriptGlobals
|
||||
{
|
||||
Instance = context,
|
||||
@@ -811,6 +819,7 @@ internal static class ScriptRunLauncher
|
||||
// Mark the body finished so the stuck-script watchdog (registered on
|
||||
// cts.Token above) treats a timely cancellation as NOT stuck.
|
||||
Interlocked.Exchange(ref completed, 1);
|
||||
logScope?.Dispose();
|
||||
cts.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using Serilog.Context;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// Stamps a script/alarm run's identity onto the Serilog <c>LogContext</c> for the lifetime of the
|
||||
/// run (ScadaBridge#56), so every log line written on the run's async flow - the script body's own
|
||||
/// callees (external-system client, database gateway, store-and-forward enqueue, ...) - carries
|
||||
/// <c>ExecutionId</c>, <c>InstanceName</c> and <c>ScriptName</c> once the host enables
|
||||
/// <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.
|
||||
/// </remarks>
|
||||
internal static class ScriptRunLogScope
|
||||
{
|
||||
/// <summary>Property name carrying the run's <c>ExecutionId</c>.</summary>
|
||||
internal const string ExecutionIdProperty = "ExecutionId";
|
||||
|
||||
/// <summary>Property name carrying the instance's unique name.</summary>
|
||||
internal const string InstanceNameProperty = "InstanceName";
|
||||
|
||||
/// <summary>Property name carrying the script (or alarm source) name.</summary>
|
||||
internal const string ScriptNameProperty = "ScriptName";
|
||||
|
||||
/// <summary>Pushes the run identity onto the <c>LogContext</c>; dispose to pop it.</summary>
|
||||
/// <param name="executionId">The run's execution id.</param>
|
||||
/// <param name="instanceName">The owning instance's unique name.</param>
|
||||
/// <param name="scriptName">The script (or alarm source) name.</param>
|
||||
/// <returns>A disposable that removes the three properties again.</returns>
|
||||
internal static IDisposable Push(Guid executionId, string instanceName, string scriptName)
|
||||
{
|
||||
var a = LogContext.PushProperty(ExecutionIdProperty, executionId);
|
||||
var b = LogContext.PushProperty(InstanceNameProperty, instanceName);
|
||||
var c = LogContext.PushProperty(ScriptNameProperty, scriptName);
|
||||
return new Scope(a, b, c);
|
||||
}
|
||||
|
||||
private sealed class Scope(IDisposable a, IDisposable b, IDisposable c) : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
// Reverse order: LogContext is a stack.
|
||||
c.Dispose();
|
||||
b.Dispose();
|
||||
a.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="Serilog" />
|
||||
<PackageReference Include="ZB.MOM.WW.Configuration" />
|
||||
<PackageReference Include="ZB.MOM.WW.LocalDb" />
|
||||
<PackageReference Include="ZB.MOM.WW.Secrets.Abstractions" />
|
||||
|
||||
Reference in New Issue
Block a user