189 lines
5.6 KiB
C#
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;
|
|
}
|
|
}
|