• 从场景、日志常见的收集方案模块来介绍日志组件的改造方案
  • 发布于 2个月前
  • 238 热度
    0 评论
  • 太伤人
  • 1 粉丝 31 篇博客
  •   
一、场景
网上采购流程中,多个系统组成了采购交易生态圈,为用户提供线上采购服务能力。而每个系统几乎都离不开日志记录。

日志分类

记录的日志可以分为两类:操作流转日志和系统日志,这两类日志面向的群体和定位也有一定的区别。其中系统日志主要面向研发层面,为研发定位问题提供依据和执行链路回溯,一般可读性要求比较低,检索也需要一定的技巧和条件。


而操作日志主要面向用户和保障人员,具备较强的可读性,主要针对用户对某一场景做出操作后进行留痕,比如用户的审批流程,可以从操作日志列表直观看到,经过了哪些流程,哪些人员审批和审批的结果。


面临的问题
笔者目前所接触的业务,每个系统中都有操作日志记录和查询的需求。早期的需求较为单一,设计也较为简单。一张公共的日志表,为若干个系统提供记录和查询服务,随着版本衍进,记录的场景变的复杂,伴随人员变更,该表字段和使用方式开始失去了约束。

改造的主要方向
为了统一收口日志的能力,也适逢部门内部系统进行服务拆分改造,针对目前的现状做一次日志系统的升级。
1、表结构改造
冗余一套基础通用的结构字段,同时满足部门未来三年的数据增长量。
2、记录和检索服务能力改造
增加 starter 二方包配合前端组件渲染,并最大限度支持历史遗留代码改造。平台记录的数据一定保障具有可读性,简洁的同时又能表达重要信息。那么如何"优雅"的记录操作日志并展开描述呢?如下图:

我们可以将单纯的文字记录进行组成元素拆分,如:时间、业务标识、场景、子业务标识、操作人等描述字段。针对场景的操作行为,皆以累加的方式进行记录,那么就可以达到场景的数据变更或对比目的,一目了然。

二、实现方式
确认了字段组成和渲染的方案后,该如何收集操作日志呢?考量到业务方代码改造量,精简了两种收集方案。
1、无侵入型方案
借助开源数据库日志订阅组件(Canal),订阅相关库的 binlog,从底层分析是哪些业务做了更改,根据场景反向生成日志记录,业务方无需感知和改动。
流程如下:

该方案的能够做到和业务逻辑完全解耦,接入方改动工作量几乎没有。但是局限性太大,只能监控到表记录变更的日志,针对于 RPC 调用和需要定制化数据的场景无法做到完美支持,由于部门业务方场景需要定制化、且包含有些较为复杂的记录需求,所以该方案 Pass。

2、浸入型方案
针对业务方提供 Starter 集成,客户端提供 Aop+Annotation 拦截定制化收集和 Component 硬编码注入实现收集实例两种方式。
Starter 内部集成了日志文案模版的概念,模板内支持集成 SpEL 表达式,可在模版中使用占位符,占位符在解析过程中将自动尝试采用方法的入参和出参以及自定义函数作为解析依赖。

如下:
@LogRecord(
        success = "执行签到操作,修改 tag 为:#{#model.getTag()};ids 为:#{T(java.lang.String).join(',',#model.getIds())}",
        bizId = "#{#model.getBizId()}",
        childBizId = "#{#_resource.getChildBizId()}",
        context = "#{@demoService.getContext(#_resource.getContextIndex())}",
        identityType = "#{#model.getIdentity()}"
        )

@PostMapping("full")
public ResponseModel fullDemo(@RequestBody RequestModel model) {
     // 堆代码 duidaima.com
    // 执行业务逻辑...
    ResponseModel responseModel = new ResponseModel();
    responseModel.setCode("200");
    responseModel.setRequestModel(model);
    responseModel.setChildBizId("biz_001");
    responseModel.setContextIndex("stash");
    return responseModel;
}

Aop 拦截流程如下:

自定义注解拦截模式优点是代码集成简洁,无需关注方法体内部逻辑。但是针对较为复杂的自定义文案模版扩展实现起来就没那么简单了,所以针对较多的定制化扩展需求时,比如超链接、关键字脱敏、快照等场景需要配合硬编码集成到方法内部进行配合改动。

三、历史数据迁移
确定了日志收集的方案后,接下来需要考虑历史数据迁移的问题。基于部门服务拆分的大背景下,本次升级重新设计了日志表的字段,所以无法平滑数据迁移,涉及到字段的聚合、重新映射、文件和超文本整合等问题。而这些数据的记录逻辑散落在多个业务方,可能还存在业务方之间对同一字段理解和使用不同。所以为了屏蔽业务差异化,在迁移方案设计中,需要将清理过程(如:字段二次映射、文件聚合记录等场景)开放给业务方自行处理。为了减轻业务方改造工作量,迁移方案引入了轻量级的规则引擎 QLExpress,主要作用是解析映射规则和宏处理 (详情可参考:https://github.com/alibaba/QLExpress#5macro-%E5%AE%8F%E5%AE%9A%E4%B9%89) 。

我们按照日志的场景 ID 为维度进行数据迁移,业务方自行梳理和归类场景,并提供场景的字段映射和处理表达式规则。单场景迁移清洗流程如下:

上流程中我们构造了基础的宏指令如:映射、截取、拼接等减短了表达式的内容长度。经调研,历史业务中存在较为复杂的文件和链接渲染,而且拆分后的业务服务将继续延用该模式。针对复杂文件和链接渲染节点场景,该场景包含大量的自定义逻辑,不适合规则表达式配置。对于场景下复杂字段处理,采用了责任链的变种设计,Pipeline 任务和单链表传播方向,实现了定制化字段逻辑处理链路。

设计如下:

Chain 传播过程:
public class ChainContext<T, V> {
    /**
     * 存储信息<处理节点>
     * */
    AbstractHandler<T, V> handler;

    /**
     * 下一个节点
     * */
    ChainContext<T, V> next;

    /**
     * 遍历 pipeline 链表执行
     * */
    public void fireChainRun(DataVector<T, V> arg){
        handler.invoke(arg);
        ChainContext<T, V> next = this.next;
        if (null!=next){
            next.fireChainRun(arg);
        }
    }

    public AbstractHandler<T, V> getHandler(){
        return this.handler;
    }

    public ChainContext<T, V> getNext(){
        return this.next;
    }

    public void setHandler(AbstractHandler<T, V> handler){
        this.handler = handler;
    }

    public void insertNext(ChainContext<T, V> chain){
        ChainContext<T, V> oldNext = getNext().next;
        this.next = chain;
        if (null!=oldNext){
            chain.next = oldNext;
        }
    }

    public void setNext(ChainContext<T, V> chain){
        this.next = chain;
    }
}

四、总结
本文从场景、日志常见的收集方案模块来介绍的笔者部门日志组件的改造方案,也涉及了重新设计表结构后的历史数据迁移思路。简单的日志记录场景,也会随着使用场景变得复杂起来,探索方案的过程中收获颇多,也欢迎大伙探讨。
用户评论