前提:
需要nuget Microsoft.Extensions.Logging.Log4Net.AspNetCore 2.2.6;
Swashbuckle.AspNetCore 我暂时用的是 4.01;
描述:通过 Filters 拦截器获取 Api 请求内容及响应内容,并记录到日志文件;
有文中代码记录接口每次请求及响应情况如下图:
解决办法:
步骤1 配置 Swagger 接口文档
对startup.cs 进行修改代码如下:
ConfigureServices 中增加Swagger 配置
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Version = "v1",
Title = "Filters 过滤器测试Api",
Description = @"通过 IActionFilter, IAsyncResourceFilter 拦截器拦截请求及响应上下文并记录到log4日志"
});
c.IncludeXmlComments(this.GetType().Assembly.Location.Replace(".dll", ".xml"), true); //是需要设置 XML 注释文件的完整路径
});
对 Configure Http管道增加 SwaggerUi
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(o =>
{
o.SwaggerEndpoint("/swagger/v1/swagger.json", "Filters 过滤器测试Api");
});
}
步骤2 创建Log4net 日志帮助类 LogHelper.cs
/// <summary>
/// 日志帮助类
/// </summary>
public static class LogHelper
{
/// <summary>
/// 日志提供者
/// </summary>
private static ILogger logger;
/// <summary>
/// 静太方法构造函数
/// </summary>
static LogHelper()
{
logger = new LoggerFactory().AddConsole().AddDebug().AddLog4Net().CreateLogger("Logs");
}
/// <summary>
/// 打印提示
/// </summary>
/// <param name="message">日志内容</param>
public static void Info(object message)
{
logger.LogInformation(message?.ToString());
}
/// <summary>
/// 打印错误
/// </summary>
/// <param name="message">日志内容</param>
public static void Error(object message)
{
logger.LogError(message?.ToString());
}
/// <summary>
/// 打印错误
/// </summary>
/// <param name="ex">异常信息</param>
/// <param name="message">日志内容</param>
public static void Error(Exception ex, string message)
{
logger.LogError(ex, message);
}
/// <summary>
/// 调试信息打印
/// </summary>
/// <param name="message"></param>
public static void Debug(object message)
{
logger.LogDebug(message?.ToString());
}
}
LogHelper
步骤3 定义可读写的Http 上下文流接口 IReadableBody.cs 及 http 请求上下文中间件 HttpContextMiddleware.cs
/// <summary>
/// 定义可读Body的接口
/// </summary>
public interface IReadableBody
{
/// <summary>
/// 获取或设置是否可读
/// </summary>
bool IsRead { get; set; }
/// <summary>
/// 读取文本内容
/// </summary>
/// <returns></returns>
Task<string> ReadAsStringAsync();
}
IReadableBody
/// <summary>
/// Http 请求中间件
/// </summary>
public class HttpContextMiddleware
{
/// <summary>
/// 处理HTTP请求
/// </summary>
private readonly RequestDelegate next;
/// <summary>
/// 构造 Http 请求中间件
/// </summary>
/// <param name="next"></param>
public HttpContextMiddleware(RequestDelegate next)
{
this.next = next;
}
/// <summary>
/// 执行响应流指向新对象
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public Task Invoke(HttpContext context)
{
context.Response.Body = new ReadableResponseBody(context.Response.Body);
return this.next.Invoke(context);
}
/// <summary>
/// 可读的Response Body
/// </summary>
private class ReadableResponseBody : MemoryStream, IReadableBody
{
/// <summary>
/// 流内容
/// </summary>
private readonly Stream body;
/// <summary>
/// 获取或设置是否可读
/// </summary>
public bool IsRead { get; set; }
/// <summary>
/// 构造自定义流
/// </summary>
/// <param name="body"></param>
public ReadableResponseBody(Stream body)
{
this.body = body;
}
/// <summary>
/// 写入响应流
/// </summary>
/// <param name="buffer"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
public override void Write(byte[] buffer, int offset, int count)
{
this.body.Write(buffer, offset, count);
if (this.IsRead)
{
base.Write(buffer, offset, count);
}
}
/// <summary>
/// 写入响应流
/// </summary>
/// <param name="source"></param>
public override void Write(ReadOnlySpan<byte> source)
{
this.body.Write(source);
if (this.IsRead)
{
base.Write(source);
}
}
/// <summary>
/// 刷新响应流
/// </summary>
public override void Flush()
{
this.body.Flush();
if (this.IsRead)
{
base.Flush();
}
}
/// <summary>
/// 读取响应内容
/// </summary>
/// <returns></returns>
public Task<string> ReadAsStringAsync()
{
if (this.IsRead == false)
{
throw new NotSupportedException();
}
this.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(this))
{
return reader.ReadToEndAsync();
}
}
protected override void Dispose(bool disposing)
{
this.body.Dispose();
base.Dispose(disposing);
}
}
}
HttpContextMiddleware
步骤4 配置Http管通增加 Http请求上下文中件间
打开 Startup.cs ,对管通 Configure 增加如下中间件代码:
app.UseMiddleware
步骤5 增加Api 过滤器 ApiFilterAttribute
/// <summary>
/// Api 过滤器,记录请求上下文及响应上下文
/// </summary>
public class ApiFilterAttribute : Attribute, IActionFilter, IAsyncResourceFilter
{
/// <summary>
/// 请求Api 资源时
/// </summary>
/// <param name="context"></param>
/// <param name="next"></param>
/// <returns></returns>
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
// 执行前
try
{
await next.Invoke();
}
catch
{
}
// 执行后
await OnResourceExecutedAsync(context);
}
/// <summary>
/// 记录Http请求上下文
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnResourceExecutedAsync(ResourceExecutingContext context)
{
var log = new HttpContextMessage
{
RequestMethod = context.HttpContext.Request.Method,
ResponseStatusCode = context.HttpContext.Response.StatusCode,
RequestQurey = context.HttpContext.Request.QueryString.ToString(),
RequestContextType = context.HttpContext.Request.ContentType,
RequestHost = context.HttpContext.Request.Host.ToString(),
RequestPath = context.HttpContext.Request.Path,
RequestScheme = context.HttpContext.Request.Scheme,
RequestLocalIp = (context.HttpContext.Request.HttpContext.Connection.LocalIpAddress.MapToIPv4().ToString() + ":" + context.HttpContext.Request.HttpContext.Connection.LocalPort),
RequestRemoteIp = (context.HttpContext.Request.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString() + ":" + context.HttpContext.Request.HttpContext.Connection.RemotePort)
};
//获取请求的Body
//数据流倒带 context.HttpContext.Request.EnableRewind();
if (context.HttpContext.Request.Body.CanSeek)
{
using (var requestSm = context.HttpContext.Request.Body)
{
requestSm.Position = 0;
var reader = new StreamReader(requestSm, Encoding.UTF8);
log.RequestBody = reader.ReadToEnd();
}
}
//将当前 http 响应Body 转换为 IReadableBody
if (context.HttpContext.Response.Body is IReadableBody body)
{
if (body.IsRead)
{
log.ResponseBody = await body.ReadAsStringAsync();
}
}
if (string.IsNullOrEmpty(log.ResponseBody) == false && log.ResponseBody.Length > 200)
{
log.ResponseBody = log.ResponseBody.Substring(0, 200) + "......";
}
LogHelper.Debug(log);
}
/// <summary>
/// Action 执行前
/// </summary>
/// <param name="context"></param>
public void OnActionExecuting(ActionExecutingContext context)
{
//设置 Http请求响应内容设为可读
if (context.HttpContext.Response.Body is IReadableBody responseBody)
{
responseBody.IsRead = true;
}
}
/// <summary>
/// Action 执行后
/// </summary>
/// <param name="context"></param>
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
ApiFilterAttribute
步骤6 对需要记录请求上下文日志的接口加上特性 [ApiFilter]
[ApiFilter]
[Route("api/[controller]/[Action]")]
[ApiController]
public class DemoController : ControllerBase
{
.......
}
Demo 地址:https://github.com/intotf/netCore/tree/master/WebFilters