樱桃喵星人 發表於 2025-4-27 17:33:00

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

<h2 id="一过滤器简介">一、过滤器简介</h2>
<p>ASP.NET Core中的过滤器是一种组件,它可以在请求处理管道中的特定阶段运行代码。过滤器有多种类型,包括授权过滤器、资源过滤器、动作过滤器、异常过滤器和结果过滤器。本文中使用的是动作过滤器(<code>Action Filter</code>),它在动作方法执行前后执行,可以用来记录请求和响应信息。</p>
<h2 id="二自定义globalactionfilter类">二、自定义GlobalActionFilter类</h2>
<h3 id="1-类定义">1. 类定义</h3>
<pre><code class="language-csharp">/// &lt;summary&gt;
/// Action过滤器
/// &lt;/summary&gt;
public class GlobalActionFilter : IActionFilter, IOrderedFilter
{
    /// &lt;summary&gt;
    /// 构造
    /// &lt;/summary&gt;
    /// &lt;param name="logger"&gt;&lt;/param&gt;
    public GlobalActionFilter(SugarDbContext dbContext, ILogger&lt;GlobalActionFilter&gt; logger)
    {
      this.dbContext = dbContext;
      this.logger = logger;
    }

    private readonly SugarDbContext dbContext;
    private readonly ILogger&lt;GlobalActionFilter&gt; logger;

    /// &lt;summary&gt;
    /// 过滤器执行顺序
    /// &lt;/summary&gt;
    public int Order =&gt; 2; // 设置执行顺序

    private (string Method, string Path, string ClientIp) GetRequestInfo(HttpContext context)
    {
      string method = context.Request.Method;
      string serverIp = context.Connection.LocalIpAddress.GetFormattedIpAddress();
      string serverBaseUrl = $"http://{serverIp}:{context.Connection.LocalPort}";
      string pathSmall = context.Request.Path.ToString();
      string path = serverBaseUrl + pathSmall;
      string clientIp = context.Connection.RemoteIpAddress.GetFormattedIpAddress();
      return (method, path, clientIp);
    }

    private string GetInParam(ActionExecutingContext context)
    {
      return context.ActionArguments.Values.FirstOrDefault()?.ToJson() ?? "Null";
    }

    private string GetOutParam(ActionExecutedContext context)
    {
      JsonResult? response = context.Result as JsonResult;
      return response?.Value != null ? JsonConvert.SerializeObject(response.Value) : "";
    }

    private EmResponseStaus GetResponseStatus(string pathSmall, string outParam)
    {
      if (string.IsNullOrWhiteSpace(outParam))
      {
            return EmResponseStaus.ReqNull;
      }

      try
      {
            //如果是TesTaskNotice接口,则使用TesResponse因为它的返回值和ApiResponse不一样
            if (pathSmall.Contains("TesTaskNotice"))
            {
                var apiResponse = outParam.ToObject&lt;TesResponse&gt;();
                return apiResponse?.ReturnCode == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
            }
            else
            {
                var apiResponse = outParam.ToObject&lt;ApiResponse&gt;();
                return apiResponse?.Code == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
            }
      }
      catch (JsonException)
      {
            logger.LogError($"在记录http日志时,序列化返参失败,path:{pathSmall} | outParam:{outParam}");
            return EmResponseStaus.Unknown;
      }
      catch (Exception ex)
      {
            logger.LogError($"在记录http日志时,序列化返参失败,走到了Catch中,msg:{ex.Message},path:{pathSmall} | outParam:{outParam}");
            return EmResponseStaus.Unknown;
      }
    }

