找回密码
 立即注册
首页 业界区 安全 ASP.NET Core中使用请求过滤器记录Http API日志 ...

ASP.NET Core中使用请求过滤器记录Http API日志

姘轻拎 6 天前
一、过滤器简介

ASP.NET Core中的过滤器是一种组件,它可以在请求处理管道中的特定阶段运行代码。过滤器有多种类型,包括授权过滤器、资源过滤器、动作过滤器、异常过滤器和结果过滤器。本文中使用的是动作过滤器(Action Filter),它在动作方法执行前后执行,可以用来记录请求和响应信息。
二、自定义GlobalActionFilter类

1. 类定义
  1. /// <summary>
  2. /// Action过滤器
  3. /// </summary>
  4. public class GlobalActionFilter : IActionFilter, IOrderedFilter
  5. {
  6.     /// <summary>
  7.     /// 构造
  8.     /// </summary>
  9.     /// <param name="logger"></param>
  10.     public GlobalActionFilter(SugarDbContext dbContext, ILogger<GlobalActionFilter> logger)
  11.     {
  12.         this.dbContext = dbContext;
  13.         this.logger = logger;
  14.     }
  15.     private readonly SugarDbContext dbContext;
  16.     private readonly ILogger<GlobalActionFilter> logger;
  17.     /// <summary>
  18.     /// 过滤器执行顺序
  19.     /// </summary>
  20.     public int Order => 2; // 设置执行顺序
  21.     private (string Method, string Path, string ClientIp) GetRequestInfo(HttpContext context)
  22.     {
  23.         string method = context.Request.Method;
  24.         string serverIp = context.Connection.LocalIpAddress.GetFormattedIpAddress();
  25.         string serverBaseUrl = $"http://{serverIp}:{context.Connection.LocalPort}";
  26.         string pathSmall = context.Request.Path.ToString();
  27.         string path = serverBaseUrl + pathSmall;
  28.         string clientIp = context.Connection.RemoteIpAddress.GetFormattedIpAddress();
  29.         return (method, path, clientIp);
  30.     }
  31.     private string GetInParam(ActionExecutingContext context)
  32.     {
  33.         return context.ActionArguments.Values.FirstOrDefault()?.ToJson() ?? "Null";
  34.     }
  35.     private string GetOutParam(ActionExecutedContext context)
  36.     {
  37.         JsonResult? response = context.Result as JsonResult;
  38.         return response?.Value != null ? JsonConvert.SerializeObject(response.Value) : "";
  39.     }
  40.     private EmResponseStaus GetResponseStatus(string pathSmall, string outParam)
  41.     {
  42.         if (string.IsNullOrWhiteSpace(outParam))
  43.         {
  44.             return EmResponseStaus.ReqNull;
  45.         }
  46.         try
  47.         {
  48.             //如果是TesTaskNotice接口,则使用TesResponse因为它的返回值和ApiResponse不一样
  49.             if (pathSmall.Contains("TesTaskNotice"))
  50.             {
  51.                 var apiResponse = outParam.ToObject<TesResponse>();
  52.                 return apiResponse?.ReturnCode == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
  53.             }
  54.             else
  55.             {
  56.                 var apiResponse = outParam.ToObject();
  57.                 return apiResponse?.Code == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
  58.             }
  59.         }
  60.         catch (JsonException)
  61.         {
  62.             logger.LogError($"在记录http日志时,序列化返参失败,path:{pathSmall} | outParam:{outParam}");
  63.             return EmResponseStaus.Unknown;
  64.         }
  65.         catch (Exception ex)
  66.         {
  67.             logger.LogError($"在记录http日志时,序列化返参失败,走到了Catch中,msg:{ex.Message},path:{pathSmall} | outParam:{outParam}");
  68.             return EmResponseStaus.Unknown;
  69.         }
  70.     }
  71.     /// <summary>
  72.     /// 执行Action之前
  73.     /// </summary>
  74.     /// <param name="context"></param>
  75.     public void OnActionExecuting(ActionExecutingContext context)
  76.     {
  77.         SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType<SkipActionFilterAttribute>().FirstOrDefault();
  78.         // 判断是否存在 SkipActionFilterAttribute 特性
  79.         bool hasSkipActionFilter = skipActionFilterAttribute != null;
  80.         var (method, path, clientIp) = GetRequestInfo(context.HttpContext);
  81.         if (method == "GET" || path.Contains("Test") || hasSkipActionFilter) return; // 忽略GET请求和测试控制器
  82.         string inParam = GetInParam(context);
  83.         //当前请求的唯一ID
  84.         string requestId = context.HttpContext.TraceIdentifier;
  85.         HttpApiLogEntity httpApiLogEntity = new HttpApiLogEntity()
  86.         {
  87.             Method = Tool.ToMethodEnum(method),
  88.             Url = path,
  89.             IPAddress = clientIp,
  90.             InParam = inParam,
  91.             OutParam = "",
  92.             ResponseStaus = EmResponseStaus.Unknown,
  93.             IsIncomingRequest = true,
  94.             SystemType = EmSystemType.WCS,
  95.             RequestId = requestId,
  96.             CreateTime = DateTime.Now,
  97.         };
  98.         dbContext.HttpApiLogEntity.Insert(httpApiLogEntity);
  99.         if (!context.ModelState.IsValid)//如果在进入Action之前 就已经判断到入参有误 则直接返回不进入Action
  100.         {
  101.             List<string>? errors = context.ModelState.SelectMany(x => x.Value.Errors)
  102.                 .Select(x => x.ErrorMessage)
  103.                 .ToList();
  104.             ApiResponse? outParam = new ApiResponse
  105.             {
  106.                 Code = EmApiResCode.ReqError, //入参有误 返回2
  107.                 Msg = string.Join(',', errors),
  108.                 Data = false
  109.             };
  110.             logger.LogInformation(
  111.                 $"Method: {method}, Path: {path}, IP: {clientIp}, InParam: {inParam}, OutParam: {outParam.ToJson()}");
  112.             httpApiLogEntity.OutParam = outParam.ToJson();
  113.             httpApiLogEntity.ResponseStaus = outParam.Code == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
  114.             httpApiLogEntity.EndTime = DateTime.Now;
  115.             dbContext.HttpApiLogEntity.Update(httpApiLogEntity);
  116.             context.Result = new JsonResult(outParam);
  117.         }
  118.     }
  119.     /// <summary>
  120.     /// 执行Action之后
  121.     /// </summary>
  122.     /// <param name="context"></param>
  123.     public void OnActionExecuted(ActionExecutedContext context)
  124.     {
  125.         SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType<SkipActionFilterAttribute>()
  126.             .FirstOrDefault();
  127.         // 判断是否存在 SkipActionFilterAttribute 特性
  128.         bool hasSkipActionFilter = skipActionFilterAttribute != null;
  129.         var (method, path, clientIp) = GetRequestInfo(context.HttpContext);
  130.         string pathSmall = context.HttpContext.Request.Path.ToString();
  131.         if (method == "GET" || pathSmall.Contains("Test") || hasSkipActionFilter) return; // 忽略GET请求和测试控制器
  132.         string requestId = context.HttpContext.TraceIdentifier;
  133.         HttpApiLogEntity? httpApiLogEntity = dbContext.HttpApiLogEntity.GetFirst(x => x.RequestId == requestId);
  134.         if (httpApiLogEntity != null)
  135.         {
  136.             string outParam = GetOutParam(context);
  137.             httpApiLogEntity.OutParam = outParam;
  138.             httpApiLogEntity.ResponseStaus = GetResponseStatus(pathSmall, outParam);
  139.             httpApiLogEntity.EndTime = DateTime.Now;
  140.             dbContext.HttpApiLogEntity.Update(httpApiLogEntity);
  141.         }
  142.     }
  143. }
