Больше двух параметров — каждый на отдельной строке (закрывающая скобка в конце последнего); два и меньше — в одну строку. Правило добавлено в docs/spec/Код-стайл-Дейл.md; применено к 628 сигнатурам в 253 файлах.
101 lines
4.9 KiB
C#
101 lines
4.9 KiB
C#
using System.Diagnostics;
|
|
using Deal.Api.Services;
|
|
using Deal.Api.Dtos;
|
|
|
|
namespace Deal.Api.Middleware;
|
|
|
|
/// <summary>
|
|
/// Access-лог HTTP-запросов core (Ruling 7, план Task 14): каждый запрос — одна структурированная
|
|
/// строка «метод путь → статус за N мс» (Serilog JSON). Логируются метод и путь БЕЗ query-строки,
|
|
/// заголовков и тела — секреты/токены в query не попадают в логи (Ruling 13); клиентский IP не
|
|
/// логируется (аудит-IP живёт в AuditLog, операторский контур).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Регистрируется самым первым в HTTP-конвейере (после UseForwardedHeaders): видит результат всех
|
|
/// слоёв ниже (CORS/session/rate-limiter/OriginGuard/эндпоинты) и полную длительность запроса.
|
|
/// Запросы gRPC-ингресса (Content-Type application/grpc) пропускаются — их содержательный access-лог
|
|
/// пишет интерцептор RpcCallLoggingInterceptor (HTTP-статус gRPC-вызовов всегда 200, полезен только
|
|
/// gRPC-статус). SSE-подписка /api/events логируется по завершении потока (длительность = время жизни
|
|
/// соединения). Отмена запроса (клиент закрыл SSE/дисконнект) отдельной строкой не пишется.
|
|
/// </remarks>
|
|
public sealed class HttpAccessLogMiddleware
|
|
{
|
|
// Content-Type gRPC-запросов (HTTP/2) — их логирует RpcCallLoggingInterceptor.
|
|
private const string GrpcContentType = "application/grpc";
|
|
|
|
private readonly RequestDelegate _next;
|
|
private readonly ILogger<HttpAccessLogMiddleware> _logger;
|
|
|
|
/// <summary>
|
|
/// Создаёт middleware access-лога HTTP-запросов.
|
|
/// </summary>
|
|
/// <param name="next">Следующий обработчик конвейера.</param>
|
|
/// <param name="logger">Логгер (Serilog, Ruling 7).</param>
|
|
public HttpAccessLogMiddleware(RequestDelegate next, ILogger<HttpAccessLogMiddleware> logger)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(next);
|
|
ArgumentNullException.ThrowIfNull(logger);
|
|
_next = next;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Обрабатывает запрос: пропускает gRPC-ингресс, остальные логирует по завершении.
|
|
/// </summary>
|
|
/// <param name="context">Контекст запроса.</param>
|
|
public async Task InvokeAsync(HttpContext context)
|
|
{
|
|
if (context.Request.ContentType?.StartsWith(GrpcContentType, StringComparison.OrdinalIgnoreCase) == true)
|
|
{
|
|
await _next(context);
|
|
return;
|
|
}
|
|
|
|
long startedAt = Stopwatch.GetTimestamp();
|
|
try
|
|
{
|
|
await _next(context);
|
|
LogCall(context, startedAt, null);
|
|
}
|
|
catch (Exception exception) when (exception is not OperationCanceledException)
|
|
{
|
|
// Необработанное исключение: фиксируем строку 500 (статус ещё не выставлен) и пробрасываем —
|
|
// обработку ошибки продолжает хостинг (DeveloperExceptionPage в dev/закрытие соединения в prod).
|
|
LogCall(context, startedAt, exception);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
// Пишет одну строку access-лога запроса (успех либо исключение).
|
|
// context: Контекст запроса (метод/путь/статус ответа).
|
|
// startedAt: Метка времени старта запроса (Stopwatch.GetTimestamp).
|
|
// exception: Необработанное исключение (null — запрос завершился штатно).
|
|
private void LogCall(
|
|
HttpContext context,
|
|
long startedAt,
|
|
Exception? exception)
|
|
{
|
|
long elapsedMs = (long)Stopwatch.GetElapsedTime(startedAt).TotalMilliseconds;
|
|
string method = context.Request.Method;
|
|
string path = context.Request.Path.Value ?? "/";
|
|
if (exception is null)
|
|
{
|
|
_logger.LogInformation(
|
|
"HTTP {Method} {Path} -> {StatusCode} за {DurationMs} мс",
|
|
method,
|
|
path,
|
|
context.Response.StatusCode,
|
|
elapsedMs);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogError(
|
|
exception,
|
|
"HTTP {Method} {Path} -> исключение за {DurationMs} мс",
|
|
method,
|
|
path,
|
|
elapsedMs);
|
|
}
|
|
}
|
|
}
|