Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user