请求响应日志记录中间件ASP.NET Core

目录

背景

日志帮助程序

日志模型

日志创建者

记录器

日志选项

中间件

过滤器

使用中间件/过滤器

测试代码

Curl

日志示例

引用

请求响应日志

IP

可观察性


背景

中间件是对ASP.NET应用程序中的每个请求执行的组件类。ASP.NET Web应用程序中可能有多个中间件。它可以是框架提供的,也可以是通过NuGet或你自己的自定义中间件添加的。此示例使用中间件记录每个请求响应和其他信息。在这里,我们将重点介绍如何使用中间件生成和存储日志。

日志帮助程序

日志模型

这是日志模型的基本数据结构,其中包含以下信息:

  • 标识符(项目客户端、请求和其他跟踪标识符)
  • 请求数据
  • 响应数据
  • 错误信息

public class RequestResponseLogModel
{
    public string LogId { get; set; }           /*Guid.NewGuid().ToString()*/
    public string Node { get; set; }            /*project name*/
    public string ClientIp { get; set; }
    public string TraceId { get; set; }         /*HttpContext TraceIdentifier*/


    public DateTime? RequestDateTimeUtc { get; set; }
    public DateTime? RequestDateTimeUtcActionLevel { get; set; }
    public string RequestPath { get; set; }
    public string RequestQuery { get; set; }
    public List<KeyValuePair<string, string>> RequestQueries { get; set; }
    public string RequestMethod { get; set; }
    public string RequestScheme { get; set; }
    public string RequestHost { get; set; }
    public Dictionary<string, string> RequestHeaders { get; set; }
    public string RequestBody { get; set; }
    public string RequestContentType { get; set; }


    public DateTime? ResponseDateTimeUtc { get; set; }
    public DateTime? ResponseDateTimeUtcActionLevel { get; set; }
    public string ResponseStatus { get; set; }
    public Dictionary<string, string> ResponseHeaders { get; set; }
    public string ResponseBody { get; set; }
    public string ResponseContentType { get; set; }


    public bool? IsExceptionActionLevel { get; set; }
    public string ExceptionMessage { get; set; }
    public string ExceptionStackTrace { get; set; }

    public RequestResponseLogModel()
    {
        LogId = Guid.NewGuid().ToString();
    }
}

日志创建者

日志创建者将创建/保存日志数据模型。在整个应用程序中,这将用于将必要的数据分配给日志模型。在应用程序中注入其创建者类的作用域非常重要。这意味着对于每个Web请求,将创建一个类的新实例。这样,它将专用于单个请求。

public interface IRequestResponseLogModelCreator
{
    RequestResponseLogModel LogModel { get; }
    string LogString();
}

public interface IRequestResponseLogger
{
    void Log(IRequestResponseLogModelCreator logCreator);
}

public class RequestResponseLogModelCreator : IRequestResponseLogModelCreator
{
    public RequestResponseLogModel LogModel { get; private set; }

    public RequestResponseLogModelCreator()
    {
        LogModel = new RequestResponseLogModel();
    }

    public string LogString()
    {
        var jsonString = JsonConvert.SerializeObject(LogModel);
        return jsonString;
    }
}

记录器

此记录器类负责序列化日志模型和存储日志。单例对象可以用于此目的。

public interface IRequestResponseLogger
{
    void Log(IRequestResponseLogModelCreator logCreator);
}

public class RequestResponseLogger : IRequestResponseLogger
{
    private readonly ILogger<RequestResponseLogger> _logger;

    public RequestResponseLogger(ILogger<RequestResponseLogger> logger)
    {
        _logger = logger;
    }
    public void Log(IRequestResponseLogModelCreator logCreator)
    {
        //_logger.LogTrace(jsonString);
        //_logger.LogInformation(jsonString);
        //_logger.LogWarning(jsonString);
        _logger.LogCritical(logCreator.LogString());
    }
}

日志选项

这是一种控制机制。中间件将创建/记录请求/响应数据如果IsEnabledtrue

public class RequestResponseLoggerOption
{
    public bool IsEnabled { get; set; }
    public string Name { get; set; }
    public string DateTimeFormat { get; set; }
}

选项模型将从应用配置或appsettings.json 序列化。