    /// &lt;summary&gt;
    /// 执行Action之前
    /// &lt;/summary&gt;
    /// &lt;param name="context"&gt;&lt;/param&gt;
    public void OnActionExecuting(ActionExecutingContext context)
    {
      SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType&lt;SkipActionFilterAttribute&gt;().FirstOrDefault();
      // 判断是否存在 SkipActionFilterAttribute 特性
      bool hasSkipActionFilter = skipActionFilterAttribute != null;
      var (method, path, clientIp) = GetRequestInfo(context.HttpContext);
      if (method == "GET" || path.Contains("Test") || hasSkipActionFilter) return; // 忽略GET请求和测试控制器

      string inParam = GetInParam(context);
      //当前请求的唯一ID
      string requestId = context.HttpContext.TraceIdentifier;

      HttpApiLogEntity httpApiLogEntity = new HttpApiLogEntity()
      {
            Method = Tool.ToMethodEnum(method),
            Url = path,
            IPAddress = clientIp,
            InParam = inParam,
            OutParam = "",
            ResponseStaus = EmResponseStaus.Unknown,
            IsIncomingRequest = true,
            SystemType = EmSystemType.WCS,
            RequestId = requestId,
            CreateTime = DateTime.Now,
      };

      dbContext.HttpApiLogEntity.Insert(httpApiLogEntity);

      if (!context.ModelState.IsValid)//如果在进入Action之前 就已经判断到入参有误 则直接返回不进入Action
      {
            List&lt;string&gt;? errors = context.ModelState.SelectMany(x =&gt; x.Value.Errors)
                .Select(x =&gt; x.ErrorMessage)
                .ToList();
            ApiResponse? outParam = new ApiResponse
            {
                Code = EmApiResCode.ReqError, //入参有误 返回2
                Msg = string.Join(',', errors),
                Data = false
            };
            logger.LogInformation(
                $"Method: {method}, Path: {path}, IP: {clientIp}, InParam: {inParam}, OutParam: {outParam.ToJson()}");

            httpApiLogEntity.OutParam = outParam.ToJson();
            httpApiLogEntity.ResponseStaus = outParam.Code == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
            httpApiLogEntity.EndTime = DateTime.Now;
            dbContext.HttpApiLogEntity.Update(httpApiLogEntity);

            context.Result = new JsonResult(outParam);
      }
    }

    /// &lt;summary&gt;
    /// 执行Action之后
    /// &lt;/summary&gt;
    /// &lt;param name="context"&gt;&lt;/param&gt;
    public void OnActionExecuted(ActionExecutedContext context)
    {
      SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType&lt;SkipActionFilterAttribute&gt;()
            .FirstOrDefault();
      // 判断是否存在 SkipActionFilterAttribute 特性
      bool hasSkipActionFilter = skipActionFilterAttribute != null;
      var (method, path, clientIp) = GetRequestInfo(context.HttpContext);
      string pathSmall = context.HttpContext.Request.Path.ToString();
      if (method == "GET" || pathSmall.Contains("Test") || hasSkipActionFilter) return; // 忽略GET请求和测试控制器

      string requestId = context.HttpContext.TraceIdentifier;
      HttpApiLogEntity? httpApiLogEntity = dbContext.HttpApiLogEntity.GetFirst(x =&gt; x.RequestId == requestId);

      if (httpApiLogEntity != null)
      {
            string outParam = GetOutParam(context);
            httpApiLogEntity.OutParam = outParam;
            httpApiLogEntity.ResponseStaus = GetResponseStatus(pathSmall, outParam);
            httpApiLogEntity.EndTime = DateTime.Now;

            dbContext.HttpApiLogEntity.Update(httpApiLogEntity);
      }
    }
}

</code></pre>
<p>该类实现了<code>IActionFilter</code>接口,该接口定义了<code>OnActionExecuting</code>和<code>OnActionExecuted</code>方法,分别在动作方法执行前和执行后调用。同时实现了<code>IOrderedFilter</code>接口,用于指定过滤器的执行顺序。</p>
<h3 id="2-构造函数">2. 构造函数</h3>
<pre><code class="language-csharp">/// &lt;summary&gt;
/// 构造
/// &lt;/summary&gt;
/// &lt;param name="logger"&gt;&lt;/param&gt;
public GlobalActionFilter(SugarDbContext dbContext, ILogger&lt;GlobalActionFilter&gt; logger)
{
    this.dbContext = dbContext;
    this.logger = logger;
}

