148 lines
5.2 KiB
C#
148 lines
5.2 KiB
C#
using System.Diagnostics;
|
||
using System.Text;
|
||
|
||
namespace Flyshot.Server.Host.Middleware;
|
||
|
||
/// <summary>
|
||
/// HTTP 请求与响应日志中间件。
|
||
/// 记录每个 HTTP 请求的进入时间、方法、路径、查询串、请求体,
|
||
/// 以及响应的状态码、耗时和响应体(调试级别)。
|
||
/// </summary>
|
||
public sealed class RequestResponseLoggingMiddleware
|
||
{
|
||
private readonly RequestDelegate _next;
|
||
private readonly ILogger<RequestResponseLoggingMiddleware> _logger;
|
||
|
||
/// <summary>
|
||
/// 请求体最大日志长度,超出则截断并附加省略标记。
|
||
/// </summary>
|
||
private const int MaxBodyLogLength = 4096;
|
||
|
||
/// <summary>
|
||
/// 初始化请求响应日志中间件。
|
||
/// </summary>
|
||
/// <param name="next">下一个中间件委托。</param>
|
||
/// <param name="logger">日志记录器。</param>
|
||
public RequestResponseLoggingMiddleware(RequestDelegate next, ILogger<RequestResponseLoggingMiddleware> logger)
|
||
{
|
||
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理 HTTP 请求并记录输入输出。
|
||
/// </summary>
|
||
/// <param name="context">HTTP 上下文。</param>
|
||
public async Task InvokeAsync(HttpContext context)
|
||
{
|
||
var stopwatch = Stopwatch.StartNew();
|
||
var request = context.Request;
|
||
var requestId = Activity.Current?.Id ?? context.TraceIdentifier;
|
||
|
||
// 记录请求进入信息(Info 级别:方法、路径、查询参数)。
|
||
_logger.LogInformation(
|
||
"[HTTP-REQ] [{RequestId}] {Method} {Path}{QueryString} — 客户端 {RemoteIp}",
|
||
requestId,
|
||
request.Method,
|
||
request.Path,
|
||
request.QueryString.HasValue ? request.QueryString.Value : string.Empty,
|
||
context.Connection.RemoteIpAddress);
|
||
|
||
// 读取并记录请求体(Debug 级别)。
|
||
string? requestBody = null;
|
||
if (request.ContentLength > 0 && request.Body.CanRead)
|
||
{
|
||
request.EnableBuffering();
|
||
requestBody = await ReadBodyAsync(request.Body, context.RequestAborted).ConfigureAwait(false);
|
||
request.Body.Position = 0;
|
||
|
||
if (!string.IsNullOrEmpty(requestBody))
|
||
{
|
||
_logger.LogDebug(
|
||
"[HTTP-REQ-BODY] [{RequestId}] {Body}",
|
||
requestId,
|
||
TruncateBody(requestBody));
|
||
}
|
||
}
|
||
|
||
// 拦截响应流以便读取响应体。
|
||
var originalResponseBody = context.Response.Body;
|
||
using var responseBodyStream = new MemoryStream();
|
||
context.Response.Body = responseBodyStream;
|
||
|
||
try
|
||
{
|
||
await _next(context).ConfigureAwait(false);
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
_logger.LogError(
|
||
exception,
|
||
"[HTTP-ERR] [{RequestId}] {Method} {Path} 处理过程中发生未捕获异常",
|
||
requestId,
|
||
request.Method,
|
||
request.Path);
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
stopwatch.Stop();
|
||
responseBodyStream.Position = 0;
|
||
var responseBody = await ReadBodyAsync(responseBodyStream, context.RequestAborted).ConfigureAwait(false);
|
||
responseBodyStream.Position = 0;
|
||
await responseBodyStream.CopyToAsync(originalResponseBody, context.RequestAborted).ConfigureAwait(false);
|
||
context.Response.Body = originalResponseBody;
|
||
|
||
var statusCode = context.Response.StatusCode;
|
||
var level = statusCode >= 500 ? LogLevel.Error : statusCode >= 400 ? LogLevel.Warning : LogLevel.Information;
|
||
|
||
// 记录响应概要(Info/Warning/Error 级别)。
|
||
_logger.Log(
|
||
level,
|
||
"[HTTP-RES] [{RequestId}] {Method} {Path} => {StatusCode} ({ElapsedMs}ms)",
|
||
requestId,
|
||
request.Method,
|
||
request.Path,
|
||
statusCode,
|
||
stopwatch.ElapsedMilliseconds);
|
||
|
||
// 记录响应体(Debug 级别)。
|
||
if (!string.IsNullOrEmpty(responseBody))
|
||
{
|
||
_logger.LogDebug(
|
||
"[HTTP-RES-BODY] [{RequestId}] {Body}",
|
||
requestId,
|
||
TruncateBody(responseBody));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从流中读取文本内容。
|
||
/// </summary>
|
||
private static async Task<string> ReadBodyAsync(Stream stream, CancellationToken cancellationToken)
|
||
{
|
||
if (!stream.CanRead)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true);
|
||
var body = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||
return body;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 截断过长内容,避免日志膨胀。
|
||
/// </summary>
|
||
private static string TruncateBody(string body)
|
||
{
|
||
if (body.Length <= MaxBodyLogLength)
|
||
{
|
||
return body;
|
||
}
|
||
|
||
return body[..MaxBodyLogLength] + " ... [截断,总长度=" + body.Length + "]";
|
||
}
|
||
}
|