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 SensitiveFields = new(StringComparer.OrdinalIgnoreCase) { "password", "passwordhash", "apikey", "x-api-key" }; private readonly RequestDelegate _next; private readonly ILogger _logger; public ApiRequestLoggingMiddleware(RequestDelegate next, ILogger 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 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; } }