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