{
  "RequestResponseLogger": {
    "IsEnabled": true,
    "Name": "Cpm.Web.Api",
    "DateTimeFormat": "yyyy-MM-dd HH:mm:ss"
}

中间件

在这里,在InvokeAsync()方法中,我们从HttpRequestHttpResponse将值分配给日志模型。

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

namespace Cpm.Web.Api.Middlewares
{
    public class RequestResponseLoggerMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly RequestResponseLoggerOption _options;
        private readonly IRequestResponseLogger _logger;

        public RequestResponseLoggerMiddleware
        (RequestDelegate next, IOptions<RequestResponseLoggerOption> options, 
         IRequestResponseLogger logger)
        {
            _next = next;
            _options = options.Value;
            _logger = logger;
        }

        public async Task InvokeAsync(HttpContext httpContext, 
               IRequestResponseLogModelCreator logCreator)
        {
            RequestResponseLogModel log = logCreator.LogModel;
            // Middleware is enabled only when the 
            // EnableRequestResponseLogging config value is set.
            if (_options == null || !_options.IsEnabled)
            {
                await _next(httpContext);
                return;
            }
            log.RequestDateTimeUtc = DateTime.UtcNow;
            HttpRequest request = httpContext.Request;

            /*log*/
            log.LogId = Guid.NewGuid().ToString();
            log.TraceId = httpContext.TraceIdentifier;
            var ip = request.HttpContext.Connection.RemoteIpAddress;
            log.ClientIp = ip == null ? null : ip.ToString();
            log.Node = _options.Name;

            /*request*/
            log.RequestMethod = request.Method;
            log.RequestPath = request.Path;
            log.RequestQuery = request.QueryString.ToString();
            log.RequestQueries = FormatQueries(request.QueryString.ToString());
            log.RequestHeaders = FormatHeaders(request.Headers);
            log.RequestBody = await ReadBodyFromRequest(request);
            log.RequestScheme = request.Scheme;
            log.RequestHost = request.Host.ToString();
            log.RequestContentType = request.ContentType;

            // Temporarily replace the HttpResponseStream, 
            // which is a write-only stream, with a MemoryStream to capture 
            // its value in-flight.
            HttpResponse response = httpContext.Response;
            var originalResponseBody = response.Body;
            using var newResponseBody = new MemoryStream();
            response.Body = newResponseBody;

            // Call the next middleware in the pipeline
            try
            {
                await _next(httpContext);
            }
            catch (Exception exception)
            {
                /*exception: but was not managed at app.UseExceptionHandler() 
                  or by any middleware*/
                LogError(log, exception);
            }

            newResponseBody.Seek(0, SeekOrigin.Begin);
            var responseBodyText = 
                await new StreamReader(response.Body).ReadToEndAsync();

            newResponseBody.Seek(0, SeekOrigin.Begin);
            await newResponseBody.CopyToAsync(originalResponseBody);

            /*response*/
            log.ResponseContentType = response.ContentType;
            log.ResponseStatus = response.StatusCode.ToString();
            log.ResponseHeaders = FormatHeaders(response.Headers);
            log.ResponseBody = responseBodyText;
            log.ResponseDateTimeUtc = DateTime.UtcNow;


            /*exception: but was managed at app.UseExceptionHandler() 
              or by any middleware*/
            var contextFeature = 
                httpContext.Features.Get<IExceptionHandlerPathFeature>();
            if (contextFeature != null && contextFeature.Error != null)
            {
                Exception exception = contextFeature.Error;
                LogError(log, exception);
            }

            //var jsonString = logCreator.LogString(); /*log json*/
            _logger.Log(logCreator);
        }

        private void LogError(RequestResponseLogModel log, Exception exception)
        {
            log.ExceptionMessage = exception.Message;
            log.ExceptionStackTrace = exception.StackTrace;
        }

        private Dictionary<string, string> FormatHeaders(IHeaderDictionary headers) 
        {
            Dictionary<string, string> pairs = new Dictionary<string, string>();
            foreach (var header in headers)
            {
                pairs.Add(header.Key, header.Value);
            }
            return pairs;
        }

        private List<KeyValuePair<string, string>> FormatQueries(string queryString)
        {
            List<KeyValuePair<string, string>> pairs = 
                 new List<KeyValuePair<string, string>>();
            string key, value;
            foreach (var query in queryString.TrimStart('?').Split("&"))
            {
                var items = query.Split("=");
                key = items.Count() >= 1 ? items[0] : string.Empty;
                value = items.Count() >= 2 ? items[1] : string.Empty;
                if (!String.IsNullOrEmpty(key))
                {
                    pairs.Add(new KeyValuePair<string, string>(key, value));
                }    
            }
            return pairs;
        }

        private async Task<string> ReadBodyFromRequest(HttpRequest request)
        {
            // Ensure the request's body can be read multiple times 
            // (for the next middlewares in the pipeline).
            request.EnableBuffering();
            using var streamReader = new StreamReader(request.Body, leaveOpen: true);
            var requestBody = await streamReader.ReadToEndAsync();
            // Reset the request's body stream position for 
            // next middleware in the pipeline.
            request.Body.Position = 0;
            return requestBody;
        }
    }
}

