using Serilog.Context; namespace ZB.MOM.WW.ScadaBridge.Commons.Observability; /// /// Stamps an execution id onto the Serilog LogContext (ScadaBridge#56), so every line /// logged inside the scope - through Serilog directly or through a Microsoft /// ILogger backed by Serilog (UseSerilog) - carries an ExecutionId /// property once the host enables Enrich.FromLogContext(). /// /// /// Why per message. LogContext is an AsyncLocal: 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: /// /// protected override bool AroundReceive(Receive receive, object message) /// { /// using (ExecutionLogContext.PushFor(message)) /// return base.AroundReceive(receive, message); /// } /// /// Continuations the handler starts (ContinueWith, PipeTo, an adapter call) /// capture the context at creation, so their lines carry the id too. /// Cost. A message that is not , or names no /// execution, costs one type test and allocates nothing. /// Nesting. 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). /// public static class ExecutionLogContext { /// The log-event property carrying the execution id. public const string ExecutionIdProperty = "ExecutionId"; /// /// The innermost execution id pushed on this async flow, or null. Mirrors the /// on the Serilog LogContext, which cannot be read back. /// /// /// ScadaBridge#120: Akka's ILoggingAdapter hands each line to a logger actor on another /// thread, where this flow's LogContext is gone. An adapter that captures this value when /// the line is logged lets the logger actor re-push it when it writes the line to Serilog. /// public static Guid? Current => CurrentId.Value; private static readonly AsyncLocal CurrentId = new(); /// Pushes as . /// The execution id; null pushes nothing. /// A disposable that pops the property, or null when nothing was pushed. public static IDisposable? Push(Guid? executionId) { if (executionId is not { } id) { return null; } var previous = CurrentId.Value; var property = LogContext.PushProperty(ExecutionIdProperty, id); CurrentId.Value = id; return new Scope(property, previous); } /// Pushes the execution a message serves, when it names one. /// Any actor message. /// A disposable that pops the property, or null when the message names no execution. public static IDisposable? PushFor(object? message) => message is IExecutionCorrelated correlated ? Push(correlated.CorrelatedExecutionId) : null; /// Pops the pushed property and restores the previous . private sealed class Scope(IDisposable property, Guid? previous) : IDisposable { private int _disposed; public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) != 0) { return; } CurrentId.Value = previous; property.Dispose(); } } }