refactor(logging): correlation scope + redaction on shared ILogRedactor seam

Move the per-request correlation context and secret redaction off the MEL
mechanism onto the Serilog primitives the shared bootstrap consumes.

- GatewayRequestLoggingMiddlewareExtensions now pushes the correlation
  properties (SessionId / WorkerProcessId / CorrelationId / CommandMethod /
  ClientIdentity) via Serilog LogContext.PushProperty for the request lifetime
  and pops them on completion, replacing MEL ILogger.BeginScope. Header parsing
  and property names are unchanged; GatewayLogScope remains the data holder.
- Add GatewayLogRedactorAdapter : ILogRedactor delegating to the existing
  GatewayLogRedactor policy (mxgw_ bearer tokens / credential-bearing command
  values), registered as a singleton so the shared RedactionEnricher masks
  secrets on every event. Remove the now-dead GatewayLoggerExtensions MEL helper.
- Tests: add GatewayLogRedactorAdapterTests; serialize the four host-building
  test classes into one non-parallel collection (HostBuildingCollection) so the
  process-wide Serilog bootstrap logger is not frozen by two concurrent host
  builds racing in parallel collections.

The net48/x86 worker is untouched.
This commit is contained in:
Joseph Doherty
2026-06-01 08:06:28 -04:00
parent 4bf71a0b2c
commit deba5ed115
10 changed files with 236 additions and 27 deletions
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Primitives;
using Serilog.Context;
namespace ZB.MOM.WW.MxGateway.Server.Diagnostics;
@@ -17,7 +18,12 @@ public static class GatewayRequestLoggingMiddlewareExtensions
/// <summary>Header name for the command method name.</summary>
public const string CommandMethodHeaderName = "x-command-method";
/// <summary>Adds gateway request logging scope middleware that reads correlation headers and redacts sensitive data.</summary>
/// <summary>
/// Adds gateway request logging middleware that reads the correlation headers and pushes them
/// as Serilog <see cref="LogContext"/> properties for the duration of the request. The pushed
/// properties (SessionId / WorkerProcessId / CorrelationId / CommandMethod / ClientIdentity)
/// are disposed when the request completes; the shared redaction enricher masks any secrets.
/// </summary>
/// <param name="app">Application builder.</param>
public static IApplicationBuilder UseGatewayRequestLoggingScope(this IApplicationBuilder app)
{
@@ -25,21 +31,56 @@ public static class GatewayRequestLoggingMiddlewareExtensions
return app.Use(async (context, next) =>
{
ILogger logger = context.RequestServices
.GetRequiredService<ILoggerFactory>()
.CreateLogger("MxGateway.Request");
using IDisposable? scope = logger.BeginGatewayScope(new GatewayLogScope(
GatewayLogScope scope = new(
SessionId: ReadHeader(context, SessionIdHeaderName),
WorkerProcessId: ReadInt32Header(context, WorkerProcessIdHeaderName),
CorrelationId: ReadUInt64Header(context, CorrelationIdHeaderName),
CommandMethod: ReadHeader(context, CommandMethodHeaderName),
ClientIdentity: ReadHeader(context, "authorization")));
ClientIdentity: ReadHeader(context, "authorization"));
using IDisposable correlationScope = PushCorrelationProperties(scope);
await next(context);
});
}
/// <summary>
/// Pushes the populated <paramref name="scope"/> properties onto the Serilog
/// <see cref="LogContext"/>, returning a single disposable that pops them all when the request
/// completes. Only the properties present in <see cref="GatewayLogScope.ToDictionary"/> (which
/// already applies the client-identity redaction policy) are pushed.
/// </summary>
/// <param name="scope">The correlation properties for the current request.</param>
/// <returns>A disposable that removes the pushed properties on disposal.</returns>
private static IDisposable PushCorrelationProperties(GatewayLogScope scope)
{
Stack<IDisposable> pushed = new();
foreach (KeyValuePair<string, object?> property in scope.ToDictionary())
{
pushed.Push(LogContext.PushProperty(property.Key, property.Value));
}
return new CorrelationPropertyScope(pushed);
}
/// <summary>
/// Disposes the pushed <see cref="LogContext"/> property bindings in reverse order, restoring
/// the ambient context to its pre-request state.
/// </summary>
private sealed class CorrelationPropertyScope(Stack<IDisposable> bindings) : IDisposable
{
private readonly Stack<IDisposable> _bindings = bindings;
public void Dispose()
{
while (_bindings.Count > 0)
{
_bindings.Pop().Dispose();
}
}
}
private static string? ReadHeader(HttpContext context, string headerName)
{
return context.Request.Headers.TryGetValue(headerName, out StringValues values)