private readonly SugarDbContext dbContext;
private readonly ILogger&lt;GlobalActionFilter&gt; logger;
</code></pre>
<p>构造函数接收数据库上下文对象<code>SugarDbContext</code>和日志记录器<code>ILogger&lt;GlobalActionFilter&gt;</code>,用于后续的数据库操作和日志记录。</p>
<h3 id="3-执行顺序">3. 执行顺序</h3>
<pre><code class="language-csharp">/// &lt;summary&gt;
/// 过滤器执行顺序
/// &lt;/summary&gt;
public int Order =&gt; 2; // 设置执行顺序
</code></pre>
<p>通过实现<code>IOrderedFilter</code>接口的<code>Order</code>属性,指定该过滤器的执行顺序为2。数值越小,过滤器越先执行。</p>
<h3 id="4-获取请求信息">4. 获取请求信息</h3>
<pre><code class="language-csharp">private (string Method, string Path, string ClientIp) GetRequestInfo(HttpContext context)
{
    string method = context.Request.Method;
    string serverIp = context.Connection.LocalIpAddress.GetFormattedIpAddress();
    string serverBaseUrl = $"http://{serverIp}:{context.Connection.LocalPort}";
    string pathSmall = context.Request.Path.ToString();
    string path = serverBaseUrl + pathSmall;
    string clientIp = context.Connection.RemoteIpAddress.GetFormattedIpAddress();
    return (method, path, clientIp);
}
</code></pre>
<p>该方法从<code>HttpContext</code>中提取请求方法、完整请求路径和客户端IP地址,并以元组形式返回。</p>
<h3 id="5-获取输入参数">5. 获取输入参数</h3>
<pre><code class="language-csharp">private string GetInParam(ActionExecutingContext context)
{
    return context.ActionArguments.Values.FirstOrDefault()?.ToJson() ?? "Null";
}
</code></pre>
<p>从<code>ActionExecutingContext</code>的<code>ActionArguments</code>中获取动作方法的输入参数,并序列化为JSON字符串。如果没有参数,则返回"Null"。</p>
<h3 id="6-获取输出参数">6. 获取输出参数</h3>
<pre><code class="language-csharp">private string GetOutParam(ActionExecutedContext context)
{
    JsonResult? response = context.Result as JsonResult;
    return response?.Value != null ? JsonConvert.SerializeObject(response.Value) : "";
}
</code></pre>
<p>从<code>ActionExecutedContext</code>的<code>Result</code>中获取动作方法的返回结果,如果是<code>JsonResult</code>类型,则将其值序列化为JSON字符串返回。</p>
<h3 id="7-获取响应状态">7. 获取响应状态</h3>
<pre><code class="language-csharp">private EmResponseStaus GetResponseStatus(string pathSmall, string outParam)
{
    if (string.IsNullOrWhiteSpace(outParam))
    {
      return EmResponseStaus.ReqNull;
    }

    try
    {
      //如果是TesTaskNotice接口,则使用TesResponse因为它的返回值和ApiResponse不一样
      if (pathSmall.Contains("TesTaskNotice"))
      {
            var apiResponse = outParam.ToObject&lt;TesResponse&gt;();
            return apiResponse?.ReturnCode == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
      }
      else
      {
            var apiResponse = outParam.ToObject&lt;ApiResponse&gt;();
            return apiResponse?.Code == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
      }
    }
    catch (JsonException)
    {
      logger.LogError($"在记录http日志时,序列化返参失败,path:{pathSmall} | outParam:{outParam}");
      return EmResponseStaus.Unknown;
    }
    catch (Exception ex)
    {
      logger.LogError($"在记录http日志时,序列化返参失败,走到了Catch中,msg:{ex.Message},path:{pathSmall} | outParam:{outParam}");
      return EmResponseStaus.Unknown;
    }
}
</code></pre>
<p>根据响应结果和请求路径判断响应状态。如果是特定接口<code>TesTaskNotice</code>,则根据<code>TesResponse</code>的<code>ReturnCode</code>判断;否则根据<code>ApiResponse</code>的<code>Code</code>判断。如果序列化失败,则记录错误日志并返回未知状态。</p>
<h3 id="8-动作执行前">8. 动作执行前</h3>
<pre><code class="language-csharp">/// &lt;summary&gt;
/// 执行Action之前
/// &lt;/summary&gt;
/// &lt;param name="context"&gt;&lt;/param&gt;
public void OnActionExecuting(ActionExecutingContext context)
{
    SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType&lt;SkipActionFilterAttribute&gt;().FirstOrDefault();
    // 判断是否存在 SkipActionFilterAttribute 特性
    bool hasSkipActionFilter = skipActionFilterAttribute != null;
    var (method, path, clientIp) = GetRequestInfo(context.HttpContext);
    if (method == "GET" || path.Contains("Test") || hasSkipActionFilter) return; // 忽略GET请求和测试控制器

    string inParam = GetInParam(context);
    //当前请求的唯一ID
    string requestId = context.HttpContext.TraceIdentifier;

    HttpApiLogEntity httpApiLogEntity = new HttpApiLogEntity()
    {
      Method = Tool.ToMethodEnum(method),
      Url = path,
      IPAddress = clientIp,
      InParam = inParam,
      OutParam = "",
      ResponseStaus = EmResponseStaus.Unknown,
      IsIncomingRequest = true,
      SystemType = EmSystemType.WCS,
      RequestId = requestId,
      CreateTime = DateTime.Now,
    };

    dbContext.HttpApiLogEntity.Insert(httpApiLogEntity);

    if (!context.ModelState.IsValid)//如果在进入Action之前 就已经判断到入参有误 则直接返回不进入Action
    {
      List&lt;string&gt;? errors = context.ModelState.SelectMany(x =&gt; x.Value.Errors)
         .Select(x =&gt; x.ErrorMessage)
         .ToList();
      ApiResponse? outParam = new ApiResponse
      {
            Code = EmApiResCode.ReqError, //入参有误 返回2
            Msg = string.Join(',', errors),
            Data = false
      };
      logger.LogInformation(
            $"Method: {method}, Path: {path}, IP: {clientIp}, InParam: {inParam}, OutParam: {outParam.ToJson()}");

      httpApiLogEntity.OutParam = outParam.ToJson();
      httpApiLogEntity.ResponseStaus = outParam.Code == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
      httpApiLogEntity.EndTime = DateTime.Now;
      dbContext.HttpApiLogEntity.Update(httpApiLogEntity);

      context.Result = new JsonResult(outParam);
    }
}
</code></pre>
<p>在动作方法执行前,首先判断是否应跳过该过滤器(例如GET请求、测试控制器或标记了<code>SkipActionFilterAttribute</code>特性的方法)。然后获取请求信息和输入参数,创建<code>HttpApiLogEntity</code>对象并插入数据库。如果模型状态无效(入参有误),则构造错误响应,更新日志记录并返回错误响应。</p>
<h3 id="9-动作执行后">9. 动作执行后</h3>
<pre><code class="language-csharp">/// &lt;summary&gt;
/// 执行Action之后
/// &lt;/summary&gt;
/// &lt;param name="context"&gt;&lt;/param&gt;
public void OnActionExecuted(ActionExecutedContext context)
{
    SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType&lt;SkipActionFilterAttribute&gt;()
       .FirstOrDefault();
    // 判断是否存在 SkipActionFilterAttribute 特性
    bool hasSkipActionFilter = skipActionFilterAttribute != null;
    var (method, path, clientIp) = GetRequestInfo(context.HttpContext);
    string pathSmall = context.HttpContext.Request.Path.ToString();
    if (method == "GET" || pathSmall.Contains("Test") || hasSkipActionFilter) return; // 忽略GET请求和测试控制器

    string requestId = context.HttpContext.TraceIdentifier;
    HttpApiLogEntity? httpApiLogEntity = dbContext.HttpApiLogEntity.GetFirst(x =&gt; x.RequestId == requestId);

    if (httpApiLogEntity != null)
    {
      string outParam = GetOutParam(context);
      httpApiLogEntity.OutParam = outParam;
      httpApiLogEntity.ResponseStaus = GetResponseStatus(pathSmall, outParam);
      httpApiLogEntity.EndTime = DateTime.Now;

      dbContext.HttpApiLogEntity.Update(httpApiLogEntity);
    }
}
</code></pre>
<p>在动作方法执行后,同样判断是否应跳过该过滤器。然后根据请求ID从数据库中获取之前插入的日志记录,获取输出参数和响应状态,更新日志记录的输出参数、响应状态和结束时间。</p>
<h2 id="三使用过滤器">三、使用过滤器</h2>
<h3 id="1-注册过滤器">1. 注册过滤器</h3>
<p>在<code>Startup.cs</code>文件的<code>ConfigureServices</code>方法中注册过滤器:</p>
<pre><code class="language-csharp">public void ConfigureServices(IServiceCollection services)
{
    // 其他服务注册...
    services.AddControllers(options =&gt;
    {
      options.Filters.Add&lt;GlobalActionFilter&gt;();
    });
}
</code></pre>
<p>这样,该过滤器将应用于所有控制器的动作方法。如果只希望应用于特定控制器或动作方法,可以在控制器类或动作方法上添加<code></code>特性。</p>
<h3 id="2-示例">2. 示例</h3>
<p>假设我们有一个简单的控制器:</p>
<pre><code class="language-csharp">using Microsoft.AspNetCore.Mvc;

namespace Project.Controllers
{
    ")]
   
    public class WeatherForecastController : ControllerBase
    {
      
      public IActionResult Post( WeatherForecast forecast)
      {
            // 处理逻辑...
            return Ok(new { Message = "Success" });
      }
    }
}
</code></pre>
<p>当发送POST请求到该控制器的<code>Post</code>方法时,<code>GlobalActionFilter</code>将记录请求和响应信息到数据库中。</p>
<h2 id="四总结">四、总结</h2>
<p>通过自定义动作过滤器,我们可以方便地在ASP.NET Core应用中记录Http API日志。这不仅有助于系统的调试和维护,还能提供有价值的运行时信息。在实际应用中,可以根据具体需求对过滤器进行扩展和优化,例如添加更多的日志字段、支持不同的日志存储方式等。</p>
<p>希望本文能帮助你理解和使用ASP.NET Core中的请求过滤器来记录API日志。如果有任何问题或建议,欢迎留言讨论。</p><br><br>
来源:https://www.cnblogs.com/cyfj/p/18849970
頁: [1]
查看完整版本: ASP.NET Core中使用请求过滤器记录Http API日志