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

article/2025/11/8 15:49:38

需求

​   为了满足从业务整体的维度 实现监控和链路复原,我们希望对于一个业务接口,记录一行请求日志,并通过某个 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+1private 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内容--endLogUtil.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的内容,未填充过的不记录*/@Overridepublic String toString() {//对digest上下文的key进行进行排序后,再转stringreturn 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() {@Overridepublic 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();}@Overridepublic 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://chatgpt.dhexx.cn/article/PwTBQ5R1.shtml

相关文章

java发起Digest Auth请求

常规认证方式 上代码&#xff1a; 需要的Maven <dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5</version></dependency><dependency><groupId>org.…

认证学习3 - Digest摘要认证讲解、代码实现、演示

文章目录 Digest摘要认证 - 密文讲解&#xff08;Digest摘要认证&#xff09;实现&#xff08;Digest认证&#xff09;代码&#xff08;Digest认证&#xff09;代码&#xff08;Digest认证-客户端&#xff09;演示&#xff08;Digest认证-postman&#xff09; 认证大全&#xf…

HTTP的认证方式之DIGEST 认证(摘要认证)

核心步骤&#xff1a; 步骤 1&#xff1a; 请求需认证的资源时&#xff0c;服务器会随着状态码 401Authorization Required&#xff0c;返回带WWW-Authenticate 首部字段的响应。该字段内包含质问响应方式认证所需的临时质询码&#xff08;随机数&#xff0c;nonce&#xff09;…

Digest Auth 摘要认证

Digest Auth 摘要认证 1.非常规方式 转载&#xff1a;https://blog.csdn.net/qq_25391785/article/details/86595529 public static void postMethod(String url, String query) {try {CredentialsProvider credsProvider new BasicCredentialsProvider();credsProvider.setC…

digest鉴权

“摘要”式认证&#xff08; Digest authentication&#xff09;是一个简单的认证机制&#xff0c;最初是为HTTP协议开发的&#xff0c;因而也常叫做HTTP摘要&#xff0c;在RFC2671中描述。其身份验证机制很简单&#xff0c;它采用杂凑式&#xff08;hash&#xff09;加密方法&…

消息摘要(Digest),数字签名(Signature),数字证书(Certificate)是什么?

1. 消息摘要&#xff08;Digest&#xff09; 1. 什么是消息摘要&#xff1f; 对一份数据&#xff0c;进行一个单向的 Hash 函数&#xff0c;生成一个固定长度的 Hash 值&#xff0c;这个值就是这份数据的摘要&#xff0c;也称为指纹。 2. 摘要算法 常见的摘要算法有 MD5、SHA…

HTTP通讯安全中的Digest摘要认证释义与实现

摘要 出于安全考虑&#xff0c;HTTP规范定义了几种认证方式以对访问者身份进行鉴权&#xff0c;最常见的认证方式之一是Digest认证 Digest认证简介 HTTP通讯采用人类可阅读的文本格式进行数据通讯&#xff0c;其内容非常容易被解读。出于安全考虑&#xff0c;HTTP规范定义了几…

http协议之digest(摘要)认证,详细讲解并附Java SpringBoot源码

目录 1.digest认证是什么&#xff1f; 2.digest认证过程 3.digest认证参数详解 4.基于SpringBoot实现digest认证 5.digest认证演示 6.digest认证完整项目 7.参考博客 1.digest认证是什么&#xff1f; HTTP通讯采用人类可阅读的文本格式进行数据通讯&#xff0c;其内容非…

【WinRAR】WinRAR 6.01 官方最新简体中文版

WinRAR 6.01 官方简体中文商业版下载地址&#xff08;需要注册&#xff09;&#xff1a; 64位&#xff1a; https://www.win-rar.com/fileadmin/winrar-versions/sc/sc20210414/wrr/winrar-x64-601sc.exe https://www.win-rar.com/fileadmin/winrar-versions/sc/sc20210414/…

WinRAR命令行

基本使用 实践 将文件夹压缩到zip包 输入&#xff1a;文件夹如下&#xff0c;文件夹为class。 输出&#xff1a;classes.zip 指令如下&#xff1a; rar a classes.zip .\classes或者 WinRAR a classes.zip .\classes结果如下&#xff1a; PS C:\Users\liyd\Desktop\kuai…

WinRAR安装教程

文章目录 WinRAR安装教程无广告1. 下载2. 安装3. 注册4. 去广告 WinRAR安装教程无广告 1. 下载 国内官网&#xff1a;https://www.winrar.com.cn/ 2. 安装 双击&#xff0c;使用默认路径&#xff1a; 点击“安装”。 点击“确定”。 点击“完成”。 3. 注册 链接&#x…

WinRAR注册+去广告教程

1、注册 在WinRAR安装目录创建rarreg.key文件&#xff0c; 拷贝如下内容并保存&#xff1a; RAR registration data Federal Agency for Education 1000000 PC usage license UIDb621cca9a84bc5deffbf 6412612250ffbf533df6db2dfe8ccc3aae5362c06d54762105357d 5e3b1489e751c…

WinRAR4.20注册文件rarreg.key

2019独角兽企业重金招聘Python工程师标准>>> 在WinRAR的安装目录下&#xff0c;新建rarreg.key文件&#xff08;注意不要创建成rarreg.key.txt文件了^_^&#xff09;&#xff0c;内容为如下&#xff1a; RAR registration data Team EAT Single PC usage license UI…

Android按钮样式

//创建一个新的XML文件&#xff0c;可命名为styles<style name"button1"><item name"android:layout_height">wrap_content</item><item name"android:textColor">#FFFFFF</item><item name"android:text…

漂亮的Button按钮样式

开发中各种样式的Button,其实这些样式所有的View都可以共用的,可能对于你改变的只有颜色 所有的都是用代码实现 边框样式,给你的View加上边框 <Buttonandroid:layout_width="0dip"android:layout_height="match_parent"android:layout_margin=&q…

「HTML+CSS」--自定义按钮样式【001】

前言 Hello&#xff01;小伙伴&#xff01; 首先非常感谢您阅读海轰的文章&#xff0c;倘若文中有错误的地方&#xff0c;欢迎您指出&#xff5e; 哈哈 自我介绍一下 昵称&#xff1a;海轰 标签&#xff1a;程序猿一只&#xff5c;C选手&#xff5c;学生 简介&#xff1a;因C语…

HTML_炫酷的按钮样式

html部分 <a href"#"><span></span><span></span><span></span><span></span>Neon button</a><a href"#"><span></span><span></span><span></span…

html改变按钮样式

今天有人问我怎么改样式&#xff0c;需求是三个按钮&#xff0c;一次点一个&#xff0c;要求被点击的按钮和没被点的按钮是两种不同的样式&#xff0c;如图所示。 最初三个按钮都没选如图一&#xff0c;然后点击“已读”按钮&#xff0c;“已读”按钮样式改变。再点击“全部”按…

button按钮的一些样式效果

先制作一个button按钮 &#xff0c;将它原本的样式取消掉再把button按钮的颜色设置成transparent &#xff0c;再设置button按钮的边框。首先将button按钮的初始样式取消掉 &#xff0c;在设置button按钮的width和 height &#xff0c;font-size &#xff0c;还有border 现在写…

vue点击按钮改变按钮样式

一. 效果 点击按钮前&#xff1a; 点击按钮后&#xff1a; 再次点击按钮变回原来的样式&#xff1a; 二. 具体代码 <template><div id"box"><button click"btn" id"but" v-bind:class"{ but01: style1, but02: style2 }&qu…