Skip to content

Commit a1a2844

Browse files
author
yangjingjing
committed
init
1 parent 8cc57a6 commit a1a2844

5 files changed

Lines changed: 817 additions & 3 deletions
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
layout: post
3+
categories: [Java]
4+
description: none
5+
keywords: Java
6+
---
7+
# Java日志格式
8+
日志是系统中熵增最快的一个模块,它承载了业务野蛮生长过程中的所有副产品。本文介绍了一个日志治理案例,围绕降本和提效两大主题,取得一定成效。
9+
10+
## 日志乱象
11+
日志是日常开发中最有可能被忽视,最容易被滥用的一个模块。被忽视是因为打日志实在是一个再简单不过的事,前人设计好了一个logback.xml。
12+
13+
后面只需要依样画葫芦定义一个logger,随手一个info调用就搞定,他甚至不确定这条日志能不能打出来,也不知道会打在哪个文件,反正先跑一次试试,不行就换error。被滥用是因为不同场景日志的格式内容千差万别,或者说日志打法太灵活,太随意了,风格太多样化了,以至于几乎每个人一言不合就要自己写一个LogUtil,我见过最夸张的,一个系统中用于打日志的工具类,有二三十个之多,后人纠结该用哪个工具可能就要做半个小时的思想斗争,完美诠释了什么叫破窗效应。
14+
15+
### 分类之乱
16+
一般来说,一个系统必然需要设计多个日志文件以区分不同业务或场景,不可能所有的日志都打到一个文件里。但是怎么进行分类,没人告诉我们,于是就有了各种各样的分类。
17+
18+
按系统模块分。这种分类应该是最基础的一种分类,也是最有层次感的分类。比如履约服务中枢的系统分层。基本上每一层对应一个日志文件。
19+
20+
按租户身份分。一般中台系统都会支持多个租户(行业),每一个租户单独对应一个日志文件。这种分类一般不会单独使用,除非你要做完全意义上的租户隔离。
21+
22+
意识流分类法。不符合MECE法则,没有清晰统一的分类逻辑,按业务分,按系统模块分,按接口能力分,按新老链路分,各种分法的影子都能看到,结果就是分出来几十个文件,打日志的人根本就不知道这一行的日志会打进哪个文件。
23+
24+
以上说的各种分类方式,都不是绝对纯粹的,因为无论哪一种,无论一开始设计的多么边界清晰,随着时间的推进,最后都会演变为一个大杂烩。
25+
26+
### 格式之乱
27+
对于日志需要有一定的格式这点相信没有人会有异议,格式的乱象主要体现在两个方面,一个是格式的设计上,有些系统设计了非常复杂的格式,用多种分隔符组合,支持日志内容的分组,用关键词定位的方式代替固定位置的格式,同时支持格式扩展,这对人脑和计算机去解析都是一种负担。第二个是同一个日志文件,还能出现不同格式的内容,堆栈和正常业务日志混杂。
28+
29+
### 工具之乱
30+
有时候甚至会出现,同一个类,同一个方法中,两行不同的日志埋点,打出来的日志格式不一样,落的日志文件也不一样。为什么会出现这种情况?就是因为用了不同的日志工具。要究其根源,我们需要分析一下不同的工具究竟是在做什么。可以发现,很多工具之间的差别就是支持的参数类型不一样,有些是打印订单对象的,有些是打印消息的,有些是打印调度日志的。
31+
32+
## 日志分层
33+
我一直信奉极简的设计原则,简单意味着牢不可破。上面提到,一套日志系统最终的结局一定是走向混乱,既然这种趋势无法避免,那么我们在最初设计的时候就只能确保一件事,保证原始的分类尽量简单,且不重叠。其实通用的分类方式无非就两种,一种按职能水平拆分,一种按业务垂直拆分。一般来说,一级分类,应该采用水平拆分。因为业务的边界一般是很难划清的,边界相对模糊,职能的边界就相对清晰稳定很多,职能其实反映的是工作流,工作流一经形成,基本不会产生太大的结构性变化。
34+
35+
从层次上来看,其实只有三层,入口,内核,出口。入口日志只负责打印流量入口的出入参,比如HSF,controller。出口日志负责打印所有第三方服务调用的出入参。内核日志,负责打印所有中间执行过程中的业务日志。就三层足矣,足够简单,不重不漏。
36+
37+
## 格式设计
38+
日志的格式设计也有一些讲究。首先日志的设计是面向人可读的,这个无需多言。另外也非常重要的一个点,要面向可监控的设计,这是容易被很多人忽视的一个点。基于这两个原则,说一下我在格式设计上的一些思路。
39+
40+
首先要做维度抽象。既然是面向监控,监控一般需要支持多个维护,比如行业维度,服务维度,商家维度等等,那么我们就需要把所有的维度因子抽出来。那么这些维度实际打印的时候怎么传给logger呢?建议是把他们存到ThreadLocal中,打的时候从上下文中取。这样做还有一个好处是,日志打印工具设计的时候就会很优雅,只需要传很少的参数。
41+
42+
格式尽量简单,采用约定大于配置的原则,每一个维度占据一个固定的位置,用逗号分割。切忌设计一个大而全的模型,然后直接整个的序列化为一个JSON字符串。
43+
44+
也不要被所谓的扩展性给诱惑,给使用方轻易开出一个能够自定义格式的口子,即便你能轻而易举的提供这种能力。根据我的经验,这种扩展性一定会被滥用,到最后连设计者也不知道实际的格式究竟是怎样的。当然这个需要设计者有较高的视野和远见,不过这不是难点,难的还是克制自己炫技的欲望。
45+
46+
在内容上,尽量打印可以自解释的文本,做到见名知义。举个例子,我们要打印退款标,退款标原本是用1, 2, 4, 8这种二进制位存储的,打印的时候不要直接打印存储值,翻译成一个能描述它含义的英文code。
47+
48+
格式示例
49+
```
50+
timeStamp|threadName logLevel loggerName|sourceAppName,flowId,traceId,sceneCode,identityCode,loginUserId,scpCode,rpcId,isYace,ip||businessCode,isSuccess||parameters||returnResult||
51+
```
52+
53+
## 堆栈倒打
54+
本文的重点来啦,这个设计就是开头提到的奇思妙想。堆栈倒打源于我在排查另一个系统问题过程中感受到的几个痛点,首先来看一个堆栈示例。
55+
56+
解这个问题,本质上就是这样一个简单的算法题:倒序打印堆栈的最后N个元素。核心代码如下。
57+
```
58+
/**
59+
* 递归逆向打印堆栈及cause(即从最底层的异常开始往上打)
60+
* @param t 原始异常
61+
* @param causeDepth 需要递归打印的cause的最大深度
62+
* @param counter 当前打印的cause的深度计数器(这里必须用引用类型,如果用基本数据类型,你对计数器的修改只能对当前栈帧可见,但是这个计数器,又必须在所有栈帧中可见,所以只能用引用类型)
63+
* @param stackDepth 每一个异常栈的打印深度
64+
* @param sb 字符串构造器
65+
*/
66+
public static void recursiveReversePrintStackCause(Throwable t, int causeDepth, ForwardCounter counter, int stackDepth, StringBuilder sb){
67+
if(t == null){
68+
return;
69+
}
70+
if (t.getCause() != null){
71+
recursiveReversePrintStackCause(t.getCause(), causeDepth, counter, stackDepth, sb);
72+
}
73+
if(counter.i++ < causeDepth){
74+
doPrintStack(t, stackDepth, sb);
75+
}
76+
}
77+
```
78+
要降低存储成本,同时也要确保信息不失真,我们考虑对堆栈行下手,把全限定类名简化为类名全打,包路径只打第一个字母,行号保留。如:c.a.u.m.s.LogAspect#log:88。核心代码如下。
79+
```
80+
public static void doPrintStack(Throwable t, int stackDepth, StringBuilder sb){
81+
StackTraceElement[] stackTraceElements = t.getStackTrace();
82+
if(sb.lastIndexOf("\t") > -1){
83+
sb.deleteCharAt(sb.length()-1);
84+
sb.append("Caused: ");
85+
}
86+
sb.append(t.getClass().getName()).append(": ").append(t.getMessage()).append("\n\t");
87+
for(int i=0; i < stackDepth; ++i){
88+
if(i >= stackTraceElements.length){
89+
break;
90+
}
91+
StackTraceElement element = stackTraceElements[i];
92+
sb.append(reduceClassName(element.getClassName()))
93+
.append("#")
94+
.append(element.getMethodName())
95+
.append(":")
96+
.append(element.getLineNumber())
97+
.append("\n\t");
98+
}
99+
}
100+
```
101+
102+
103+
104+
105+
106+
107+
108+
109+
110+
111+
112+
113+
114+
115+
116+
117+
118+
119+
120+
121+
122+
123+
124+
125+

0 commit comments

Comments
 (0)