using System.Diagnostics; using System.Text; namespace Flyshot.Server.Host.Middleware; /// /// HTTP 请求与响应日志中间件。 /// 记录每个 HTTP 请求的进入时间、方法、路径、查询串、请求体, /// 以及响应的状态码、耗时和响应体(调试级别)。 /// public sealed class RequestResponseLoggingMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; /// /// 请求体最大日志长度,超出则截断并附加省略标记。 /// private const int MaxBodyLogLength = 4096; /// /// 初始化请求响应日志中间件。 /// /// 下一个中间件委托。 /// 日志记录器。 public RequestResponseLoggingMiddleware(RequestDelegate next, ILogger logger) { _next = next ?? throw new ArgumentNullException(nameof(next)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// 处理 HTTP 请求并记录输入输出。 /// /// HTTP 上下文。 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)); } } } /// /// 从流中读取文本内容。 /// private static async Task 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; } /// /// 截断过长内容,避免日志膨胀。 /// private static string TruncateBody(string body) { if (body.Length <= MaxBodyLogLength) { return body; } return body[..MaxBodyLogLength] + " ... [截断,总长度=" + body.Length + "]"; } }