目录
背景
中间件是对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());
}
}
日志选项
这是一种控制机制。中间件将创建/记录请求/响应数据如果IsEnabled是true。
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()方法中,我们从HttpRequest和HttpResponse将值分配给日志模型。
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> options和IRequestResponseLogger logger.
我们可以取消注释此行并检查日志JSON string:
//var jsonString = logCreator.LogString(); /*log json*/
过滤器
这些筛选器是可选的。在我们的例子中,我们使用两个过滤器为日志模型分配一些值。这是为了证明我们甚至可以在需要时从过滤器或控制器设置额外的日志信息。与中间件相同,IRequestResponseLogModelCreator在构造函数中不可注入。这就是我们用context.RequestServices.GetService<IRequestResponseLogModelCreator>()来获取注入模型的原因。
RequestResponseLoggerActionFilter 分配RequestDateTimeUtcActionLevel和ResponseDateTimeUtcActionLevel计算操作级别执行时间。
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
}
引用
请求响应日志
- Log Requests and Responses in ASP.NET Core 3 – Eric L. Anderson
- Using Middleware in .NET 5.0 to Log Requests and Responses
- c# - How to add 'request body' in serilog's output .net core? - Stack Overflow
IP
可观察性
https://www.codeproject.com/Articles/5337511/Request-Response-Logging-Middleware-ASP-NET-Core