业务维度digest日志的记录与监控方案

news2024/11/27 10:25:36

需求

​   为了满足从业务整体的维度 实现监控和链路复原,我们希望对于一个业务接口,记录一行请求日志,并通过某个 Unique Id(如UserId、OrderId)将多行日志关联起来,最终产出一批和业务强相关的数据,帮助业务或管理层更加清晰、及时的了解到业务变化情况,做出更合理的业务发展判断。

   在需求中,涉及到了digest日志记录、日志数据清洗、日志数据呈现方式等,但在本文档中,我们重点讨论项目中digest日志的记录方案。

digest日志实现时,需要考虑的能力

  • 使用成本(监控配置)
    • 一行日志,包含一个接口请求的所有监控要素,实现从接口的角度复原业务链路情况;
    • 记录规则清晰,持续维护使用文档
    • 对比按需打印的业务日志而言,digest日志具有更高的业务监控价值,按需打印的日志更适用于复原某个接口的处理过程
  • 扩展性
    • 对每个监控要素,新建唯一的 index,新增监控要素时,只需分配新的 index 即可,不影响原有的监控
  • 维护成本
    • 通过添加单测,上线前校验index的唯一性,保证不出现重复index覆盖日志内容

实现方案详情

日志打印框架功能要点:

  • 埋点方法:

    • 在上下文中新增日志对象,在系统的入口、出口及其他关键位置设置埋点,将关键信息填充至日志对象中。针对每个关键信息分配唯一 index,同类型信息使用连续的index
    • 使用 ThreadLocal ,方便在业务流程中,添加日志信息
  • 日志打印方法:

    • 将关键信息按照 LogFiledEnum 中定义的 Index 排序后,依次打印
  • 日志记录不阻塞业务链路:

    • 日志记录过程中,捕获所有异常

digest 日志实现方案

实现流程梳理

时序图

请添加图片描述

说明:

  1. RouterTemplate类是所有业务执行时通用模板,用来完成一些公共的、非业务的操作
  2. AbstractThreadRunnable是提供给业务层、对Runnable二次封装的异步线程执行抽象工具类
  3. 图中除了1、2两个类,其他为digest基础框架实现类

类图:
请添加图片描述

说明:

  1. LogFieldEnum中,name为日志含义名称,其中的属性index表示日志记录的索引位置
  2. AddDigestFromComplexTypeConsumer中,定义了从复杂对象中,解析并记录日志到指定索引的静态方法

日志打印样例:

