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

一、过滤器简介

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

二、自定义GlobalActionFilter类

1. 类定义

/// <summary>
/// Action过滤器
/// </summary>
public class GlobalActionFilter : IActionFilter, IOrderedFilter
{
    /// <summary>
    /// 构造
    /// </summary>
    /// <param name="logger"></param>
    public GlobalActionFilter(SugarDbContext dbContext, ILogger<GlobalActionFilter> logger)
    {
        this.dbContext = dbContext;
        this.logger = logger;
    }

    private readonly SugarDbContext dbContext;
    private readonly ILogger<GlobalActionFilter> logger;

    /// <summary>
    /// 过滤器执行顺序
    /// </summary>
    public int Order => 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<TesResponse>();
                return apiResponse?.ReturnCode == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
            }
            else
            {
                var apiResponse = outParam.ToObject<ApiResponse>();
                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;
        }
    }

    /// <summary>
    /// 执行Action之前
    /// </summary>
    /// <param name="context"></param>
    public void OnActionExecuting(ActionExecutingContext context)
    {
        SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType<SkipActionFilterAttribute>().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<string>? errors = context.ModelState.SelectMany(x => x.Value.Errors)
                .Select(x => 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);
        }
    }

    /// <summary>
    /// 执行Action之后
    /// </summary>
    /// <param name="context"></param>
    public void OnActionExecuted(ActionExecutedContext context)
    {
        SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType<SkipActionFilterAttribute>()
            .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 => 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);
        }
    }
}

该类实现了IActionFilter接口,该接口定义了OnActionExecutingOnActionExecuted方法,分别在动作方法执行前和执行后调用。同时实现了IOrderedFilter接口,用于指定过滤器的执行顺序。

2. 构造函数

/// <summary>
/// 构造
/// </summary>
/// <param name="logger"></param>
public GlobalActionFilter(SugarDbContext dbContext, ILogger<GlobalActionFilter> logger)
{
    this.dbContext = dbContext;
    this.logger = logger;
}

private readonly SugarDbContext dbContext;
private readonly ILogger<GlobalActionFilter> logger;

构造函数接收数据库上下文对象SugarDbContext和日志记录器ILogger<GlobalActionFilter>,用于后续的数据库操作和日志记录。

3. 执行顺序

/// <summary>
/// 过滤器执行顺序
/// </summary>
public int Order => 2; // 设置执行顺序

通过实现IOrderedFilter接口的Order属性,指定该过滤器的执行顺序为2。数值越小,过滤器越先执行。

4. 获取请求信息

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);
}

该方法从HttpContext中提取请求方法、完整请求路径和客户端IP地址,并以元组形式返回。

5. 获取输入参数

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

ActionExecutingContextActionArguments中获取动作方法的输入参数,并序列化为JSON字符串。如果没有参数,则返回”Null”。

6. 获取输出参数

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

ActionExecutedContextResult中获取动作方法的返回结果,如果是JsonResult类型,则将其值序列化为JSON字符串返回。

7. 获取响应状态

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<TesResponse>();
            return apiResponse?.ReturnCode == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
        }
        else
        {
            var apiResponse = outParam.ToObject<ApiResponse>();
            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;
    }
}

根据响应结果和请求路径判断响应状态。如果是特定接口TesTaskNotice,则根据TesResponseReturnCode判断;否则根据ApiResponseCode判断。如果序列化失败,则记录错误日志并返回未知状态。

8. 动作执行前

/// <summary>
/// 执行Action之前
/// </summary>
/// <param name="context"></param>
public void OnActionExecuting(ActionExecutingContext context)
{
    SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType<SkipActionFilterAttribute>().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<string>? errors = context.ModelState.SelectMany(x => x.Value.Errors)
           .Select(x => 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);
    }
}

在动作方法执行前,首先判断是否应跳过该过滤器(例如GET请求、测试控制器或标记了SkipActionFilterAttribute特性的方法)。然后获取请求信息和输入参数,创建HttpApiLogEntity对象并插入数据库。如果模型状态无效(入参有误),则构造错误响应,更新日志记录并返回错误响应。

9. 动作执行后

/// <summary>
/// 执行Action之后
/// </summary>
/// <param name="context"></param>
public void OnActionExecuted(ActionExecutedContext context)
{
    SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType<SkipActionFilterAttribute>()
       .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 => 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);
    }
}

在动作方法执行后,同样判断是否应跳过该过滤器。然后根据请求ID从数据库中获取之前插入的日志记录,获取输出参数和响应状态,更新日志记录的输出参数、响应状态和结束时间。

三、使用过滤器

1. 注册过滤器

Startup.cs文件的ConfigureServices方法中注册过滤器:

public void ConfigureServices(IServiceCollection services)
{
    // 其他服务注册...
    services.AddControllers(options =>
    {
        options.Filters.Add<GlobalActionFilter>();
    });
}

这样,该过滤器将应用于所有控制器的动作方法。如果只希望应用于特定控制器或动作方法,可以在控制器类或动作方法上添加[TypeFilter(typeof(GlobalActionFilter))]特性。

2. 示例

假设我们有一个简单的控制器:

using Microsoft.AspNetCore.Mvc;

namespace Project.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class WeatherForecastController : ControllerBase
    {
        [HttpPost]
        public IActionResult Post([FromBody] WeatherForecast forecast)
        {
            // 处理逻辑...
            return Ok(new { Message = "Success" });
        }
    }
}

当发送POST请求到该控制器的Post方法时,GlobalActionFilter将记录请求和响应信息到数据库中。

四、总结

通过自定义动作过滤器,我们可以方便地在ASP.NET Core应用中记录Http API日志。这不仅有助于系统的调试和维护,还能提供有价值的运行时信息。在实际应用中,可以根据具体需求对过滤器进行扩展和优化,例如添加更多的日志字段、支持不同的日志存储方式等。

希望本文能帮助你理解和使用ASP.NET Core中的请求过滤器来记录API日志。如果有任何问题或建议,欢迎留言讨论。

来源链接:https://www.cnblogs.com/cyfj/p/18849970

© 版权声明
THE END
支持一下吧
点赞14 分享
评论 抢沙发
头像
请文明发言!
提交
头像

昵称

取消
昵称表情代码快捷回复

    暂无评论内容