复制代码
该类实现了IActionFilter接口,该接口定义了OnActionExecuting和OnActionExecuted方法,分别在动作方法执行前和执行后调用。同时实现了IOrderedFilter接口,用于指定过滤器的执行顺序。
2. 构造函数
  1. /// <summary>
  2. /// 构造
  3. /// </summary>
  4. /// <param name="logger"></param>
  5. public GlobalActionFilter(SugarDbContext dbContext, ILogger<GlobalActionFilter> logger)
  6. {
  7.     this.dbContext = dbContext;
  8.     this.logger = logger;
  9. }
  10. private readonly SugarDbContext dbContext;
  11. private readonly ILogger<GlobalActionFilter> logger;
复制代码
构造函数接收数据库上下文对象SugarDbContext和日志记录器ILogger,用于后续的数据库操作和日志记录。
3. 执行顺序
  1. /// <summary>
  2. /// 过滤器执行顺序
  3. /// </summary>
  4. public int Order => 2; // 设置执行顺序
复制代码
通过实现IOrderedFilter接口的Order属性,指定该过滤器的执行顺序为2。数值越小,过滤器越先执行。
4. 获取请求信息
  1. private (string Method, string Path, string ClientIp) GetRequestInfo(HttpContext context)
  2. {
  3.     string method = context.Request.Method;
  4.     string serverIp = context.Connection.LocalIpAddress.GetFormattedIpAddress();
  5.     string serverBaseUrl = $"http://{serverIp}:{context.Connection.LocalPort}";
  6.     string pathSmall = context.Request.Path.ToString();
  7.     string path = serverBaseUrl + pathSmall;
  8.     string clientIp = context.Connection.RemoteIpAddress.GetFormattedIpAddress();
  9.     return (method, path, clientIp);
  10. }
