log4j MDC實現日志追蹤
MDC 中包含的可以被同一線程中執行的代碼所訪問內容。當前線程的子線程會繼承其父線程中的 MDC 的內容。記錄日志時,只需要從 MDC 中獲取所需的信息即可。
作用:
使用MDC來記錄日志,可以規范多開發下日志格式。
1、新建線程處理類 ThreadContext
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
88
89
90
91
92
93
94
95
96
97
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
|
import java.io.Serializable; import java.util.HashMap; import java.util.Map; import java.util.Optional; /** * 線程上下文 * * @date 2017年3月1日 * @since 1.0.0 */ public class ThreadContext { /** * 線程上下文變量的持有者 */ private final static ThreadLocal<Map<String, Object>> CTX_HOLDER = new ThreadLocal<Map<String, Object>>(); static { CTX_HOLDER.set( new HashMap<String, Object>()); } /** * traceID */ private final static String TRACE_ID_KEY = "traceId" ; /** * 會話ID */ private final static String SESSION_KEY = "token" ; /** * 操作用戶ID */ private final static String VISITOR_ID_KEY = "userId" ; /** * 操作用戶名 */ private final static String VISITOR_NAME_KEY = "userName" ; /** * 客戶端IP */ private static final String CLIENT_IP_KEY = "clientIp" ; /** * 添加內容到線程上下文中 * * @param key * @param value */ public final static void putContext(String key, Object value) { Map<String, Object> ctx = CTX_HOLDER.get(); if (ctx == null ) { return ; } ctx.put(key, value); } /** * 從線程上下文中獲取內容 * * @param key */ @SuppressWarnings ( "unchecked" ) public final static <T extends Object> T getContext(String key) { Map<String, Object> ctx = CTX_HOLDER.get(); if (ctx == null ) { return null ; } return (T) ctx.get(key); } /** * 獲取線程上下文 */ public final static Map<String, Object> getContext() { Map<String, Object> ctx = CTX_HOLDER.get(); if (ctx == null ) { return null ; } return ctx; } /** * 刪除上下文中的key * * @param key */ public final static void remove(String key) { Map<String, Object> ctx = CTX_HOLDER.get(); if (ctx != null ) { ctx.remove(key); } } /** * 上下文中是否包含此key * * @param key * @return */ public final static boolean contains(String key) { Map<String, Object> ctx = CTX_HOLDER.get(); if (ctx != null ) { return ctx.containsKey(key); } return false ; } /** * 清空線程上下文 */ public final static void clean() { CTX_HOLDER.remove(); } /** * 初始化線程上下文 */ public final static void init() { CTX_HOLDER.set( new HashMap<String, Object>()); } /** * 設置traceID數據 */ public final static void putTraceId(String traceId) { putContext(TRACE_ID_KEY, traceId); } /** * 獲取traceID數據 */ public final static String getTraceId() { return getContext(TRACE_ID_KEY); } /** * 設置會話的用戶ID */ public final static void putUserId(Integer userId) { putContext(VISITOR_ID_KEY, userId); } /** * 設置會話的用戶ID */ public final static int getUserId() { Integer val = getContext(VISITOR_ID_KEY); return val == null ? 0 : val; } /** * 設置會話的用戶名 */ public final static void putUserName(String userName) { putContext(VISITOR_NAME_KEY, userName); } /** * 獲取會話的用戶名稱 */ public final static String getUserName() { return Optional.ofNullable(getContext(VISITOR_NAME_KEY)) .map(name -> String.valueOf(name)) .orElse( "" ); } /** * 取出IP * * @return */ public static final String getClientIp() { return getContext(CLIENT_IP_KEY); } /** * 設置IP * * @param ip */ public static final void putClientIp(String ip) { putContext(CLIENT_IP_KEY, ip); } /** * 設置會話ID * * @param token */ public static void putSessionId(String token) { putContext(SESSION_KEY, token); } /** * 獲取會話ID * * @param token */ public static String getSessionId(String token) { return getContext(SESSION_KEY); } /** * 清空會話數據 */ public final static void removeSessionId() { remove(SESSION_KEY); } } |
2、添加工具類TraceUtil
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
|
import java.util.UUID; import org.slf4j.MDC; import ThreadContext; /** * trace工具 * * @date 2017年3月10日 * @since 1.0.0 */ public class TraceUtil { public static void traceStart() { ThreadContext.init(); String traceId = generateTraceId(); MDC.put( 'traceId' , traceId); ThreadContext.putTraceId(traceId); } public static void traceEnd() { MDC.clear(); ThreadContext.clean(); } /** * 生成跟蹤ID * * @return */ private static String generateTraceId() { return UUID.randomUUID().toString(); } } |
3、添加ContextFilter
對于每個請求隨機生成RequestID并放入MDC
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
|
import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.filter.OncePerRequestFilter; import com.pingan.manpan.common.util.TraceUtil; import com.pingan.manpan.user.dto.ThreadContext; import com.pingan.manpan.web.common.surpport.IpUtils; /** * 上下文Filter * * @date 2017/3/10 * @since 1.0.0 */ //@Order 標記組件的加載順序 @Order (Ordered.HIGHEST_PRECEDENCE) public class ContextFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { ThreadContext.putClientIp(IpUtils.getClientIp(request)); TraceUtil.traceStart(); filterChain.doFilter(request, response); } finally { TraceUtil.traceEnd(); } } } |
4、在webConfiguriation注冊filter
1
2
3
4
5
6
7
8
9
10
11
12
|
/** * 請求上下文,應該在最外層 * * @return */ @Bean public FilterRegistrationBean requestContextRepositoryFilterRegistrationBean() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter( new ContextFilter()); filterRegistrationBean.addUrlPatterns( "/*" ); return filterRegistrationBean; } |
5、修改log4j日志配置文件,設置日志traceId
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
|
<? xml version = "1.0" encoding = "UTF-8" ?> < configuration > < jmxConfigurator /> < property name = "LOG_LEVEL_PATTERN" value = "%X{traceId:-} %5p" /> < property name = "LOG_FILE" value = "${LOG_PATH}/web.logx" /> < property name = "LOG_FILE_SUFFIX" value = ".logx" /> < include resource = "org/springframework/boot/logging/logback/defaults.xml" /> < include resource = "org/springframework/boot/logging/logback/console-appender.xml" /> < appender name = "FILE" class = "ch.qos.logback.core.rolling.RollingFileAppender" > < encoder > < pattern >${FILE_LOG_PATTERN}</ pattern > </ encoder > < file >${LOG_FILE}${LOG_FILE_SUFFIX}</ file > < rollingPolicy class = "ch.qos.logback.core.rolling.TimeBasedRollingPolicy" > < fileNamePattern >${LOG_FILE}.%d{yyyy-MM-dd}${LOG_FILE_SUFFIX}</ fileNamePattern > </ rollingPolicy > </ appender > < appender name = "SYSLOG" class = "ch.qos.logback.classic.net.SyslogAppender" > < syslogHost >127.0.0.1</ syslogHost > < facility >local6</ facility > < port >514</ port > < suffixPattern >${FILE_LOG_PATTERN}</ suffixPattern > </ appender > < logger name = "druid.sql" level = "DEBUG" /> < root level = "INFO" > < appender-ref ref = "CONSOLE" /> < appender-ref ref = "FILE" /> < appender-ref ref = "SYSLOG" /> </ root > </ configuration > |
log4j2實現日志跟蹤
日志跟蹤
在每條日志前添加一個隨機字符串并且確保同一個請求的字符串相同。如下:c6019df137174d2b98631474db4156b7為此次請求的標識。通過次標識可以查詢到所有該請求的日志信息
[traceID:c6019df137174d2b98631474db4156b7]-[2020-08-11 19:56:58:204]-[http-nio-8803-exec-4]-
[traceID:c6019df137174d2b98631474db4156b7]-[2020-08-11 19:56:58:204]-[http-nio-8803-exec-4]-
[traceID:c6019df137174d2b98631474db4156b7]-[2020-08-11 19:56:58:205]-[http-nio-8803-exec-4]-
[traceID:c6019df137174d2b98631474db4156b7]-[2020-08-11 19:56:58:205]-[http-nio-8803-exec-4]-
[traceID:c6019df137174d2b98631474db4156b7]-[2020-08-11 19:56:58:209]-[http-nio-8803-exec-4]-
[traceID:c6019df137174d2b98631474db4156b7]-[2020-08-11 19:56:58:214]-[http-nio-8803-exec-4]-
[traceID:c6019df137174d2b98631474db4156b7]-[2020-08-11 19:56:58:223]-[http-nio-8803-exec-4]-
[traceID:c6019df137174d2b98631474db4156b7]-[2020-08-11 19:56:58:224]-[http-nio-8803-exec-4]-
同時可以將此標識返回給前端,便于問題查詢。traceID: c6019df137174d2b98631474db4156b7
1
2
3
4
5
6
7
8
9
10
11
12
13
|
Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: http: //test.page.qingin.cn Cache-Control: max-age= 30 Connection: keep-alive Content-Type: application/json;charset=UTF- 8 Date: Tue, 11 Aug 2020 12 : 02 : 19 GMT Expires: Tue, 11 Aug 2020 12 : 02 : 49 GMT Server: nginx/ 1.16 . 1 traceID: c6019df137174d2b98631474db4156b7 Transfer-Encoding: chunked Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers |
我們可以通過過濾器實現以上的功能
Log4j2Filter.java
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
|
package com.generator.admin.filter; import org.apache.logging.log4j.ThreadContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.UUID; /** * @calssName Log4j2Filter * @Description 對用戶的請求添加日志編號,并將此編號返回給前端,便于日志查詢 */ @WebFilter (filterName = "Log4j2Filter" , urlPatterns = "/*" , initParams = { @WebInitParam (name = "DESCRIPTION" , value = "這是Log4j2Filter過濾器" )}) public class Log4j2Filter implements Filter { private String description; public static final String TRACE_ID = "traceID" ; private static final Logger logger = LoggerFactory.getLogger(Log4j2Filter. class ); @Override public void init(FilterConfig filterConfig) throws ServletException { description = filterConfig.getInitParameter( "DESCRIPTION" ); System.out.println( "過濾器初始化:" + description); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse resp = (HttpServletResponse) servletResponse; // 生成一個隨機數給到前端 String traceId = UUID.randomUUID().toString().replace( "-" , "" ); // 隨機數放到此線程的上下文中,可以在每條日志前加入。具體看下面log4j2.xml ThreadContext.put(TRACE_ID, traceId); // 隨機數放到Header中,在Response Headers中可查看到此數據 resp.addHeader(TRACE_ID, traceId); filterChain.doFilter(req, resp); ThreadContext.clearAll(); } @Override public void destroy() { System.out.println( "過濾器,被銷毀:" + description); } } |
log4j2.xml <PatternLayout pattern="[traceID:%X{traceID}]-[%d{yyyy-MM-dd HH:mm:ss:SSS}]-[%t]-[%p]-[%l]-%m%n"/>
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
|
<? xml version = "1.0" encoding = "UTF-8" ?> <!--設置log4j2的自身log級別為warn--> <!--日志級別以及優先級排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --> <!--Configuration后面的status,這個用于設置log4j2自身內部的信息輸出,可以不設置,當設置成trace時,你會看到log4j2內部各種詳細輸出--> <!--monitorInterval:Log4j能夠自動檢測修改配置 文件和重新配置本身,設置間隔秒數--> < configuration status = "warn" monitorInterval = "30" > <!--全局參數--> < Properties > < Property name = "logPath" >logs</ Property > </ Properties > <!--先定義所有的appender--> < appenders > <!--這個輸出控制臺的配置--> < console name = "Console" target = "SYSTEM_OUT" > <!-- traceID:就是在過濾器中生成的隨機數 --> < PatternLayout pattern = "[traceID:%X{traceID}]-[%d{yyyy-MM-dd HH:mm:ss:SSS}]-[%t]-[%p]-[%l]-%m%n" /> </ console > </ appenders > <!--然后定義logger,只有定義了logger并引入的appender,appender才會生效--> < loggers > <!--過濾掉spring和mybatis的一些無用的debug信息--> < logger name = "org.springframework" level = "INFO" ></ logger > < logger name = "org.mybatis" level = "INFO" ></ logger > < logger name = "com.zaxxer" level = "WARN" ></ logger > <!-- com.generator開發/測試環境用DEBUG,并用控制臺輸出即可 --> < logger name = "com.generator" level = "DEBUG" additivity = "false" > < appender-ref ref = "Console" /> </ logger > <!-- 未指定的包都按此 level 打印日志 --> < root level = "DEBUG" > < appender-ref ref = "Console" /> </ root > </ loggers > </ configuration > |
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持服務器之家。
原文鏈接:https://segmentfault.com/a/1190000014236769