-
C#中关于中间件——请求日志记录
第三部分:Web应用开发
4. 中间件——请求日志记录
实例介绍
之前做电商后台时,排查问题全靠猜:用户说“提交订单失败”,但不知道他请求了什么URL、传了什么参数、服务器返回了什么状态;接口响应慢,也不知道是哪个环节卡了。后来加了请求日志中间件,所有请求的方法、路径、参数、响应状态、耗时都自动记录到vb.net教程C#教程python教程SQL教程access 2010教程文件,排查问题时搜一下请求ID,整个流程一目了然,定位问题时间从几小时缩短到几分钟。这节就带你给电商后台加请求日志,彻底搞懂中间件的工作原理。
需求分析
请求日志要解决“全链路追踪、问题快速定位、性能监控”的问题,具体需求如下:
1.核心日志内容:请求ID、请求时间、请求方法、URL、查询参数、表单数据、响应状态码、响应耗时;
2.环境区分:开发环境日志输出到控制台(带颜色),生产环境输出到文件(结构化JSON);
3.路径过滤:排除静态文件(CSS/JS/图片)、健康检查接口(/health)的日志,减少冗余;
4.敏感信息过滤:密码、token等敏感字段自动脱敏,避免泄露;
5.性能监控:记录响应耗时,超过1秒的请求标记为慢请求;
6.结构化日志:生产环境用JSON格式输出,方便ELK等日志系统分析;
7.异常捕获:请求过程中出现异常时,记录异常堆栈信息;
8.可配置性:允许通过配置文件调整日志级别、过滤规则、输出格式。
代码实现
前置条件:.NET 6+、ASP.NET Core MVC/API;需掌握HTTP请求流程、日志框架(默认ILogger或Serilog);推荐集成Serilog(结构化日志更友好)。
场景1:基础请求日志中间件(控制台输出)
实现一个简单的中间件,记录请求的核心信息到控制台,适合开发环境调试。
步骤1:创建RequestLoggingMiddleware(Middleware/RequestLoggingMiddleware.cs)
csharp
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Text;
namespace ECommerceWeb.Middleware
{
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next; // 下一个中间件的委托
private readonly ILogger<RequestLoggingMiddleware> _logger;
// 构造函数注入:RequestDelegate是必须的(管道的下一个环节),ILogger用于记录日志
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
// 中间件核心方法:处理请求,调用下一个中间件,记录日志
public async Task InvokeAsync(HttpContext context)
{
// 1. 记录请求开始时间
var stopwatch = Stopwatch.StartNew();
try
{
// 2. 记录请求信息(方法、URL、参数)
await LogRequest(context);
// 3. 调用下一个中间件(管道的核心:把请求传递给后续处理)
await _next(context);
// 4. 记录响应信息(状态码、耗时)
stopwatch.Stop();
await LogResponse(context, stopwatch.ElapsedMilliseconds);
}
catch (Exception ex)
{
// 5. 捕获异常,记录堆栈信息
stopwatch.Stop();
_logger.LogError(ex, "请求处理异常 | 请求ID: {TraceId} | 耗时: {ElapsedMs}ms",
context.TraceIdentifier, stopwatch.ElapsedMilliseconds);
throw; // 重新抛出异常,让后续中间件(比如异常处理中间件)处理
}
}
// 记录请求信息
private async Task LogRequest(HttpContext context)
{
var request = context.Request;
// 重置请求流指针(因为Body只能读一次,后续中间件可能还要用)
request.EnableBuffering();
// 读取请求Body(只读取非文件上传的请求,避免大文件占用内存)
string requestBody = string.Empty;
if (request.ContentLength.HasValue && request.ContentLength > 0 && !IsMultipartContent(request))
{
using var reader = new StreamReader(request.Body, Encoding.UTF8, leaveOpen: true);
requestBody = await reader.ReadToEndAsync();
// 重置指针到开头,让后续中间件能读取Body
request.Body.Position = 0;
}
// 脱敏敏感字段(比如password、token)
requestBody = DesensitizeSensitiveData(requestBody);
// 记录请求日志
_logger.LogInformation(
"请求开始 | 请求ID: {TraceId} | 方法: {Method} | URL: {Url} | 查询参数: {Query} | Body: {Body}",
context.TraceIdentifier,
request.Method,
$"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}",
request.QueryString.ToString(),
requestBody);
}
// 记录响应信息
private Task LogResponse(HttpContext context, long elapsedMs)
{
var response = context.Response;
// 标记慢请求(耗时超过1秒)
var isSlowRequest = elapsedMs > 1000;
var logLevel = isSlowRequest ? LogLevel.Warning : LogLevel.Information;
_logger.Log(logLevel,
"请求结束 | 请求ID: {TraceId} | 状态码: {StatusCode} | 耗时: {ElapsedMs}ms | 慢请求: {IsSlow}",
context.TraceIdentifier,
response.StatusCode,
elapsedMs,
isSlowRequest);
return Task.CompletedTask;
}
// 辅助方法:判断是否是文件上传请求
private bool IsMultipartContent(HttpRequest request)
{
return !string.IsNullOrEmpty(request.ContentType) && request.ContentType.StartsWith("multipart/form-data");
}
// 辅助方法:脱敏敏感数据
private string DesensitizeSensitiveData(string data)
{
if (string.IsNullOrEmpty(data)) return data;
// 替换password字段的值为***
data = System.Text.RegularExpressions.Regex.Replace(data, "\"password\":\"[^\"]*\"", "\"password\":\"***\"");
// 替换token字段的值为***
data = System.Text.RegularExpressions.Regex.Replace(data, "\"token\":\"[^\"]*\"", "\"token\":\"***\"");
return data;
}
}
}
步骤2:注册中间件(Program.cs)
csharp
var builder = WebApplication.CreateBuilder(args);
// 1. 添加日志服务(默认控制台日志,开发环境启用详细日志)
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
// 2. 添加MVC/API服务
builder.Services.AddControllersWithViews();
var app = builder.Build();
// 3. 注册自定义中间件(注意顺序:要在UseRouting、UseEndpoints之前,否则无法捕获完整请求)
app.UseMiddleware<RequestLoggingMiddleware>();
// 4. 内置中间件(顺序很重要:异常处理→HTTPS重定向→静态文件→路由→授权→端点)
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
场景2:生产环境结构化日志(Serilog集成)
开发环境用控制台日志足够,但生产环境需要结构化JSON日志,方便日志系统分析。这里集成Serilog,输出JSON格式到文件。
步骤1:安装Serilog NuGet包
bash
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Formatting.Compact
步骤2:配置Serilog(Program.cs)
csharp
using Serilog;
// 1. 初始化Serilog(要在WebApplication.CreateBuilder之前)
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information() // 日志级别:只记录Information及以上
.MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning) // 过滤Microsoft的日志(只记录Warning及以上)
.MinimumLevel.Override("System", Serilog.Events.LogEventLevel.Warning) // 过滤System的日志
.Enrich.FromLogContext() // 从日志上下文添加属性(比如请求ID)
.WriteTo.Console() // 开发环境输出到控制台
.WriteTo.File(
path: "logs/log-.json", // 日志文件路径,每天生成一个文件
rollingInterval: RollingInterval.Day, // 按天滚动
formatter: new CompactJsonFormatter(), // 输出结构化JSON
retainedFileCountLimit: 30, // 保留30天的日志
fileSizeLimitBytes: 1024 * 1024 * 100) // 单个文件最大100MB
.CreateLogger();
try
{
Log.Information("启动电商后台服务");
var builder = WebApplication.CreateBuilder(args);
// 2. 替换默认日志为Serilog
builder.Host.UseSerilog();
// 3. 其他服务注册...
builder.Services.AddControllersWithViews();
var app = builder.Build();
// 4. 注册中间件(和场景1一样)
app.UseMiddleware<RequestLoggingMiddleware>();
// 5. 内置中间件...
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "服务启动失败");
}
finally
{
Log.CloseAndFlush(); // 关闭日志,确保所有日志都写入文件
}
场景3:路径过滤与可配置性
通过配置文件指定要过滤的路径(比如静态文件、健康检查),避免冗余日志。
步骤1:添加配置(appsettings.json)
json
{
"RequestLogging": {
"ExcludePaths": [ "/css/", "/js/", "/images/", "/health" ], // 要排除的路径前缀
"SlowRequestThreshold": 1000, // 慢请求阈值(毫秒)
"LogLevel": "Information" // 日志级别
}
}
步骤2:创建RequestLoggingOptions(Models/RequestLoggingOptions.cs)
csharp
namespace ECommerceWeb.Models
{
public class RequestLoggingOptions
{
public const string SectionName = "RequestLogging"; // 配置文件中的节点名称
public List<string> ExcludePaths { get; set; } = new();
public int SlowRequestThreshold { get; set; } = 1000;
public string LogLevel { get; set; } = "Information";
}
}
步骤3:修改中间件,支持配置过滤(RequestLoggingMiddleware.cs)
csharp
// 新增:注入IOptions<RequestLoggingOptions>
private readonly RequestLoggingOptions _options;
// 修改构造函数
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger, IOptions<RequestLoggingOptions> options)
{
_next = next;
_logger = logger;
_options = options.Value;
}
// 在InvokeAsync开头添加过滤逻辑
public async Task InvokeAsync(HttpContext context)
{
// 过滤指定路径的请求
var path = context.Request.Path.ToString().ToLower();
if (_options.ExcludePaths.Any(p => path.StartsWith(p.ToLower())))
{
// 直接调用下一个中间件,不记录日志
await _next(context);
return;
}
// ...后续逻辑和场景1一样
}
步骤4:注册配置(Program.cs)
csharp
// 绑定配置文件中的RequestLogging节点到RequestLoggingOptions
builder.Services.Configure<RequestLoggingOptions>(
builder.Configuration.GetSection(RequestLoggingOptions.SectionName));
逐行讲解
场景1:基础中间件核心代码
1.RequestDelegate:中间件管道的核心,_next代表管道的下一个环节,必须通过构造函数注入;
2.InvokeAsync方法:中间件的入口,所有请求都会经过这里,返回Task支持异步;
3.Stopwatch:记录请求耗时,StartNew()开始计时,Stop()结束,ElapsedMilliseconds获取耗时;
4.EnableBuffering():请求Body是只读流,默认只能读一次,调用这个方法后可以重置指针,让后续中间件也能读取Body;
5.LogRequest/LogResponse:分别记录请求和响应的核心信息,context.TraceIdentifier是请求ID(全局唯一,用于追踪请求);
6.异常捕获:用try-catch捕获请求过程中的异常,记录堆栈信息后重新抛出,让异常处理中间件(比如UseDeveloperExceptionPage)处理;
7.脱敏处理:用正则表达式替换敏感字段的值,避免密码、token等信息泄露到日志中。
场景2:Serilog集成
1.LoggerConfiguration:配置Serilog的日志级别、输出目标(控制台+文件)、格式(CompactJsonFormatter是结构化JSON);
2.UseSerilog():替换ASP.NET Core的默认日志框架为Serilog,后续ILogger会使用Serilog的实现;
3.RollingInterval.Day:按天生成日志文件,避免单个文件过大;
4.CloseAndFlush():程序退出时关闭日志,确保所有缓存的日志都写入文件,避免丢失。
场景3:路径过滤与配置
1.IOptions
2.ExcludePaths:在InvokeAsync开头判断请求路径是否在排除列表中,如果是则直接调用下一个中间件,不记录日志;
3.可配置性:通过配置文件调整日志规则,不需要修改代码,适合生产环境动态调整。
基础知识拓展
-
中间件管道模型
管道顺序:中间件的注册顺序决定了执行顺序,比如UseStaticFiles要在UseRouting之前,因为静态文件不需要路由匹配;
短路中间件:如果中间件不调用_next(context),管道就会短路,后续中间件不会执行(比如授权中间件验证失败时,直接返回401,不会调用后续的控制器);
内置中间件顺序:官方推荐的顺序是:
异常处理 → HTTPS重定向 → 静态文件 → 路由 → CORS → 认证 → 授权 → 自定义中间件 → 端点; - 中间件注册方式
| 方式 | 用途说明 | 示例 |
|---|---|---|
|
UseMiddleware |
注册自定义中间件(推荐) |
app.UseMiddleware |
| Use() | 内联中间件(简单逻辑) | app.Use(async (context, next) => { ... }) |
| Run() | 终端中间件(管道的最后一步) | app.Run(async context => { await context.Response.WriteAsync("Hello World"); }) |
| Map() | 分支管道(根据路径分支) |
app.Map("/api", apiApp => { apiApp.UseMiddleware |
- 日志级别与最佳实践
| 级别 | 用途说明 | 示例场景 |
|---|---|---|
| Trace | 最详细的日志(调试用) | 记录变量值、循环次数 |
| Debug | 调试信息(开发环境) | 记录请求参数、中间件执行步骤 |
| Information | 正常业务流程记录 | 用户登录、订单创建、请求完成 |
| Warning | 警告信息(不影响业务但需关注) | 慢请求、资源不足、参数格式不规范 |
| Error | 错误信息(业务失败) | 数据库连接失败、API调用错误、异常抛出 |
| Fatal | 致命错误(程序崩溃) | 服务启动失败、数据库宕机 |
最佳实践:
开发环境用Debug级别,生产环境用Information级别;
避免在循环中记录Trace/Debug日志,会影响性能;
敏感信息必须脱敏,比如密码、token、手机号;
日志中要包含请求ID,方便全链路追踪。
总结
中间件是ASP.NET Core的核心,它的价值在于统一处理横切关注点(比如日志、认证、授权、异常处理),避免在每个控制器/动作中重复写相同的代码。请求日志中间件的关键要点:
1.管道顺序:注册顺序决定执行顺序,要确保中间件能捕获完整的请求/响应信息;
2.性能考虑:避免在中间件中做耗时操作,比如读取大文件Body时要限制大小;
3.敏感信息保护:必须对密码、token等敏感字段脱敏;
4.可配置性:通过配置文件调整日志规则,适应不同环境的需求;
5.结构化日志:生产环境推荐用Serilog等框架输出JSON格式日志,方便日志系统分析。
比如电商后台中,请求日志中间件能帮你快速定位用户登录失败、订单提交超时、API响应慢等问题,大大提升排查效率。掌握中间件的原理和实现,你可以轻松扩展ASP.NET Core的功能,处理各种横切关注点!
本站原创,转载请注明出处:










