间件会记录请求方法、路径、查询字符串、请求体和运行时间,同时还会处理一些特定路由(如 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>();