deba5ed115
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.
109 lines
4.2 KiB
C#
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;
|
|
}
|
|
}
|