在日常 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 项目中,如果你也觉得内置日志不够顺手,不妨参考这种写法,定制一个符合团队规范的请求日志中间件。