2022-10-18 12:49:46,251 [0602257d166609738088945222092 0.9.10 - /// - ] INFO IGPAYROUTER-DIGEST - [0602257d166609738088945222092,0,GNETW7CN,][0,OTHER_SYSTEM][1,alipay.aps.payments.userInitiatedPay][2,1.0][3,3863][4,ApsUserInitiatedPaymentRouterService][5,true][6,SUCCESS][7,-][8,RouterApiRequest][9,1022172000000000001][10,-][11,Success][21,3073][22,alipay.aps.payments.userInitiatedPay][23,1.0][24,true][25,SUCCESS][26,Success][27,1022188000000000001][29,-][30,-][31,3.0.1][32,SUCCESS][33,true][40,1022188000000000001][41,1022172000000000001][42,NET][43,-][50,2019101411058602000007700084912][51,adyen-平台][52,US][53,2188410000010007][54,adyen-平台][55,2019101411058602000007700084912][60,ORDER_CODE_PAYMENT][61,false][62,PAY_ROUTE][63,2022101819074101000050006322437][64,VzbzGHHzhjLCRBMEsSkqJpUWWlBCyFcU][65,-][66,-][67,ACCEPT][68,PAY_ORDER][69,false][70,-][71,-][72,-][73,-][74,ACCEPT][75,-][76,1022188000000000001|2188410000010007|2019101411058602000007700084912][77,-][78,1022188000000000001@2188410000010007@2019101411058602000007700084912][79,-][80,KRW][81,777][82,USD][83,68][84,KRW][85,777][86,-][87,-][100,false][101,false][102,-][103,false][104,-][105,-][106,-][107,-][108,WEB][109,-][110,https://global.alipay.com/281002040017Rc4B1k6dQ9fwjd3pdiscount][111,-][112,false][113,-][114,-][115,-][116,API][117,-][118,-][119,-][120,-][200,-][201,-][202,-][203,-][220,-][221,-][222,-][223,-][224,-][300,true][301,NO_USER_ID][320,-][321,-][323,-][324,MN400401000000000122][325,-][328,-][331,-][332,-][333,-][334,-][335,-][336,-][337,-][338,-][339,-][340,-][1000,-][1003,-][1006,-][1007,-][1008,-][1009,-][1010,-][1011,false][1012,false][1013,-][1014,-][1015,-][1016,true][1017,-][1018,1022172000000000001][1019,-][1020,true][1021,-][1022,-][1023,-]

关键代码梳理与说明:

digest日志核心模型:

/**
 * digest日志记录、打印核心模型
 *
 */
public class RouterDigestLog {

    private static final Logger                       LOGGER       = LoggerFactory
                                                                       .getLogger("IGPAYROUTER-DIGEST");

    private final static ThreadLocal<RouterDigestLog> TL           = ThreadLocal
                                                                       .withInitial(RouterDigestLog::new);
		//digest日志上下文。为避免扩容,initialCapacity = LogFieldEnum.values().length/0.75+1
    private Map<LogFieldEnum, Object>                 digestFields = new HashMap<>();

    /**
     * 在日志上下文中添加值
     *
     * @param fieldEnum
     * @param value
     */
    public static void add(LogFieldEnum fieldEnum, Object value) {
        if (Objects.nonNull(fieldEnum)) {
            TL.get().digestFields.put(fieldEnum, value);
        }
    }

    /**
     * 打印日志并清理线程上下文
     */
    public static void printAndRemove() {
        try {
            // 记录一些与业务完全无关的digest内容
						// 记录一些与业务完全无关的digest内容--end
            LogUtil.info(LOGGER, TL.get().toString());
        } finally {
            remove();
        }
    }

    /**
     * 获取当前上下文中的值,不存在时返回空字符串
     *
     * @param fieldEnum
     * @return null:未填值
     */
    public static Object getFieldValue(LogFieldEnum fieldEnum) {
        return TL.get().digestFields.get(fieldEnum);
    }

    /**
     * 获取当前上下文中的值,并将转换为Sting类型,值为空时返回"-"
     *
     * @param fieldEnum
     * @return
     */
    private static String getFieldStringValue(LogFieldEnum fieldEnum) {
        return Objects.toString(getFieldValue(fieldEnum), "-");
    }

    /**
     * 清理线程上下文
     */
    public static void remove() {
        TL.remove();
    }

    /**
     * 重写 toString
     * 
     * @return 只返回在digestFields中,填充过LogFieldEnum的内容,未填充过的不记录
     */
    @Override
    public String toString() {
				//对digest上下文的key进行进行排序后,再转string
        return digestFields.entrySet().stream()
            .sorted(Comparator.comparingInt(e -> e.getKey().getIndex()))
            .map(e -> "[" + e.getKey().getIndex() + ","
                      + getFieldStringValue(e.getKey()).replaceAll("[\\[\\],\n]", " ") + "]")
            .collect(Collectors.joining());
    }

    /**
     * 克隆摘要日志上下文实例
     *
     * @return
     */
    public RouterDigestLog cloneInstance() {

        RouterDigestLog routerDigestLog = new RouterDigestLog();

        Map<LogFieldEnum, Object> digestFields = new HashMap<>(this.digestFields);

        routerDigestLog.setDigestFields(digestFields);

        return routerDigestLog;
    }

    /**
     * 获取摘要日志上下文
     *
     * @return
     */
    public static RouterDigestLog get() {
        return TL.get();
    }

    /**
     * 设置摘要日志上下文。
     *
     * @param context 上下文内容
     */
    public static void set(RouterDigestLog context) {
        TL.set(context);
    }

    /**
     * Setter method for property <tt>digestFields</tt>.
     *
     * @param digestFields value to be assigned to property digestFields
     */
    public void setDigestFields(Map<LogFieldEnum, Object> digestFields) {
        this.digestFields = digestFields;
    }
}

复杂对象 digest 内容解析:

/**
 * 复杂对象与解析器的关系枚举
 */
public enum ClassForDigestEnum {

    /**
     * 从commonOrder对象中添加摘要日志
     *
     * @see CommonOrder
     */
    COMMON_ORDER(CommonOrder.class, AddDigestFromComplexTypeConsumer::addFromCommonOrder),

    /**
     * 异常对象
     *
     * @see RouterCommonException
     */
    EXCEPTION(RouterCommonException.class, AddDigestFromComplexTypeConsumer::addFromException);

    /**
     * 复杂对象类型
     */
    private Class    classType;

    /**
     * 按照复杂类型添加摘要日志处理器
     */
    private Consumer addDigestConsumer;

    <T> ClassForDigestEnum(Class<T> clazz, Consumer<T> addDigestConsumer) {
        this.classType = clazz;
        this.addDigestConsumer = addDigestConsumer;
    }

    /**
     * 用复杂对象批量添加摘要日志,兼容null对象
     *
     * @param object
     */
    static void addFieldsFromObject(Object object) {
        for (ClassForDigestEnum value : values()) {
            if (value.classType.isInstance(object)) {
                value.addDigestConsumer.accept(object);
                return;
            }
        }
    }
}
/**
 * 从复杂类型中添加digest日志方法
 */
class AddDigestFromComplexTypeConsumer {

    /**
     * 从commonOrder中添加digest日志
     *
     */
    static void addFromCommonOrder(CommonOrder commonOrder) {
				//指定向ACQUIRE_ID的位置,添加内容
        addIfValueEmpty(LogFieldEnum.ACQUIRE_ID, commonOrder.getSourceSiteId());
        forceAdd(LogFieldEnum.REPEAT_REQUEST, commonOrder.isRepeatRequest());

    }
}

异步任务中 digest 的复制

//统一线程池执行器管理对象
protected ThreadPoolExecutorManager threadPoolExecutorManager;

……

//异步任务提交入口。初始化AbstractThreadRunnable时,拷贝当前线程的digest日志上下文到创建的新线程中
threadPoolExecutorManager.getExecutor().submit(new AbstractThreadRunnable() {

    @Override
    public void doRealRun() {
        DigestLogUtil.forceAdd(LogFieldEnum.REQUEST_SOURCE,
            RequestSourceEnum.ASYN_THREAD);

        forwardAndApprovePayment(payOrder, srPayRequest, response, function);

    }

});

……
/**
 * 异步线程执行器
 *
 */
public abstract class AbstractThreadRunnable extends EchoxRunnable {

    /**
     * BIZ-LOGGER
     */
    private static final Logger BIZ_ERROR    = LoggerFactory
        .getLogger(LoggerConstant.ROUTER_BIZ_ERROR);

    /**
     * ERROR-LOGGER
     */
    private static final Logger SYSTEM_ERROR = LoggerFactory.getLogger(LoggerConstant.SYSTEM_ERROR);

    /**
     * 新建线程时,临时存储日志上下文
     */
    private RouterDigestLog     routerDigestLog;

    /**
     * 构造方法
     */
    public AbstractThreadRunnable() {
        initRouterRunnable();
    }

    /**
     * 初始化方法
     */
    public final void initRouterRunnable() {
        super.init();
      
        // 初始化子线程上下文信息
        routerDigestLog = RouterDigestLog.get().cloneInstance();
    }

    @Override
    public void doRun() {
        try {
            beforeRun();
            doRealRun();

        } catch (RouterCommonException e) {
            DigestLogUtil.addFieldsFromObject(e);
            LogUtil.warn(BIZ_ERROR, e, "ASYNC_THREAD_EXCEPTION", e.getResultCode().getErrorName(),
                e.getResultCode().getResultMsg());
        } catch (Throwable t) {
            LogUtil.error(SYSTEM_ERROR, t, "ASYNC_THREAD_EXCEPTION.");
        } finally {
            afterRun();
        }
    }

    /**
     * 执行真正的操作。交给子类实现
     */
    public abstract void doRealRun();

    /**
     * 前置方法,添加上下文,添加日志等
     */
    private void beforeRun() {
        // 添加摘要日志上下文
        RouterDigestLog.set(routerDigestLog);

        // 修改摘要日志来源
        DigestLogUtil.forceAdd(LogFieldEnum.REQUEST_SOURCE, RequestSourceEnum.ASYN_THREAD);
    }

    /**
     * 后置方法,清理上下文,打印日志等
     */
    private void afterRun() {
        // 打印新摘要日志
        DigestLogUtil.printAndRemove();
    }
}

对方案的思考:

  1. 复杂对象字段的解析行为,是通过遍历 enum 中维护的映射来完成的。该行为属于复杂对象模型的行为,却不在模型中,不够内聚,不符合项目中DDD的规范。可以考虑解析的动作定义在基类中,由具体的子类来实现解析的动作。
  2. 日志的落地,使用文件的方式进行记录,没有从代码层面上预留扩展能力,未来需要将 digest 日志同时落地到DB或投递到Msg时,需要修改已有的代码,不符合Open Closed Principle。

监控:

  • 监控规则:按 左起第0个 “[indexValue,” ,右至第0个 “]” 的规则,来实现对每个业务接口中的字段进行监控
  • 由于digest日志是系统接口层面的,所以当需要复原一个完整业务链路时,需要借助贯穿业务链路的Unique Id,将多个digest日志串联起来。常用的Unique Id有UserId、OrderId等。

数据分析:

实现方案 v2.0

针对思考中提到的两个问题,我们做如下两个方面的升级:

  1. 对于项目内自建的复杂对象,其digest日志解析方法,内聚到各自的请求 DTO / Request / Domain 中,通过重写基类中定义的 extractFields2DigestContext 方法来实现digest日志字段定制化添加。
  2. 对于集成的第三方对象,如果需要解析digest日志,有如下两种方案的对比选型:
    • 在业务流程中,按需提取需要的部分字段
      • 优点:日志字段解析在业务流程中,更好的贴合业务,适用场景更广
      • 缺点:日志解析逻辑分散在各个业务流程中,不方便查看digest字段的记录情况
    • 将对象的digest日志解析,参考方案一中的方式,将对象和对应的日志解析方法用AddDigestFromComplexTypeConsumer维护起来
      • 优点:集成的第三方对象中,digest日志被统一管理起来,方便查看对象有哪些解析的字段
      • 缺点:如果对象中的日志字段,需要按业务场景进行记录时,需要在业务中使用forceAdd
  3. 日志的落地,使用责任链设计模式,按需选择一种或多种落地的方式
    • 对于方案的选用,需结合实际业务发展方向,不要做过度的设计,但也不要不预留扩展能力。

实现方案详情:

时序图:

请添加图片描述

类图:

请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/474968.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软件维护(Software maintenance)的流程

软件维护(Software maintenance)是一个软件工程名词&#xff0c;是指在软件产品发布后&#xff0c;因修正错误、提升性能或其他属性而进行的软件修改。 软件维护主要根据需求变化或硬件环境的变化对应用程序进行部分或全部的修改&#xff0c;修改时应充分利用源程序。修改后要填…

2023年的深度学习入门指南(10) - 前端同学如何进行chatgpt开发

2023年的深度学习入门指南(10) - 前端同学如何进行chatgpt开发 在第二篇&#xff0c;我们使用openai的python库封装&#xff0c;搞得它有点像之前学习的PyTorch一样的库。这一节我们专门给它正下名&#xff0c;前端就是字面意义上的前端。 给gpt4写前端 下面我们写一个最土的…

【BeautifulSoup】——05全栈开发——如桃花来

目录索引 介绍&#xff1a;解析库&#xff1a; 安装&#xff1a;pip install BeautifulSoup4pip install lxml 标签选择器&#xff1a;1.string属性&#xff1a;.name属性&#xff1a;获取标签中的属性值&#xff1a; 实用——标准选择器&#xff1a;使用find_all()根据标签名查…

百城巡展 | 人大金仓4月“双向奔赴”告一段落

人间最美四月天&#xff0c;人大金仓走过上海、宁波、合肥&#xff0c;联合伙伴发布医疗、金融、信息安全、电子档案等多个关键领域的信创联合解决方案&#xff0c;共同为数字基础设施的安全和可持续发展贡献力量&#xff0c;吸引了线上线下近7000人参与。 左右滑动&#xff0c…

大数据架构(一)背景和概念

-系列目录- 大数据架构(一)背景和概念 大数据架构(二)大数据发展史 一、背景 1.岗位现状 大数据在一线互联网已经爆发了好多年&#xff0c;2015年-2020年(国内互联网爆发期)那时候的大数据开发&#xff0c;刚毕业能写Hive SQL配置个离线任务、整个帆软报表都20K起步。如果做到架…

Midjourney 创建私人画图机器人,共享账号如何设置独立绘画服务器(保姆级教程)

你是不是遇到以下问题&#xff1a; 1.Midjourney会员怎么自建绘图服务器&#xff0c;不受其他人的打扰&#xff1f; 2.Midjourney会员共享账号如何自建服务器&#xff0c;供其他人使用&#xff1f; 3.在官方服务器作图&#xff0c;频道里面的人太多了&#xff0c;自己的指令…

【五一创作】( 字符串) 409. 最长回文串 ——【Leetcode每日一题】

❓ 409. 最长回文串 难度&#xff1a;简单 给定一个包含大写字母和小写字母的字符串 s &#xff0c;返回 通过这些字母构造成的 最长的回文串 。 在构造过程中&#xff0c;请注意 区分大小写 。比如 "Aa" 不能当做一个回文字符串。 示例 1: 输入:s “abccccdd”…

时序预测 | Matlab实现SSA-GRU、GRU麻雀算法优化门控循环单元时间序列预测(含优化前后对比)

时序预测 | Matlab实现SSA-GRU、GRU麻雀算法优化门控循环单元时间序列预测(含优化前后对比) 目录 时序预测 | Matlab实现SSA-GRU、GRU麻雀算法优化门控循环单元时间序列预测(含优化前后对比)预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Matlab实现SSA-GRU、GRU麻雀算法…

第十四章 移动和旋转(下)

本章节我们介绍另外两种形式的旋转&#xff0c;也对应了两个方法。首先是RotateAround方法&#xff0c;他是围绕穿过世界坐标中的 point 点的 axis轴旋转 angle 度。这个方法虽然比较晦涩难懂&#xff0c;但是我们使用一个案例&#xff0c;大家就非常明白了。我们创建一个新的“…

JDBC详解(三):使用PreparedStatement实现CRUD操作(超详解)

JDBC详解&#xff08;三&#xff09;&#xff1a;使用PreparedStatement实现CRUD操作&#xff08;超详解&#xff09; 前言一、操作和访问数据库二、使用Statement操作数据表的弊端三、PreparedStatement的使用1、PreparedStatement介绍2、PreparedStatement vs Statement3、Ja…

连接分析工具箱 | 利用CATO进行结构和功能连接重建

导读 本研究描述了一个连接分析工具箱(CATO)&#xff0c;用于基于扩散加权成像(DWI)和静息态功能磁共振成像(rs-fMRI)数据来重建大脑结构和功能连接。CATO是一个多模态软件包&#xff0c;使研究人员能够运行从MRI数据到结构和功能连接组图的端到端重建&#xff0c;定制其分析并…

牛郎织女

我写的十二星座十二人大多是奇女子&#xff0c;如双子的刘若英《若》、天秤的叶倩文《AB天秤座&#xff0c;Sally》、射手的桂纶镁《半人马座&#xff0c;桂纶镁》、水瓶的杨千嬅《可惜我是水瓶座》、双鱼的安妮伊能静《十二星座十二人之&#xff1a;双鱼&#xff0c;伊能&…

使用cube studio开发机器学习建模的pipeline

&#xff08;作者&#xff1a;陈玓玏&#xff09; Cube Studio目前包含了传统机器学习模板&#xff0c;400AI模型&#xff0c;欢迎私信了解哇&#xff01; 在使用cube studio进行模型训练或推理的过程中&#xff0c;我们有时会发现没有符合自己要求的模板&#xff0c;此时我们…

Unity 后处理(Post-Processing) -- (1)概览

在Unity中&#xff0c;后处理&#xff08;Post-Processing&#xff09;是在相机所捕捉的图像上应用一些特殊效果的过程&#xff0c;后处理会让图像视觉效果更好&#xff08;前提是做的好&#xff09;。 这些效果的范围有非常细微的颜色调整&#xff0c;也包括整体的美术风格的大…

graalvm spring 打包成exe

graalvm jdk下载https://www.graalvm.org/downloads/ 把graalvm加入环境变量和就是JAVA_HOME 安装native-image gu.cmd install native-image 问题: Error: Default native-compiler executable cl.exe not found via environment variable PATH Error: To prevent native-tool…

Redis基础——Redis常用命令

Redis基础 1.1 Redis通用命令 通用指令是部分数据类型的&#xff0c;都可以使用的指令&#xff0c;常见的有&#xff1a; KEYS&#xff1a;查看符合模板的所有keyDEL&#xff1a;删除一个指定的keyEXISTS&#xff1a;判断key是否存在EXPIRE&#xff1a;给一个key设置有效期&…

C++动态规划模板汇总大全

前言 如果你不太了解dp&#xff08;动态规划&#xff09;是个什么东西&#xff0c;请回到上次dp。 链接&#xff1a;动态规划算法详解 数字三角形模型 问题 A: 【一本通基础DP基础模型】【例9.2】数字金字塔 【题目描述】 观察下面的数字金字塔。写一个程序查找从最高点到…

【计算机图形学】三维图形投影和消隐(三视图构造)

模块4-1 三维图形投影和消隐 一 实验目的 编写三维图形各种变换的投影算法 二 实验内容 1&#xff1a;自行选择三维物体&#xff08;不能选长方体&#xff09;&#xff0c;建立坐标系&#xff0c;给定点的三维坐标值&#xff0c;建立边表结构。完成三视图。 实验结果如下图所…

如何解决服务器认证失败

服务器认证失败是指在连接服务器时&#xff0c;由于身份认证失败而无法访问服务器。其实这是一种非常常见的问题&#xff0c;这种问题的原因很多&#xff0c;多方面导致的&#xff0c;但是我们又该如何解决这种问题呢&#xff1f;接下来就让小编为大家介绍服务器认证失败的原因…

41.Java单列集合LinkedList

单列集合LinkedList 1.LinkedList集合2.源码3. ArrayList和LinkedList的区别 1.LinkedList集合 在许多情况下&#xff0c;ArrayList效率更高&#xff0c;因为通常需要访问列表中的某一个元素&#xff0c;但是LinkedList提供了几种方法来更有效地执行某些操作。 2.源码 3. Arr…