using Serilog.Context; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Observability; /// /// Convenience wrapper around Serilog — attaches the set of /// structured properties a capability call should carry (DriverInstanceId, DriverType, /// CapabilityName, CorrelationId). Callers wrap their call-site body in a using /// block; inner Log.Information / Log.Warning calls emit the context /// automatically via the Serilog enricher chain. /// /// /// Per docs/v2/implementation/phase-6-1-resilience-and-observability.md §Stream C.2. /// The correlation ID should be the OPC UA RequestHeader.RequestHandle when in-flight; /// otherwise a short random GUID. Callers supply whichever is available. /// public static class LogContextEnricher { /// Attach the capability-call property set. Dispose the returned scope to pop. public static IDisposable Push(string driverInstanceId, string driverType, DriverCapability capability, string correlationId) { ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId); ArgumentException.ThrowIfNullOrWhiteSpace(driverType); ArgumentException.ThrowIfNullOrWhiteSpace(correlationId); var a = LogContext.PushProperty("DriverInstanceId", driverInstanceId); var b = LogContext.PushProperty("DriverType", driverType); var c = LogContext.PushProperty("CapabilityName", capability.ToString()); var d = LogContext.PushProperty("CorrelationId", correlationId); return new CompositeScope(a, b, c, d); } /// /// Generate a short correlation ID when no OPC UA RequestHandle is available. /// 12-hex-char slice of a GUID — long enough for log correlation, short enough to /// scan visually. /// public static string NewCorrelationId() => Guid.NewGuid().ToString("N")[..12]; private sealed class CompositeScope : IDisposable { private readonly IDisposable[] _inner; public CompositeScope(params IDisposable[] inner) => _inner = inner; public void Dispose() { // Reverse-order disposal matches Serilog's stack semantics. for (var i = _inner.Length - 1; i >= 0; i--) _inner[i].Dispose(); } } }