-
【原】MDC日志链路设计
背景
我们项目中现有日志系统,采用的是slf4j+logback这套日志组件,也是Java生态里面比较常用的一个日志组件,但是随着分布式的演进,这套组件明显存在以下几个问题:
解决方案
正文
本篇博客主题是MDC(MDC 全称是 Mapped Diagnostic Context,可以粗略的理解成是一个线程安全的存放诊断日志的容器),其具体流程是通过某些标识将整个轨迹串起来,例如A-B-C-远程接口-D这条链路相关日志信息在日志文件里可以通过某个标识快速查找。下面介绍下目前我负责的项目中日志方案
logback.xml
将traceId配置在logback.xml,有点像占位符的方式
MDC
将对应的traceId变量通过MDC写入
源码分析
1.MDC是什么?
下图可知MDC是slf4j-api的一个类,里面提供了put,get,remove等方法,看完源码其实可知就是一个ThreadLocal,每put一个元素就放到里面,当调用logger.info的时候将ThreadLocal变量取出赋到输出日志
由上可知
1 MDCAdapter 是一个适配接口,存放于spi包下面,由此便知MDCAdapter是为了适配其它日志组件2 MDC 提供的 put 方法,可以将一个 K-V 的键值对放到容器中,并且能保证同一个线程内,Key 是唯一的,不同的线程 MDC 的值互不影响
3 在 logback.xml 中,在 layout 中可以通过声明 %X{REQ_ID} 来输出 MDC 中 REQ_ID 的信息
4 MDC 提供的 remove 方法,可以清除 MDC 中指定 key 对应的键值对信息
LogbackMDCAdapters源码
如上是MDC的使用方法以及源码分析,下面介绍的是本地调用外部系统的时候,假设用 的是restTemplate,那么得考虑如何把调用前后的日志情况进行抽取封装,做到统一打印,因为笔者之前的代码是没有做抽取,导致每个不同的调用方法都要手动去写log.info,这样的做法虽然没有大问题,但是明显是比较多余且可以进行抽取
外部接口日志轨迹输出
调用过程中涉及到外部接口,由于外部接口是在第三方系统,我们无法将traceId传递下去,需要改造我们这边的远程调用代码,由于笔者项目用的是restTemplate,所以需要对restTemplate添加拦截器,用于发送请求前和请求后打印出相关日志,如下是我这边的restTemplate对应的日志拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
class MyRequestInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte [] bytes, ClientHttpRequestExecution execution) throws IOException { traceRequest(request, bytes); ClientHttpResponse response = execution.execute(request, bytes); ClientHttpResponse responseCopy = new BufferingClientHttpResponseWrapper(response); traceResponse(responseCopy); return responseCopy; } /** * 打印请求数据 * * @param request 请求 * @param bytes 请求体 */ private void traceRequest(HttpRequest request, byte [] bytes) { String body = new String(bytes, StandardCharsets.UTF_8); log.info( "Request Body = {}" , body); } /** * 打印响应结果 * * @param response 响应结果 * @throws IOException io */ private void traceResponse(ClientHttpResponse response) throws IOException { StringBuilder inputStringBuilder = new StringBuilder(); try (BufferedReader bufferedReader = new BufferedReader( new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) { String line = bufferedReader.readLine(); while (line != null ) { inputStringBuilder.append(line); // inputStringBuilder.append('\n'); line = bufferedReader.readLine(); } } log.info( "Response Body: {}" , inputStringBuilder.toString()); } final class BufferingClientHttpResponseWrapper implements ClientHttpResponse { private final ClientHttpResponse response; private byte [] body; BufferingClientHttpResponseWrapper(ClientHttpResponse response) { this .response = response; } @Override public HttpStatus getStatusCode() throws IOException { return this .response.getStatusCode(); } @Override public int getRawStatusCode() throws IOException { return this .response.getRawStatusCode(); } @Override public String getStatusText() throws IOException { return this .response.getStatusText(); } @Override public HttpHeaders getHeaders() { return this .response.getHeaders(); } @Override public InputStream getBody() throws IOException { if ( this .body == null ) { this .body = StreamUtils.copyToByteArray( this .response.getBody()); } return new ByteArrayInputStream( this .body); } @Override public void close() { this .response.close(); } } } |
最后
以上就是关于MDC常见的使用场景,包括携程里面的日志组件其实内部也是通过MDC实现,只不过是根据业务做了调整,一般分布式环境下最好将日志输出到Redis或者ES,然后提供一个界面查询日志,目前也有很多类似的开源框架集成了分布式链路日志打印+看板
原文:
https://www.cnblogs.com/zdd-java/p/15630210.html