过滤器实现接口操作日志记录

利用过滤器特性优雅实现接口操作日志记录

在Web开发中,记录用户操作日志是常见需求。传统方式需要在每个接口中重复编写日志代码,不仅繁琐且容易遗漏。本文将介绍如何通过ASP.NET Core过滤器特性,实现零侵入的操作日志记录方案。

一、传统方式 vs 过滤器方案

传统方式痛点:

public async Task<IActionResult> AddUser([FromBody] UserDto dto)
{
    // 业务逻辑前
    LogStart(dto); 
    
    try {
        // 业务逻辑
        LogSuccess(result);
    } catch {
        LogError(ex);
    }
}

过滤器方案优势:

  • ✅ 业务与日志分离

  • ✅ 统一异常处理

  • ✅ 避免重复代码

  • ✅ 支持动态日志描述

二、实现步骤

1. 创建日志过滤器
`using MySite.Entities;
using MySite.Exceptions;
using MySite.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using SqlSugar;
using System.Security.Claims;

namespace MySite.Filters
{
    /// <summary>
    /// 日志记录过滤器
    /// @author : chenjingzhi
    /// @date : 2025/7/3 18:41:18 
    /// @desc : 
    /// </summary>
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
    public class OperationLogAttribute : Attribute, IFilterFactory
    {
        public string _operationDetail { get; }
        public bool IsReusable => false; // 确保每次请求都创建新实例

        public OperationLogAttribute(string operationDetail)
        {
            _operationDetail = operationDetail;
        }

        // 通过 DI 容器创建 Filter 实例
        public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
        {
            // 从 DI 容器中获取 OperationLogFilter 实例
            var filter = serviceProvider.GetRequiredService<OperationLogFilter>();
            // 动态设置操作信息
            filter.SetOperationInfo(_operationDetail);
            return filter;
        }
    }

    /// <summary>
    /// 
    /// </summary>
    public class OperationLogFilter : IAsyncActionFilter
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly ISqlSugarClient _sqlSugarClient;
        private string _operationDetail;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="httpContextAccessor"></param>
        /// <param name="sqlSugarClient"></param>
        /// <exception cref="ArgumentNullException"></exception>
        public OperationLogFilter(IHttpContextAccessor httpContextAccessor, ISqlSugarClient sqlSugarClient)
        {
            _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
            _sqlSugarClient = sqlSugarClient;

        }

        // 动态设置操作信息
        public void SetOperationInfo(string operationDetail)
        {
            _operationDetail = operationDetail;
        }

        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {

            // 获取方法信息
            var methodName = context.ActionDescriptor.DisplayName; // 方法名
            var className = context.Controller.GetType().Name; // 类名
            var parameters = context.ActionArguments; // 方法参数
            var startTime = DateTime.Now; // 开始时间

            // 模拟获取当前用户(实际可以从上下文或认证信息中获取)
            var currentUser = GetCurrentUserId(_httpContextAccessor);
            var operateSystem = HttpContextClient.GetClientOperatingSystem(_httpContextAccessor.HttpContext) + "|" +
            HttpContextClient.GetClientBrowser(_httpContextAccessor.HttpContext);
            try
            {
                var resultContext = await next();

                // 获取操作结果
                var operationResult = "成功";
                var outputParam = string.Empty;

                if (resultContext.Exception != null)
                {
                    // 如果发生异常,记录失败日志
                    operationResult = "失败";
                    if (resultContext.Exception is ResultMessageException resultMessageException)
                    {
                        // 如果是 ResultMessageException,记录异常消息
                        outputParam = resultMessageException.Message;
                    }
                    else
                    {
                        // 如果是其他异常,记录异常信息
                        outputParam = resultContext.Exception.Message;
                    }
                }
                else if (resultContext.Result != null)
                {
                    if (resultContext.Result is ObjectResult objectResult)
                    {
                        // 如果是 ObjectResult,序列化返回值
                        outputParam = JsonConvert.SerializeObject(objectResult.Value);
                    }
                    else if (resultContext.Result is StatusCodeResult statusCodeResult)
                    {
                        // 如果是状态码结果,记录状态码
                        outputParam = $"StatusCode: {statusCodeResult.StatusCode}";
                        if (statusCodeResult.StatusCode >= 400)
                        {
                            operationResult = "失败";
                        }
                    }
                }

                // 根据方法名生成日志信息
                await GenerateLogMessage(methodName, className, _operationDetail, currentUser, operateSystem, startTime, parameters, operationResult, outputParam);


            }
            catch (ResultMessageException ex)
            {
                // 捕获 ResultMessageException 并记录失败日志
                await GenerateLogMessage(methodName, className, _operationDetail, currentUser, operateSystem, startTime, parameters, "失败", ex.Message);
                throw; // 重新抛出异常,确保全局异常处理仍然生效
            }
            catch (Exception ex)
            {
                // 记录其他异常日志
                await GenerateLogMessage(methodName, className, _operationDetail, currentUser, operateSystem, startTime, parameters, "失败", ex.Message);
                throw; // 重新抛出异常,确保全局异常处理仍然生效
            }
        }

        /// <summary>
        /// 模拟获取当前用户ID
        /// </summary>
        private string GetCurrentUserId(IHttpContextAccessor httpContextAccessor)
        {
            // 实际可以从 HttpContext.User 中获取用户信息
            var userIdClaim = httpContextAccessor.HttpContext?.User?.Claims
        .FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);

            return userIdClaim?.Value ?? "Anonymous";
        }


        /// <summary>
        /// 生成日志信息 此处写入数据库,也可写日志文件或其他存储
        /// </summary>
        /// <param name="methodName">http方法名</param>
        /// <param name="className">类名</param>
        /// <param name="operationDetail">操作详情</param>
        /// <param name="currentUser">当前用户</param>
        /// <param name="operateSystem">操作系统</param>
        /// <param name="startTime">开始时间</param>
        /// <param name="parameters">入参</param>
        /// <param name="operationResult">操作结果</param>
        /// <param name="outputParam">出参</param>
        /// <returns></returns>
        private async Task GenerateLogMessage(string methodName, string className, string operationDetail, string currentUser,
            string operateSystem, DateTime startTime, IDictionary<string, object> parameters, string operationResult, string outputParam)
        {

            try
            {
                var operationLog = new Tb_Operation_Logs
                {
                    Id = Guid.NewGuid().ToString(),
                    UserId = currentUser,
                    OperationTime = startTime,
                    OperationType = 1,
                    OperationDetail = operationDetail,
                    OperationResult = operationResult,
                    Method = methodName,
                    FailureReason = operationResult == "失败" ? outputParam : string.Empty,
                    OperationSystem = operateSystem,
                    IpAddress = _httpContextAccessor.HttpContext?.Connection?.RemoteIpAddress?.ToString(),
                    InputParam = JsonConvert.SerializeObject(parameters),
                    OutputParam = operationResult == "成功" ? outputParam : string.Empty
                };
                await _sqlSugarClient.Insertable(operationLog).ExecuteCommandAsync();

            }
            catch (Exception ex)
            {
                Console.WriteLine($"系统错误,写入操作日志错误: {ex.Message}");
            }
        }
    }
}
2. 服务注册(Program.cs)
builder.Services
    .AddHttpContextAccessor()
    .AddTransient<OperationLogFilter>();
3. 控制器使用示例
[ApiController]
[Route("api/users")]
public class UserController : ControllerBase
{
    [HttpPost]
    [OperationLog("创建新用户")]
    public IActionResult CreateUser([FromBody] UserCreateDto dto)
    {
        // 纯净的业务逻辑
        var newUser = _userService.Create(dto);
        return CreatedAtAction(nameof(GetUser), new { id = newUser.Id }, newUser);
    }

    [HttpDelete("{id}")]
    [OperationLog("删除用户账户")]
    public IActionResult DeleteUser(string id)
    {
        _userService.Delete(id);
        return NoContent();
    }
}

三、方案亮点

1. 动态日志描述

支持在特性中直接定义可读性强的操作描述:

[OperationLog("审批报销单")]
public IActionResult ApproveExpense(...)
2. 智能异常处理
sequenceDiagram
    participant Client
    participant Filter
    participant Controller
    participant Database
    
    Client->>Filter: 请求接口
    Filter->>Controller: 执行前置逻辑
    Controller-->>Filter: 返回结果/异常
    alt 成功
        Filter->>Database: 记录成功日志
    else 失败
        Filter->>Database: 记录错误日志
        Filter-->>Client: 返回错误信息
    end
3. 完整上下文捕获

日志包含的关键信息:

{
  "UserId": "u12345",
  "Operation": "修改订单状态",
  "Params": {"orderId":1001, "status":"SHIPPED"},
  "System": "Windows|Chrome",
  "IP": "192.168.1.100",
  "Result": "成功",
  "Duration": "120ms"
}

四、高级优化技巧

1. 敏感数据过滤
private string SanitizeInput(object input)
{
    var json = JsonConvert.SerializeObject(input);
    return Regex.Replace(json, @"(""password\"":\s*"")([^""]+)","$1***");
}
2. 性能优化
// 异步批量写入
private static readonly ConcurrentQueue<OperationLog> _logQueue = new();

// 后台任务定期处理
builder.Services.AddHostedService<LogBackgroundService>();
3. 动态操作描述
// 支持占位符
[OperationLog("更新#{id}订单")]

// 在过滤器中解析
var detail = _operationDetail.Replace("#{id}", context.ActionArguments["id"].ToString());

五、适用场景对比

场景过滤器方案AOP方案中间件方案
需要方法参数
需要返回值
需要具体操作描述
全局日志记录
与业务逻辑解耦

六、总结

通过过滤器实现操作日志记录具有以下核心优势:

  1. 关注点分离 - 业务代码保持纯净

  2. 声明式编程 - 通过特性配置日志行为

  3. 上下文完备 - 完整获取方法参数和返回值

  4. 异常安全 - 内置统一的错误处理机制

  5. 灵活扩展 - 支持动态日志模板和敏感数据处理

最佳实践建议:对于管理后台、财务系统等高安全要求场景,建议组合使用过滤器日志+数据库审计功能,实现操作留痕的双重保障。

// 最终的优雅使用方式
[OperationLog("${userName}修改了部门设置")]
public IActionResult UpdateDepartment(DepartmentUpdateDto dto)
{
    // 只需关注核心业务逻辑
    _deptService.Update(dto);
    return Ok();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gene Z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值