要访问日志模型,我们需要获取IRequestResponseLogModelCreator,它将作为范围注入。这种注入对承包商不起作用。所以我们需要在InvokeAsync(HttpContext httpContext, IRequestResponseLogModelCreator logCreator)方法中使用它。

单例注入仅在中间件的构造函数级别工作。在中间件中,在构造函数级别使用注入是非常普遍的。即使在这个示例中,我们也使用了注入的IOptions<RequestResponseLoggerOption> optionsIRequestResponseLogger logger.

我们可以取消注释此行并检查日志JSON string

//var jsonString = logCreator.LogString(); /*log json*/

过滤器

这些筛选器是可选的。在我们的例子中,我们使用两个过滤器为日志模型分配一些值。这是为了证明我们甚至可以在需要时从过滤器或控制器设置额外的日志信息。与中间件相同,IRequestResponseLogModelCreator在构造函数中不可注入。这就是我们用context.RequestServices.GetService<IRequestResponseLogModelCreator>()来获取注入模型的原因。

RequestResponseLoggerActionFilter 分配RequestDateTimeUtcActionLevelResponseDateTimeUtcActionLevel计算操作级别执行时间。

using Cpm.Web.Api.Middlewares;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Diagnostics;

namespace Cpm.Web.Api.Filters
{
    [AttributeUsage(validOn: AttributeTargets.Class | AttributeTargets.Method)]
    public class RequestResponseLoggerActionFilter : Attribute, IActionFilter
    {
        private RequestResponseLogModel GetLogModel(HttpContext context)
        {
            return context.RequestServices.GetService
                   <IRequestResponseLogModelCreator>().LogModel;
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            var model = GetLogModel(context.HttpContext);
            model.RequestDateTimeUtcActionLevel = DateTime.UtcNow;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            var model = GetLogModel(context.HttpContext);
            model.ResponseDateTimeUtcActionLevel = DateTime.UtcNow;
        }
    }
}

RequestResponseLoggerErrorFilter分配IsExceptionActionLevel用于跟踪错误是在操作级别还是在其他地方。在应用程序中,我们可能会遇到这样一种情况:控制器操作执行没有任何错误,但在其他中间件或处理器之后遇到错误。

using Cpm.Web.Api.Middlewares;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Diagnostics;

namespace Cpm.Web.Api.Filters
{
    [AttributeUsage(validOn: AttributeTargets.Class | AttributeTargets.Method)]
    public class RequestResponseLoggerErrorFilter : Attribute, IExceptionFilter
    {
        private RequestResponseLogModel GetLogModel(HttpContext context)
        {
            return context.RequestServices.GetService
                   <IRequestResponseLogModelCreator>().LogModel;
        }

        public void OnException(ExceptionContext context)
        {
            var model = GetLogModel(context.HttpContext);
            model.IsExceptionActionLevel = true;
            if (model.ResponseDateTimeUtcActionLevel == null)
            {
                model.ResponseDateTimeUtcActionLevel = DateTime.UtcNow;
            }
        }
    }
}

使用中间件/过滤器

让我们在Startup.cs或引导程序文件中添加中间件、过滤器和依赖项注入。

void ConfigureServices(IServiceCollection services)里面:

/*Options*/
services.AddOptions<RequestResponseLoggerOption>().Bind
(Configuration.GetSection("RequestResponseLogger")).ValidateDataAnnotations();
/*IOC*/
services.AddSingleton<IRequestResponseLogger, RequestResponseLogger>();
services.AddScoped<IRequestResponseLogModelCreator, RequestResponseLogModelCreator>();
/*Filter*/
services.AddMvc(options =>
{
    options.Filters.Add(new RequestResponseLoggerActionFilter());
    options.Filters.Add(new RequestResponseLoggerErrorFilter());
});

并在void Configure(IApplicationBuilder app, IWebHostEnvironment env)中:

/*Middleware*/
app.UseMiddleware<RequestResponseLoggerMiddleware>();

完整的Startup.cs或在引导程序中:

using Cpm.Web.Api.Filters;
using Cpm.Web.Api.Middlewares;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.OpenApi.Models;

