ASP.NET Core 请求日志中间件

发布于:2025-07-04 ⋅ 阅读:(17) ⋅ 点赞:(0)

间件会记录请求方法、路径、查询字符串、请求体和运行时间,同时还会处理一些特定路由(如 SignalR 和 Swagger)的请求,避免记录这些请求。

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Serilog;

namespace LogMiddleware
{
    public class LogMiddleware
    {
        private readonly RequestDelegate _next;

        public LogMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            // 处理特定路径
            if (context.Request.Path.HasValue &&
                (context.Request.Path.Value.IndexOf("SignalR", StringComparison.InvariantCultureIgnoreCase) > -1 ||
                 context.Request.Path.Value.IndexOf("Swagger", StringComparison.InvariantCultureIgnoreCase) > -1))
            {
                await _next(context);
                return;
            }

            // 添加请求时间戳
            if (!context.Request.Headers.ContainsKey("REQST"))
            {
                context.Request.Headers.Add("REQST", DateTime.Now.ToString());
            }

            context.Request.EnableBuffering(); // 允许多次读取请求体
            string requestBody = string.Empty;
            using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true))
            {
                requestBody = await reader.ReadToEndAsync();
                context.Request.Body.Position = 0; // 重置请求流的位置
            }

            // 创建一个新的 MemoryStream 用于捕获响应
            var originalBodyStream = context.Response.Body;
            using (var responseBody = new MemoryStream())
            {
                context.Response.Body = responseBody; // 使用新的 MemoryStream 代替响应流

                Stopwatch stopwatch = new Stopwatch();
                stopwatch.Start();
                try
                {
                    await _next(context); // 调用下一个中间件

                    // 记录响应数据
                    context.Response.Body.Seek(0, SeekOrigin.Begin);
                    var responseText = await new StreamReader(context.Response.Body).ReadToEndAsync();
                    context.Response.Body.Seek(0, SeekOrigin.Begin); // 重置响应流的位置

                    stopwatch.Stop();
                    long runTime = stopwatch.ElapsedMilliseconds;

                    WriteVisitLog(context, runTime, requestBody, responseText);
                }
                finally
                {
                    // 将捕获的响应流写回原始响应流
                    await responseBody.CopyToAsync(originalBodyStream);
                    context.Response.Body = originalBodyStream; // 恢复原始响应流
                }
            }
        }

        private void WriteVisitLog(HttpContext context, long runTime, string requestBody, string responseBody)
        {
            // 记录请求详细信息
            var requestInfo = new
            {
                Method = context.Request.Method,
                Path = context.Request.Path,
                QueryString = context.Request.QueryString.ToString(),
                Headers = context.Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString()),
                Body = requestBody,
                RunTime = runTime,
                ResponseBody = responseBody
            };

            var correlationId = requestInfo.Headers.ContainsKey("X-Correlation-Id") ? requestInfo.Headers["X-Correlation-Id"] : "N/A";
            var bodyOutput = string.IsNullOrWhiteSpace(requestInfo.Body) ? string.Empty : $"Body: {requestInfo.Body}; ";
            var queryStringOutput = string.IsNullOrWhiteSpace(requestInfo.QueryString) ? string.Empty : $"QueryString: {requestInfo.QueryString}; ";
            var responseOutput = string.IsNullOrWhiteSpace(requestInfo.ResponseBody) ? string.Empty : $"Response: {requestInfo.ResponseBody}; ";

            // 仅在 QueryString 和 Body 不为空时输出
            var logMessage = $"[{correlationId}] Request Info: Method: {requestInfo.Method}; Path: {requestInfo.Path}; " +
                             $"{queryStringOutput}{bodyOutput}{responseOutput}Run Time: {requestInfo.RunTime} ms";

            // 仅当 logMessage 不包含空部分时才记录
            if (!string.IsNullOrWhiteSpace(queryStringOutput) || !string.IsNullOrWhiteSpace(bodyOutput) || !string.IsNullOrWhiteSpace(responseOutput))
            {
                Log.Information(logMessage);
            }
        }
    }
}
  1. 捕获响应数据

    • 在中间件中,使用 MemoryStream 替代 HttpContext.Response.Body 以捕获响应数据。
    • 在调用下一个中间件 (await _next(context);) 后,读取 responseBody 的内容。
  2. 记录响应数据

    • 在 WriteVisitLog 方法中,将响应数据作为 ResponseBody 记录。
    • 通过 string.IsNullOrWhiteSpace 检查响应数据是否为空,以决定是否输出相关信息。
  3. 恢复响应流

    • 在完成响应后,将捕获的 responseBody 内容写入到原始的 Response.Body 中,以确保响应能够正确返回给客户端。

使用:

 // 注册 LogMiddleware
 app.UseMiddleware<LogMiddleware>();