Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayRequestLoggingMiddlewareExtensions.cs
T
Joseph Doherty deba5ed115 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.
2026-06-01 08:06:28 -04:00

109 lines
4.2 KiB
C#

using Microsoft.Extensions.Primitives;
using Serilog.Context;
namespace ZB.MOM.WW.MxGateway.Server.Diagnostics;
/// <summary>Middleware extensions for structured gateway request logging with correlation context.</summary>
public static class GatewayRequestLoggingMiddlewareExtensions
{
/// <summary>Header name for the session ID.</summary>
public const string SessionIdHeaderName = "x-session-id";
/// <summary>Header name for the worker process ID.</summary>
public const string WorkerProcessIdHeaderName = "x-worker-process-id";
/// <summary>Header name for the correlation ID.</summary>
public const string CorrelationIdHeaderName = "x-correlation-id";
/// <summary>Header name for the command method name.</summary>
public const string CommandMethodHeaderName = "x-command-method";
/// <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)
{
ArgumentNullException.ThrowIfNull(app);
return app.Use(async (context, next) =>
{
GatewayLogScope scope = new(
SessionId: ReadHeader(context, SessionIdHeaderName),
WorkerProcessId: ReadInt32Header(context, WorkerProcessIdHeaderName),
CorrelationId: ReadUInt64Header(context, CorrelationIdHeaderName),
CommandMethod: ReadHeader(context, CommandMethodHeaderName),
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)
? values.ToString()
: null;
}
private static int? ReadInt32Header(HttpContext context, string headerName)
{
string? value = ReadHeader(context, headerName);
return int.TryParse(value, out int parsedValue)
? parsedValue
: null;
}
private static ulong? ReadUInt64Header(HttpContext context, string headerName)
{
string? value = ReadHeader(context, headerName);
return ulong.TryParse(value, out ulong parsedValue)
? parsedValue
: null;
}
}