Skip to content

操作日志

本页将帮助你快速了解 Ape‑Volo‑Admin 的操作日志使用与配置。

你将获得

  • 如何开启/关闭操作日志
  • 支持的采集字段与数据来源
  • 直写数据库 vs 经 Redis 队列异步入库
  • 全局注册方式与忽略记录的注解
  • 常见问题与最佳实践(脱敏、体积控制、保留策略)

操作过滤器

位置:Ape.Volo.Infrastructure/ActionFilter/OperateLogFilter.cs

csharp
/// <summary>
/// 操作日志过滤器
/// </summary>
public class OperateLogFilter : IAsyncActionFilter
{
    private readonly IOperateLogService _operateLogService;
    private readonly ISettingService _settingService;
    private readonly IBrowserDetector _browserDetector;
    private readonly ISearcher _ipSearcher;

    public OperateLogFilter(IOperateLogService operateLogService, ISearcher searcher,
        ISettingService settingService, IBrowserDetector browserDetector)
    {
        _operateLogService = operateLogService;
        _settingService = settingService;
        _browserDetector = browserDetector;
        _ipSearcher = searcher;
    }

    public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        return Execute(context, next);
    }

    /// <summary>
    /// 执行审计功能
    /// </summary>
    /// <param name="context"></param>
    /// <param name="next"></param>
    /// <returns></returns>
    private async Task Execute(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        try
        {
            var sw = new Stopwatch();
            sw.Start();
            var resultContext = await next();
            sw.Stop();

            var action = (ControllerActionDescriptor)context.ActionDescriptor;
            if (action.MethodInfo.IsDefined(typeof(NotOperateAttribute), false))
            {
                return;
            }

            //执行结果
            //var action = context.ActionDescriptor as ControllerActionDescriptor;
            //var isTrue = action.MethodInfo.IsDefined(typeof(DescriptionAttribute), false);
            var saveDb = await _settingService.GetSettingValue<bool>("IsOperateLogSaveDB");
            if (saveDb && resultContext.Result.IsNotNull())
            {
                var operateLog = CreateOperateLog(context);
                operateLog.ResponseData = resultContext.Result switch
                {
                    ContentResult contentResult => contentResult.Content,
                    NoContentResult okResult => okResult.ToJson(),
                    OkObjectResult okResult => okResult.Value?.ToJson(),
                    FileContentResult fileContentResult => GetFileContentResult(fileContentResult),
                    ObjectResult objectResult => objectResult.Value?.ToJson(),
                    _ => null // 处理其他未知类型
                };


                //用时
                operateLog.ExecutionDuration = sw.ElapsedMilliseconds;

                if (App.GetOptions<SystemOptions>().UseRedisCache &&
                    App.GetOptions<MiddlewareOptions>().RedisMq.Enabled)
                {
                    // 实时队列
                    // await App.GetService<ICache>().GetDatabase()
                    //     .ListLeftPushAsync(MqTopicNameKey.OperateLogQueue, operateLog.ToJson());

                    //延迟队列
                    var stopTimeStamp = DateTime.Now.AddSeconds(10).ToUnixTimeStampSecond();
                    await App.GetService<ICache>().GetDatabase()
                        .SortedSetAddAsync(MqTopicNameKey.OperateLogQueue, operateLog.ToJson(), stopTimeStamp);
                }
                else
                {
                    await Task.Factory.StartNew(() => _operateLogService.CreateAsync(operateLog))
                        .ConfigureAwait(false);
                }
            }
        }
        catch (Exception ex)
        {
            var remoteIp = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "0.0.0.0";
            var ipAddress = _ipSearcher.Search(remoteIp);
            LogHelper.WriteLog(ExceptionHelper.ErrorFormat(context.HttpContext, remoteIp, ipAddress, ex,
                App.HttpUser?.Account,
                _browserDetector.Browser?.OS, _browserDetector.Browser?.DeviceType, _browserDetector.Browser?.Name,
                _browserDetector.Browser?.Version), null);
        }
    }

    /// <summary>
    /// 创建审计对象
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    private OperateLog CreateOperateLog(ActionExecutingContext context)
    {
        //略
        return operateLog;
    }

    private string GetFileContentResult(FileContentResult fileContentResult)
    {
        //略
    }
}

