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();
}
}
}