Files
MikrocopTest/MikrocopApi/Middleware/ApiRequestLoggingMiddleware.cs

189 lines
5.6 KiB
C#

using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using MikrocopApi.Exceptions;
namespace MikrocopApi.Middleware;
public sealed class ApiRequestLoggingMiddleware
{
private static readonly HashSet<string> SensitiveFields = new(StringComparer.OrdinalIgnoreCase)
{
"password",
"passwordhash",
"apikey",
"x-api-key"
};
private readonly RequestDelegate _next;
private readonly ILogger<ApiRequestLoggingMiddleware> _logger;
public ApiRequestLoggingMiddleware(RequestDelegate next, ILogger<ApiRequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var requestBody = await ExtractBodyAsync(context.Request);
try
{
await _next(context);
var requestParameters = BuildRequestParameters(context, requestBody);
_logger.LogInformation(
"Time={Time}; ClientIp={ClientIp}; ClientName={ClientName}; Host={Host}; ApiMethod={ApiMethod}; RequestParameters={RequestParameters}; Message={Message}",
DateTimeOffset.UtcNow,
GetClientIp(context),
GetClientName(context),
Environment.MachineName,
GetApiMethod(context),
requestParameters,
"Request completed.");
}
catch (AppException ex)
{
var requestParameters = BuildRequestParameters(context, requestBody);
_logger.LogInformation(
ex,
"Time={Time}; ClientIp={ClientIp}; ClientName={ClientName}; Host={Host}; ApiMethod={ApiMethod}; RequestParameters={RequestParameters}; Message={Message}",
DateTimeOffset.UtcNow,
GetClientIp(context),
GetClientName(context),
Environment.MachineName,
GetApiMethod(context),
requestParameters,
"Request handled with business exception.");
throw;
}
catch (Exception ex)
{
var requestParameters = BuildRequestParameters(context, requestBody);
_logger.LogError(
ex,
"Time={Time}; ClientIp={ClientIp}; ClientName={ClientName}; Host={Host}; ApiMethod={ApiMethod}; RequestParameters={RequestParameters}; Message={Message}",
DateTimeOffset.UtcNow,
GetClientIp(context),
GetClientName(context),
Environment.MachineName,
GetApiMethod(context),
requestParameters,
"Request failed.");
throw;
}
}
private static string GetClientIp(HttpContext context)
{
return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
}
private static string GetClientName(HttpContext context)
{
if (context.User.Identity?.IsAuthenticated == true)
{
return context.User.Identity.Name ?? "authenticated-user";
}
return "anonymous";
}
private static string GetApiMethod(HttpContext context)
{
return context.GetEndpoint()?.DisplayName ?? $"{context.Request.Method} {context.Request.Path}";
}
private static string BuildRequestParameters(HttpContext context, string? body)
{
var query = context.Request.Query.ToDictionary(kvp => kvp.Key, kvp => SanitizeValue(kvp.Key, kvp.Value.ToString()));
var route = context.Request.RouteValues.ToDictionary(kvp => kvp.Key, kvp => SanitizeValue(kvp.Key, kvp.Value?.ToString() ?? string.Empty));
var payload = new
{
query,
route,
body
};
return JsonSerializer.Serialize(payload);
}
private static async Task<string?> ExtractBodyAsync(HttpRequest request)
{
if (request.ContentLength is not > 0)
{
return null;
}
request.EnableBuffering();
request.Body.Position = 0;
using var reader = new StreamReader(request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
var rawBody = await reader.ReadToEndAsync();
request.Body.Position = 0;
return SanitizeBody(rawBody);
}
private static string SanitizeBody(string body)
{
if (string.IsNullOrWhiteSpace(body))
{
return body;
}
try
{
var node = JsonNode.Parse(body);
if (node is null)
{
return body;
}
SanitizeNode(node);
return node.ToJsonString();
}
catch
{
return body;
}
}
private static void SanitizeNode(JsonNode node)
{
if (node is JsonObject jsonObject)
{
foreach (var kvp in jsonObject.ToList())
{
if (SensitiveFields.Contains(kvp.Key))
{
jsonObject[kvp.Key] = "***";
continue;
}
if (kvp.Value is not null)
{
SanitizeNode(kvp.Value);
}
}
}
if (node is JsonArray jsonArray)
{
foreach (var item in jsonArray)
{
if (item is not null)
{
SanitizeNode(item);
}
}
}
}
private static string SanitizeValue(string key, string value)
{
return SensitiveFields.Contains(key) ? "***" : value;
}
}