功能概览

操作日志用于记录接口调用的关键信息,便于审计与问题追溯。其核心通过全局 IAsyncActionFilter 拦截请求,收集请求上下文、返回结果、执行耗时、客户端环境等信息,并根据开关选择“直接写库”或“写入 Redis 队列由后台异步入库”。

为什么需要操作日志(逐项说明系统带来的具体好处):

  1. 审计合规(Audit)

    • 作用:保留谁在何时、从哪儿、通过哪个接口对系统做了什么的不可篡改记录。
    • 带来价值:满足审计与合规要求(如变更审计、合规检查、法务取证),降低合规风险。
  2. 事故溯源与根因定位

    • 作用:记录请求参数、返回结果与执行耗时,形成完整调用快照。
    • 带来价值:快速重现问题场景(重放输入)、定位异常接口或异常请求链,缩短故障定位时间(MTTR)。
  3. 安全与异常检测

    • 作用:捕获异常请求、异常返回、频繁失败或异常耗时的调用。
    • 带来价值:检测越权、暴力破解、异常流量或滥用行为,便于触发告警与封禁策略。
  4. 责任追踪与审查

    • 作用:将操作与用户/会话绑定(Account、IP、User‑Agent 等)。
    • 带来价值:明确责任人,支持事后核查、审批与争议处理。
  5. 性能监控与容量规划

    • 作用:按接口统计 ExecutionDuration、失败率与响应数据量。
    • 带来价值:发现慢接口、识别热点 API、为优化、扩容与限流提供数据依据。
  6. 运营与产品分析

    • 作用:记录业务事件(谁做了哪类操作、请求参数的业务维度)。
    • 带来价值:分析功能使用率、用户行为路径、为产品决策与优先级提供量化支持。
  7. 回滚与补偿操作的依据

    • 作用:提供操作历史与输入快照。
    • 带来价值:在误操作或数据异常时,用日志作为回滚或补偿操作的参考。
  8. 测试与质量保障

    • 作用:在灰度/回归验证期间记录外部交互与响应。
    • 带来价值:验证变更影响,评估回归风险,帮助构建更可靠的测试用例。
  9. 运营告警与自动化响应

    • 作用:基于日志统计触发阈值告警(消费积压、落库失败、异常增长等)。
    • 带来价值:自动化运维响应与故障降级,降低人工处理成本与业务中断风险。
  10. 合理化存储与隐私控制(实践价值)

    • 作用:结合脱敏、截断、白名单策略控制落库内容与体积。
    • 带来价值:在满足审计要求的同时控制成本、降低泄露风险,并满足隐私合规(如脱敏、保留策略)。

简短建议(落地指引):

  • 必需字段优先采集(路由、用户、时间、IP、参数摘要、耗时、结果摘要),避免直接落库敏感明文;
  • 高流量/大体积场景优先使用异步队列(Redis 延迟队列)以降低接口 RT;
  • 制定日志保留与归档策略,结合监控对消费、失败率和存储成本进行定期评估。

开关与前置条件

  • IsOperateLogSaveDB(系统设置项):是否保存操作日志到数据库。
    • ISettingService.GetSettingValue<bool>("IsOperateLogSaveDB") 读取。
  • SystemOptions.UseRedisCache(配置项):启用后端 Redis。
  • MiddlewareOptions.RedisMq.Enabled(配置项):启用 Redis 消息队列模块。

组合行为:

  • 当 IsOperateLogSaveDB=true 且 UseRedisCache=true 且 RedisMq.Enabled=true 时,操作日志写入 Redis(延迟队列)等待后台消费入库。
  • 当 IsOperateLogSaveDB=true,但任一 Redis 开关为 false 时,直接异步写入数据库。
  • 当 IsOperateLogSaveDB=false 时,不进行持久化(仍会执行拦截但不落库)。

