using Microsoft.Extensions.Primitives;
using Serilog.Context;
namespace ZB.MOM.WW.MxGateway.Server.Diagnostics;
/// Middleware extensions for structured gateway request logging with correlation context.
public static class GatewayRequestLoggingMiddlewareExtensions
{
/// Header name for the session ID.
public const string SessionIdHeaderName = "x-session-id";
/// Header name for the worker process ID.
public const string WorkerProcessIdHeaderName = "x-worker-process-id";
/// Header name for the correlation ID.
public const string CorrelationIdHeaderName = "x-correlation-id";
/// Header name for the command method name.
public const string CommandMethodHeaderName = "x-command-method";
///
/// Adds gateway request logging middleware that reads the correlation headers and pushes them
/// as Serilog 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.
///
/// Application builder.
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);
});
}
///
/// Pushes the populated properties onto the Serilog
/// , returning a single disposable that pops them all when the request
/// completes. Only the properties present in (which
/// already applies the client-identity redaction policy) are pushed.
///
/// The correlation properties for the current request.
/// A disposable that removes the pushed properties on disposal.
private static IDisposable PushCorrelationProperties(GatewayLogScope scope)
{
Stack pushed = new();
foreach (KeyValuePair property in scope.ToDictionary())
{
pushed.Push(LogContext.PushProperty(property.Key, property.Value));
}
return new CorrelationPropertyScope(pushed);
}
///
/// Disposes the pushed property bindings in reverse order, restoring
/// the ambient context to its pre-request state.
///
private sealed class CorrelationPropertyScope(Stack bindings) : IDisposable
{
private readonly Stack _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;
}
}