← Назад к вопросам
ASP.NET Core: Custom Middleware для логирования
2.0 Middle🔥 181 комментариев
#Основы C# и .NET
Условие
Создайте custom Middleware для ASP.NET Core, который логирует информацию о каждом HTTP-запросе.
Требования:
- Логировать: HTTP метод, URL, время выполнения запроса, статус-код ответа
- Не логировать запросы к статическим файлам
- Для ошибок (статус >= 400) добавлять тело запроса в лог
Структура Middleware:
public class RequestLoggingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<RequestLoggingMiddleware> _logger;
// TODO: реализовать
}
Критерии оценки:
- Корректная работа с pipeline (вызов _next)
- Правильное измерение времени (Stopwatch)
- Обработка исключений
- Регистрация в DI-контейнере
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Анализ задачи
Middleware в ASP.NET Core — это компонент, который обрабатывает HTTP-запросы и ответы. Pipeline работает как матрёшка: каждое middleware может:
- Обработать входящий запрос
- Передать управление следующему middleware через
_next(context) - Обработать исходящий ответ
Ключевые моменты:
- Middleware выполняется в порядке регистрации в Program.cs
- Нужно правильно вызвать
_next()для продолжения pipeline - Нужно исключить статические файлы из логирования
- Нужно захватить время выполнения
- Нужно обработать исключения
Базовая реализация
using System.Diagnostics;
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Пропускаем статические файлы
if (IsStaticFile(context.Request.Path))
{
await _next(context);
return;
}
var stopwatch = Stopwatch.StartNew();
var originalBodyStream = context.Response.Body; // Сохраняем оригинальный поток
try
{
// Создаём temp stream для захвата тела ответа
using (var memoryStream = new MemoryStream())
{
context.Response.Body = memoryStream;
// Переводим входящий поток в позицию 0 (если нужно читать)
var requestBody = string.Empty;
if (context.Request.ContentLength > 0)
{
context.Request.EnableBuffering(); // Позволяет прочитать Body несколько раз
using (var reader = new StreamReader(context.Request.Body))
{
requestBody = await reader.ReadToEndAsync();
context.Request.Body.Position = 0; // Возвращаемся в начало для контроллера
}
}
// Вызываем следующее middleware в pipeline
await _next(context);
// Копируем тело ответа из памяти в оригинальный поток
memoryStream.Seek(0, SeekOrigin.Begin);
await memoryStream.CopyToAsync(originalBodyStream);
}
stopwatch.Stop();
// Логируем результат
LogRequest(context, stopwatch.ElapsedMilliseconds, requestBody);
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "Exception occurred during request handling. " +
$"{context.Request.Method} {context.Request.Path} took {stopwatch.ElapsedMilliseconds}ms");
throw; // Пробрасываем дальше для обработки
}
finally
{
context.Response.Body = originalBodyStream; // Возвращаем оригинальный поток
}
}
private void LogRequest(HttpContext context, long elapsedMilliseconds, string requestBody)
{
var request = context.Request;
var response = context.Response;
var logLevel = response.StatusCode >= 400 ? LogLevel.Warning : LogLevel.Information;
var logMessage = $"{request.Method} {request.Path}{request.QueryString} " +
$"- {response.StatusCode} ({elapsedMilliseconds}ms)";
if (response.StatusCode >= 400 && !string.IsNullOrEmpty(requestBody))
{
logMessage += $" | RequestBody: {requestBody}";
}
_logger.Log(logLevel, logMessage);
}
private bool IsStaticFile(PathString path)
{
var staticExtensions = new[] { ".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".woff", ".woff2" };
var pathValue = path.Value?.ToLower();
return staticExtensions.Any(ext => pathValue?.EndsWith(ext) ?? false);
}
}
Улучшенная версия с настройками
public class RequestLoggingOptions
{
public bool LogRequestBody { get; set; } = true;
public bool LogResponseBody { get; set; } = false;
public int MaxBodyLength { get; set; } = 1000; // Максимум символов в логе
public string[] ExcludedPaths { get; set; } = { "/health", "/metrics" };
public string[] ExcludedExtensions { get; set; } =
{
".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".woff", ".woff2"
};
}
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
private readonly RequestLoggingOptions _options;
public RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger,
IOptions<RequestLoggingOptions> options)
{
_next = next;
_logger = logger;
_options = options.Value;
}
public async Task InvokeAsync(HttpContext context)
{
// Проверяем, нужно ли логировать этот запрос
if (ShouldSkipLogging(context.Request.Path))
{
await _next(context);
return;
}
var stopwatch = Stopwatch.StartNew();
var originalBodyStream = context.Response.Body;
var requestBody = string.Empty;
var responseBody = string.Empty;
try
{
// Читаем тело запроса (если логируем)
if (_options.LogRequestBody && context.Request.ContentLength > 0)
{
context.Request.EnableBuffering();
using (var reader = new StreamReader(context.Request.Body))
{
requestBody = await reader.ReadToEndAsync();
if (requestBody.Length > _options.MaxBodyLength)
requestBody = requestBody.Substring(0, _options.MaxBodyLength) + "...";
context.Request.Body.Position = 0;
}
}
// Захватываем тело ответа (если логируем)
if (_options.LogResponseBody)
{
using (var memoryStream = new MemoryStream())
{
context.Response.Body = memoryStream;
await _next(context);
memoryStream.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(memoryStream))
{
responseBody = await reader.ReadToEndAsync();
if (responseBody.Length > _options.MaxBodyLength)
responseBody = responseBody.Substring(0, _options.MaxBodyLength) + "...";
}
memoryStream.Seek(0, SeekOrigin.Begin);
await memoryStream.CopyToAsync(originalBodyStream);
}
}
else
{
await _next(context);
}
stopwatch.Stop();
LogRequest(context, stopwatch.ElapsedMilliseconds, requestBody, responseBody);
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "Exception: {Method} {Path} ({ElapsedMs}ms)",
context.Request.Method, context.Request.Path, stopwatch.ElapsedMilliseconds);
throw;
}
finally
{
context.Response.Body = originalBodyStream;
}
}
private void LogRequest(
HttpContext context,
long elapsedMilliseconds,
string requestBody,
string responseBody)
{
var request = context.Request;
var response = context.Response;
var statusCode = response.StatusCode;
var logLevel = statusCode >= 400 ? LogLevel.Warning : LogLevel.Information;
var logMessage = $"{request.Method} {request.Path}{request.QueryString} " +
$"→ {statusCode} ({elapsedMilliseconds}ms)";
if (statusCode >= 400)
{
if (!string.IsNullOrEmpty(requestBody))
logMessage += $" | Req: {requestBody}";
if (!string.IsNullOrEmpty(responseBody))
logMessage += $" | Resp: {responseBody}";
}
_logger.Log(logLevel, logMessage);
}
private bool ShouldSkipLogging(PathString path)
{
var pathValue = path.Value?.ToLower();
// Проверяем исключённые пути
if (_options.ExcludedPaths.Any(p => pathValue?.StartsWith(p.ToLower()) ?? false))
return true;
// Проверяем исключённые расширения
if (_options.ExcludedExtensions.Any(ext => pathValue?.EndsWith(ext) ?? false))
return true;
return false;
}
}
Регистрация в Program.cs
Вариант 1: Простая регистрация
var builder = WebApplicationBuilder.CreateBuilder(args);
builder.Services.AddLogging();
var app = builder.Build();
// Регистрируем middleware
app.UseMiddleware<RequestLoggingMiddleware>();
app.MapGet("/api/test", () => Results.Ok(new { message = "Hello" }));
app.Run();
Вариант 2: С настройками
var builder = WebApplicationBuilder.CreateBuilder(args);
builder.Services.AddLogging();
// Регистрируем middleware с опциями
builder.Services.Configure<RequestLoggingOptions>(options =>
{
options.LogRequestBody = true;
options.LogResponseBody = false;
options.MaxBodyLength = 500;
options.ExcludedPaths = new[] { "/health", "/metrics" };
});
var app = builder.Build();
app.UseMiddleware<RequestLoggingMiddleware>();
app.MapGet("/api/test", () => Results.Ok(new { message = "Hello" }));
app.Run();
Альтернатива: Extension Method для удобства
public static class RequestLoggingExtensions
{
public static IApplicationBuilder UseRequestLogging(
this IApplicationBuilder builder,
Action<RequestLoggingOptions>? configureOptions = null)
{
if (configureOptions != null)
{
builder.ApplicationServices
.GetRequiredService<IOptionsChangeTokenSource<RequestLoggingOptions>>()
.GetChangeToken();
}
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
// Использование:
app.UseRequestLogging(options =>
{
options.LogRequestBody = true;
options.MaxBodyLength = 1000;
});
Пример вывода логов
info: RequestLoggingMiddleware[0]
GET /api/products?pageSize=10 → 200 (125ms)
warn: RequestLoggingMiddleware[0]
POST /api/products → 400 (85ms) | Req: {"name":""}
error: RequestLoggingMiddleware[0]
DELETE /api/products/999 → 500 (212ms)
Exception: Null reference exception
Обработка особых случаев
1. Логирование чувствительных данных (пароли, токены)
private string MaskSensitiveData(string body)
{
// Маскируем пароли
var masked = System.Text.RegularExpressions.Regex.Replace(
body,
@"\"password\"\s*:\s*\"[^\"]*\"",
"\"password\": \"***\"",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
return masked;
}
2. Логирование с контекстом (User ID, Request ID)
var userId = context.User?.FindFirst("sub")?.Value ?? "anonymous";
var requestId = context.TraceIdentifier;
var logMessage = $"[{requestId}] User: {userId} | " +
$"{request.Method} {request.Path} → {statusCode} ({elapsedMilliseconds}ms)";
_logger.LogInformation(logMessage);
3. Асинхронное логирование (чтобы не блокировать запрос)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await Task.Run(() => LogRequest(context, elapsedMilliseconds), cts.Token)
.ConfigureAwait(false);
Порядок middleware в pipeline
// ВАЖНО! Порядок регистрации влияет на выполнение
var app = builder.Build();
// 1. Логирование — должно быть одним из первых
app.UseMiddleware<RequestLoggingMiddleware>();
// 2. Обработка исключений
app.UseExceptionHandling();
// 3. Аутентификация / Авторизация
app.UseAuthentication();
app.UseAuthorization();
// 4. Маршрутизация
app.MapControllers();
app.Run();
Выводы
✅ Правильный middleware:
- Регистрируется в Program.cs через
UseMiddleware<T>() - Должен вызвать
_next(context)для продолжения pipeline - Использует
Stopwatchдля измерения времени - Обрабатывает исключения через try/catch
- Логирует на соответствующем уровне (Warning для ошибок)
- Исключает статические файлы и health-checks
- Кэширует тело запроса/ответа для анализа
- Маскирует чувствительные данные