注意

IsOperateLogSaveDB 来源于系统设置服务(而非 appsettings.json)。请确保后台已配置该键,或在初始化阶段提供默认值。

采集范围与字段

默认情况下,所有未显式忽略的接口都会被记录,包含但不限于:

  • 路由信息:Area、Controller、Action、Method、RequestUrl
  • 文案描述:[Description] 特性值(支持多语言转换 App.L.R
  • 请求参数:Query/Form/Body 汇总(HttpHelper.GetAllRequestParams
  • 调用人:App.HttpUser?.Account,登录接口特殊处理为提交的用户名
  • 客户端环境:IP、地理位置(ISearcher)、User-Agent、操作系统、设备类型、浏览器名称与版本(IBrowserDetector
  • 执行耗时:毫秒(Stopwatch
  • 返回结果:根据结果类型提取(OkObjectResult/ObjectResult/ContentResult/NoContentResult/FileContentResult等)
    • 对文件下载,记录文件名/大小/MIME 并附加 MD5 摘要

字段示例(实体 OperateLog):

  • Id、CreateBy、CreateTime
  • Area、Controller、Action、Method、Description、RequestUrl
  • RequestParameters、RequestIp、IpAddress、UserAgent、OperatingSystem、DeviceType、BrowserName、Version
  • ResponseData、ExecutionDuration

工作模式

  • 直接写库:当未启用 Redis 或未启用 RedisMq 时,调用 _operateLogService.CreateAsync(operateLog) 入库。
  • Redis 队列:当同时启用 Redis 与 RedisMq 时,向 MqTopicNameKey.OperateLogQueue 写入延迟任务:
    • 采用有序集合(SortedSet)写入,score 为未来 10 秒的 UNIX 时间戳,实现“延迟队列”。
    • 后台消费方(独立任务/服务)按到期时间拉取并批量入库,降低接口响应时延与写库压力。

参见《消息队列(Redis)》文档了解队列与消费者的部署要点与运维监控。

全局注册

将操作日志过滤器注册为全局过滤器即可对所有接口生效:

csharp
services.AddControllers(options =>
{
        options.Filters.Add<OperateLogFilter>();
});

依赖项请确保已注册:IOperateLogServiceISettingServiceIBrowserDetectorISearcher 等。

忽略记录

对不需要记录的接口,可使用 [NotOperate] 注解标记方法(或控制器),过滤器将自动跳过:

csharp
[HttpGet]
[AllowAnonymous]
[NotOperate]
[Route("index")]
public Task<ActionResult> Index()
{
    return Task.FromResult(Ok(new OperateResult { IsSuccess = true }));
}

常见忽略场景:健康检查、内部回调、文件直传签名、频繁轮询等。

最佳实践

  • 脱敏:对密码、令牌、身份证号、手机号等敏感字段进行掩码或过滤,避免明文落库。
  • 体积控制:ResponseData 宜限制体积(如分页数据仅记录查询条件与总量,或对大对象进行截断)。
  • 白/黑名单:结合路由前缀或标签进行有选择记录,降低存储压力。
  • 异步入库:在高并发场景建议开启 RedisMq 模式,减少接口 RT。
  • 监控与告警:对消费堆积、落库失败与异常进行监控,避免日志丢失。

常见问题(FAQ)

  • 为什么没有生成日志?

    • 检查 IsOperateLogSaveDB 设置是否为 true。
    • 检查是否被 [NotOperate] 标记忽略。
    • 若启用队列,确认 Redis 连接与消费者服务运行正常。
  • 返回文件如何记录?

    • 通过 GetFileContentResult 仅记录元信息与 MD5 摘要,不保存文件二进制,避免库膨胀。
  • 多语言描述如何生效?

    • 通过 [Description] 特性与 App.L.R 转换,确保资源文件存在对应键值。

相关阅读:

版权所有 © 2021-2026 ApeVolo-Team