ASP.NET Core 请求日志中间件
间件会记录请求方法、路径、查询字符串、请求体和运行时间,同时还会处理一些特定路由(如 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);}}}
}
-
捕获响应数据:
- 在中间件中,使用
MemoryStream
替代HttpContext.Response.Body
以捕获响应数据。 - 在调用下一个中间件 (
await _next(context);
) 后,读取responseBody
的内容。
- 在中间件中,使用
-
记录响应数据:
- 在
WriteVisitLog
方法中,将响应数据作为ResponseBody
记录。 - 通过
string.IsNullOrWhiteSpace
检查响应数据是否为空,以决定是否输出相关信息。
- 在
-
恢复响应流:
- 在完成响应后,将捕获的
responseBody
内容写入到原始的Response.Body
中,以确保响应能够正确返回给客户端。
- 在完成响应后,将捕获的
使用:
// 注册 LogMiddleware
app.UseMiddleware<LogMiddleware>();