namespace Cpm.Web.Api
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. 
        // Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo 
                { Title = "Cpm.Web.Api", Version = "v1" });
            });

            /*Options*/
            services.AddOptions<RequestResponseLoggerOption>().Bind
            (Configuration.GetSection("RequestResponseLogger")).ValidateDataAnnotations();
            /*IOC*/
            services.AddSingleton<IRequestResponseLogger, RequestResponseLogger>();
            services.AddScoped<IRequestResponseLogModelCreator, 
                               RequestResponseLogModelCreator>();
            /*Filter*/
            services.AddMvc(options =>
            {
                options.Filters.Add(new RequestResponseLoggerActionFilter());
                options.Filters.Add(new RequestResponseLoggerErrorFilter());
            });
        }

        // This method gets called by the runtime. 
        // Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", 
                                 "Cpm.Web.Api v1"));
            }

            /*Middleware*/
            app.UseMiddleware<RequestResponseLoggerMiddleware>();

            /*error manage*/
            app.UseExceptionHandler(c => c.Run(async context =>
            {
                var exception = context.Features
                    .Get<IExceptionHandlerPathFeature>()
                    .Error;
                var response = new { details = "An error occurred" };
                await context.Response.WriteAsJsonAsync(response);
            }));

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

测试代码

运行Web应用程序。

Curl

url -X POST "https://localhost:7178/api/Hello/Details/1?eventId=100&eventName=name" 
-H  "accept: text/plain" -H  "Content-Type: application/json" -d "{\"value\":\"here\"}"

日志示例

{
   "LogId":"0b51f509-c8a0-471d-bd79-b3161afb19e7",
   "Node":"Cpm.Web.Api",
   "ClientIp":"::1",
   "TraceId":"0HMJ8HNMBP84I:00000007",
   "RequestDateTimeUtc":"2022-07-18T07:20:15.9655886Z",
   "RequestDateTimeUtcActionLevel":"2022-07-18T07:20:16.0408026Z",
   "RequestPath":"/api/Hello/Details/1",
   "RequestQuery":"?eventId=100&eventName=name",
   "RequestQueries":[
      {
         "Key":"eventId",
         "Value":"100"
      },
      {
         "Key":"eventName",
         "Value":"name"
      }
   ],
   "RequestMethod":"POST",
   "RequestScheme":"https",
   "RequestHost":"localhost:7178",
   "RequestHeaders":{
      "Accept":"text/plain",
      "Host":"localhost:7178",
      "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) 
       AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
      ":method":"POST",
      "Accept-Encoding":"gzip, deflate, br",
      "Accept-Language":"en-GB,en-US;q=0.9,en;q=0.8",
      "Content-Type":"application/json",
      "Cookie":"ai_user=tfFT6|2022-02-03T10:19:20.897Z,
       _ga=GA1.1.1007006786.1656487658,.AspNetCore.Culture=c%3Den%7Cuic%3Den,
       ai_session=qUyl5|1658128684507|1658128810611.5",
      "Origin":"https://localhost:7178",
      "Referer":"https://localhost:7178/swagger/index.html",
      "Content-Length":"16",
      "sec-ch-ua":"\".Not/A)Brand\";v=\"99\", \"Google Chrome\";v=\"103\", 
                   \"Chromium\";v=\"103\"",
      "sec-ch-ua-mobile":"?0",
      "sec-ch-ua-platform":"\"Windows\"",
      "sec-fetch-site":"same-origin",
      "sec-fetch-mode":"cors",
      "sec-fetch-dest":"empty"
   },
   "RequestBody":"{\"value\":\"here\"}",
   "RequestContentType":"application/json",
   "ResponseDateTimeUtc":"2022-07-18T07:20:16.0454527Z",
   "ResponseDateTimeUtcActionLevel":"2022-07-18T07:20:16.0411664Z",
   "ResponseStatus":"200",
   "ResponseHeaders":{
      "Content-Type":"application/json; charset=utf-8",
      "Date":"Mon, 18 Jul 2022 07:20:15 GMT",
      "Server":"Kestrel"
   },
   "ResponseBody":"{\"value\":\"here\"}",
   "ResponseContentType":"application/json; charset=utf-8",
   "IsExceptionActionLevel":null,
   "ExceptionMessage":null,
   "ExceptionStackTrace":null
}

引用

请求响应日志

IP

可观察性

https://www.codeproject.com/Articles/5337511/Request-Response-Logging-Middleware-ASP-NET-Core

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值