复制代码
该方法从HttpContext中提取请求方法、完整请求路径和客户端IP地址,并以元组形式返回。
5. 获取输入参数
  1. private string GetInParam(ActionExecutingContext context)
  2. {
  3.     return context.ActionArguments.Values.FirstOrDefault()?.ToJson() ?? "Null";
  4. }
复制代码
从ActionExecutingContext的ActionArguments中获取动作方法的输入参数,并序列化为JSON字符串。如果没有参数,则返回"Null"。
6. 获取输出参数
  1. private string GetOutParam(ActionExecutedContext context)
  2. {
  3.     JsonResult? response = context.Result as JsonResult;
  4.     return response?.Value != null ? JsonConvert.SerializeObject(response.Value) : "";
  5. }
复制代码
从ActionExecutedContext的Result中获取动作方法的返回结果,如果是JsonResult类型,则将其值序列化为JSON字符串返回。
7. 获取响应状态
  1. private EmResponseStaus GetResponseStatus(string pathSmall, string outParam)
  2. {
  3.     if (string.IsNullOrWhiteSpace(outParam))
  4.     {
  5.         return EmResponseStaus.ReqNull;
  6.     }
  7.     try
  8.     {
  9.         //如果是TesTaskNotice接口,则使用TesResponse因为它的返回值和ApiResponse不一样
  10.         if (pathSmall.Contains("TesTaskNotice"))
  11.         {
  12.             var apiResponse = outParam.ToObject<TesResponse>();
  13.             return apiResponse?.ReturnCode == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
  14.         }
  15.         else
  16.         {
  17.             var apiResponse = outParam.ToObject();
  18.             return apiResponse?.Code == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
  19.         }
  20.     }
  21.     catch (JsonException)
  22.     {
  23.         logger.LogError($"在记录http日志时,序列化返参失败,path:{pathSmall} | outParam:{outParam}");
  24.         return EmResponseStaus.Unknown;
  25.     }
  26.     catch (Exception ex)
  27.     {
  28.         logger.LogError($"在记录http日志时,序列化返参失败,走到了Catch中,msg:{ex.Message},path:{pathSmall} | outParam:{outParam}");
  29.         return EmResponseStaus.Unknown;
  30.     }
  31. }
复制代码
根据响应结果和请求路径判断响应状态。如果是特定接口TesTaskNotice,则根据TesResponse的ReturnCode判断;否则根据ApiResponse的Code判断。如果序列化失败,则记录错误日志并返回未知状态。
8. 动作执行前
  1. /// <summary>
  2. /// 执行Action之前
  3. /// </summary>
  4. /// <param name="context"></param>
  5. public void OnActionExecuting(ActionExecutingContext context)
  6. {
  7.     SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType<SkipActionFilterAttribute>().FirstOrDefault();
  8.     // 判断是否存在 SkipActionFilterAttribute 特性
  9.     bool hasSkipActionFilter = skipActionFilterAttribute != null;
  10.     var (method, path, clientIp) = GetRequestInfo(context.HttpContext);
  11.     if (method == "GET" || path.Contains("Test") || hasSkipActionFilter) return; // 忽略GET请求和测试控制器
  12.     string inParam = GetInParam(context);
  13.     //当前请求的唯一ID
  14.     string requestId = context.HttpContext.TraceIdentifier;
  15.     HttpApiLogEntity httpApiLogEntity = new HttpApiLogEntity()
  16.     {
  17.         Method = Tool.ToMethodEnum(method),
  18.         Url = path,
  19.         IPAddress = clientIp,
  20.         InParam = inParam,
  21.         OutParam = "",
  22.         ResponseStaus = EmResponseStaus.Unknown,
  23.         IsIncomingRequest = true,
  24.         SystemType = EmSystemType.WCS,
  25.         RequestId = requestId,
  26.         CreateTime = DateTime.Now,
  27.     };
  28.     dbContext.HttpApiLogEntity.Insert(httpApiLogEntity);
  29.     if (!context.ModelState.IsValid)//如果在进入Action之前 就已经判断到入参有误 则直接返回不进入Action
  30.     {
  31.         List<string>? errors = context.ModelState.SelectMany(x => x.Value.Errors)
  32.            .Select(x => x.ErrorMessage)
  33.            .ToList();
  34.         ApiResponse? outParam = new ApiResponse
  35.         {
  36.             Code = EmApiResCode.ReqError, //入参有误 返回2
  37.             Msg = string.Join(',', errors),
  38.             Data = false
  39.         };
  40.         logger.LogInformation(
  41.             $"Method: {method}, Path: {path}, IP: {clientIp}, InParam: {inParam}, OutParam: {outParam.ToJson()}");
  42.         httpApiLogEntity.OutParam = outParam.ToJson();
  43.         httpApiLogEntity.ResponseStaus = outParam.Code == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
  44.         httpApiLogEntity.EndTime = DateTime.Now;
  45.         dbContext.HttpApiLogEntity.Update(httpApiLogEntity);
  46.         context.Result = new JsonResult(outParam);
  47.     }
  48. }
