在日常 API
开发与运维中,我们常遇到这样的问题:某个接口突然变慢,用户反馈偶发报错,但查日志时只能看到框架默认访问记录,既没有时间戳也没有业务上下文。今天从
SwitchData 项目的 DisplayLoggingMiddleware
出发,手写一个轻量级请求日志中间件,实现对每一次 HTTP
请求的性能监控与异常追踪。
一、为什么不用内置日志
ASP.NET Core 自带的 Microsoft.AspNetCore.HttpLogging
可以记录请求路径、状态码、耗时等基础信息,但在企业级项目里往往不够灵活:输出格式固定,难以与现有日志规范统一;无法读取
Endpoint 元数据,不能自动关联菜单/模块名称;缺少按状态码区分 Warn/Info
的能力;异常发生时,默认不会把耗时、路径、异常信息一次性聚合输出。因此,SwitchData
选择用自定义中间件接管请求生命周期。
二、核心实现
public class DisplayLoggingMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
{
if (ShouldSkipLogging(context))
{
await next(context);
return;
}
var endpoint = context.GetEndpoint();
var displayAttribute = endpoint?.Metadata.GetMetadata<DisplayAttribute>();
var displayName = displayAttribute?.Name ?? context.Request.Path;
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
await next(context);
stopwatch.Stop();
var statusCode = context.Response.StatusCode;
string msg = string.Format("完成处理({0}): {1} - 状态码: {2} - 耗时: {3}ms",
context.Request.Path, displayName, statusCode, stopwatch.ElapsedMilliseconds);
if (statusCode >= 400) Logger.Warn(msg);
else Logger.Info(msg);
}
catch (Exception ex)
{
stopwatch.Stop();
var msg = string.Format("处理失败({0}): {1} - 异常: {2} - 耗时: {3}ms",
context.Request.Path, displayName, ex.Message, stopwatch.ElapsedMilliseconds);
Logger.Error(msg, ex);
throw;
}
}
private bool ShouldSkipLogging(HttpContext context)
{
var path = context.Request.Path.Value ?? "";
return path.StartsWith("/swagger") ||
path.StartsWith("/health") ||
path.StartsWith("/favicon.ico") ||
path.Contains(".");
}
}
三、关键设计解析
1.
静态文件过滤:生产环境一次页面加载可能触发几十个静态文件请求,ShouldSkipLogging
通过路径特征过滤 Swagger、健康检查、favicon 以及带 .
的静态资源,避免日志被淹没。
2. 读取 Endpoint
元数据:context.GetEndpoint() 可拿到当前匹配
Endpoint,再提取 DisplayAttribute。这样日志里不再是冷冰冰的
/api/users/list,而是
用户管理 - 用户列表,低成本实现日志语义化。
3. Stopwatch
耗时统计:System.Diagnostics.Stopwatch 是 .NET
中精度最高的计时器之一。注意 Stop() 必须放在
catch 中,确保异常发生时也能拿到耗时。
4. 按状态码分级日志:成功走 Info,4xx/5xx 走 Warn,异常走 Error。生产排查时,只需查看 Warn/Error 级别即可快速定位。
5. 异常不吞掉:中间件捕获异常后记录日志,但仍然要
throw,否则异常被静默吞掉,上层统一响应中间件无法接收。
四、注册中间件
app.MapControllers();
app.UseMiddleware<DisplayLoggingMiddleware>();
app.Run();
实际项目中,建议把日志中间件放在更靠前的位置,例如
UseRouting()之后、UseAuthentication()之前,以便在异常发生前记录更完整的信息。
五、输出效果
2026-08-21 10:30:15,000 INFO DisplayLoggingMiddleware.InvokeAsync[25]: 完成处理(/api/users/list): 用户列表 - 状态码: 200 - 耗时: 45ms
2026-08-21 10:31:02,000 ERROR DisplayLoggingMiddleware.InvokeAsync[49]: 处理失败(/api/users/list): 用户列表 - 异常: Object reference... - 耗时: 12ms
六、扩展方向
- 记录请求体/响应体:配合
MemoryStream替换Response.Body,抓取响应内容用于审计; - 链路追踪:注入
TraceId/SpanId,方便微服务场景串联请求; - 慢接口告警:耗时超过阈值时单独输出 Warn 或推送告警;
- 指标上报:把耗时、状态码写入 Prometheus Metrics,用于 Grafana 可视化。
七、总结
DisplayLoggingMiddleware 用不到 70
行代码完成了请求路径识别、Endpoint
元数据提取、耗时统计、状态码分级、异常追踪五个核心能力。它的设计哲学是:中间件只做它该做的事——记录,不处理业务;抛异常,不吞错误。
在 ASP.NET Core
项目中,如果你也觉得内置日志不够顺手,不妨参考这种写法,定制一个符合团队规范的请求日志中间件。