复制代码
在动作方法执行前,首先判断是否应跳过该过滤器(例如GET请求、测试控制器或标记了SkipActionFilterAttribute特性的方法)。然后获取请求信息和输入参数,创建HttpApiLogEntity对象并插入数据库。如果模型状态无效(入参有误),则构造错误响应,更新日志记录并返回错误响应。
9. 动作执行后
  1. /// <summary>
  2. /// 执行Action之后
  3. /// </summary>
  4. /// <param name="context"></param>
  5. public void OnActionExecuted(ActionExecutedContext context)
  6. {
  7.     SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType<SkipActionFilterAttribute>()
  8.        .FirstOrDefault();
  9.     // 判断是否存在 SkipActionFilterAttribute 特性
  10.     bool hasSkipActionFilter = skipActionFilterAttribute != null;
  11.     var (method, path, clientIp) = GetRequestInfo(context.HttpContext);
  12.     string pathSmall = context.HttpContext.Request.Path.ToString();
  13.     if (method == "GET" || pathSmall.Contains("Test") || hasSkipActionFilter) return; // 忽略GET请求和测试控制器
  14.     string requestId = context.HttpContext.TraceIdentifier;
  15.     HttpApiLogEntity? httpApiLogEntity = dbContext.HttpApiLogEntity.GetFirst(x => x.RequestId == requestId);
  16.     if (httpApiLogEntity != null)
  17.     {
  18.         string outParam = GetOutParam(context);
  19.         httpApiLogEntity.OutParam = outParam;
  20.         httpApiLogEntity.ResponseStaus = GetResponseStatus(pathSmall, outParam);
  21.         httpApiLogEntity.EndTime = DateTime.Now;
  22.         dbContext.HttpApiLogEntity.Update(httpApiLogEntity);
  23.     }
  24. }
复制代码
在动作方法执行后,同样判断是否应跳过该过滤器。然后根据请求ID从数据库中获取之前插入的日志记录,获取输出参数和响应状态,更新日志记录的输出参数、响应状态和结束时间。
三、使用过滤器

1. 注册过滤器

在Startup.cs文件的ConfigureServices方法中注册过滤器:
  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3.     // 其他服务注册...
  4.     services.AddControllers(options =>
  5.     {
  6.         options.Filters.Add<GlobalActionFilter>();
  7.     });
  8. }
复制代码
这样,该过滤器将应用于所有控制器的动作方法。如果只希望应用于特定控制器或动作方法,可以在控制器类或动作方法上添加[TypeFilter(typeof(GlobalActionFilter))]特性。
2. 示例

假设我们有一个简单的控制器:
  1. using Microsoft.AspNetCore.Mvc;
  2. namespace Project.Controllers
  3. {
  4.     [Route("api/[controller]")]
  5.     [ApiController]
  6.     public class WeatherForecastController : ControllerBase
  7.     {
  8.         [HttpPost]
  9.         public IActionResult Post([FromBody] WeatherForecast forecast)
  10.         {
  11.             // 处理逻辑...
  12.             return Ok(new { Message = "Success" });
  13.         }
  14.     }
  15. }
复制代码
当发送POST请求到该控制器的Post方法时,GlobalActionFilter将记录请求和响应信息到数据库中。
四、总结

通过自定义动作过滤器,我们可以方便地在ASP.NET Core应用中记录Http API日志。这不仅有助于系统的调试和维护,还能提供有价值的运行时信息。在实际应用中,可以根据具体需求对过滤器进行扩展和优化,例如添加更多的日志字段、支持不同的日志存储方式等。
希望本文能帮助你理解和使用ASP.NET Core中的请求过滤器来记录API日志。如果有任何问题或建议,欢迎留言讨论。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册