Mapped Diagnostic Contexts (MDC) (譯:診斷上下文映射)html
Logback的設計目標之一是審計和調試複雜的分佈式應用程序。大多數實際的分佈式系統須要同時處理來自多個客戶端的請求。爲了區分開每一個客戶端的日誌,也爲了可以快速定位某個請求日誌來自哪一個客戶端,最簡單地方式是,給每一個客戶端的每一個日誌請求一個惟一標記。java
爲了對每一個請求進行唯一的標記,用戶將上下文信息放入MDC中。web
MDC類只包含靜態方法。它容許開發人員將信息放在診斷上下文中,而後經過某些logback組件檢索這些信息。MDC在每一個線程的基礎上管理上下文信息。一般,在開始服務新的客戶端請求時,開發人員會將相關的上下文信息(如客戶端id、客戶端IP地址、請求參數等)插入到MDC中。若是配置得當,Logback組件將自動在每一個日誌條目中包含此信息。另外請注意,子線程不會自動繼承其父線程的映射診斷上下文的副本。spring
下面是一個例子:apache
假設Logback配置文件是這樣配置的:編程
運行這段程序,將會看到如下輸出:安全
請注意,在PatternLayout轉換模式中使用%X服務器
補充一句,若是不知道這些格式字符串怎麼寫,能夠參加 ch.qos.logback.classic.PatternLayoutsession
一般,服務器用多個線程爲多個客戶端提供服務。儘管MDC類中的方法是靜態的,可是診斷上下文是在每一個線程的基礎上進行管理的,容許每一個服務器線程都帶有不一樣的MDC戳記。諸如put()和get()之類的MDC操做隻影響當前線程的MDC和當前線程的子線程。其餘線程中的MDC不受影響。因爲MDC信息是在每一個線程的基礎上管理的,因此每一個線程都有本身的MDC副本。所以,開發人員在使用MDC編程時不須要擔憂線程安全或同步問題,由於它能夠安全、透明地處理這些問題。app
正如咱們所看到的,MDC很是有用。咱們能夠在MDC中設置用戶名,這樣即可以從日誌中追蹤用戶行爲。可是,由於MDC是在每一個線程的基礎上管理數據的,並且項目中基本上都是用的線程池,因此回收線程的服務器可能會致使MDC中包含錯誤信息。爲了使MDC中包含的信息在處理請求時始終正確,一種可能的方法是在線程開始時存儲用戶名,並在線程結束時刪除它。在這種狀況下,servlet過濾器就派上用場了。
MDC與線程管理
worker線程不老是能夠從啓動線程那裏繼承診斷上下文副本,例如當使用java.util.concurrent.Executors來管理線程時就不能。在這種狀況下,推薦在提交任務到executor以前在原始線程上先調用MDC.getCopyOfContextMap()。當任務啓動後,第一個動做應該先調用MDC.setContextMapValues()來關聯這個原始MDC的副本。
MDCInsertingServletFilter
在Web應用程序中,知道給定的HTTP請求的主機名、請求URL以及user-agent等信息一般是很是有用的。MDCInsertingServletFilter插入這些數據到MDC中。
爲了引入MDCInsertingServletFilter過濾器,添加如下到web.xml中
若是有多個過濾器,請務必確保MDCInsertingServletFilter在全部其餘過濾器的前面。
示例:追蹤用戶行爲(舉個栗子)
使用過濾器或者攔截器都行
pom.xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4 <modelVersion>4.0.0</modelVersion>
5 <parent>
6 <groupId>org.springframework.boot</groupId>
7 <artifactId>spring-boot-starter-parent</artifactId>
8 <version>2.2.1.RELEASE</version>
9 <relativePath/> <!-- lookup parent from repository -->
10 </parent>
11 <groupId>com.cjs.example</groupId>
12 <artifactId>demo123</artifactId>
13 <version>0.0.1-SNAPSHOT</version>
14 <name>demo123</name>
15
16 <properties>
17 <java.version>1.8</java.version>
18 </properties>
19
20 <dependencies>
21 <dependency>
22 <groupId>org.springframework.boot</groupId>
23 <artifactId>spring-boot-starter-web</artifactId>
24 </dependency>
25
26 <dependency>
27 <groupId>ch.qos.logback</groupId>
28 <artifactId>logback-classic</artifactId>
29 <version>1.2.3</version>
30 </dependency>
31
32 <dependency>
33 <groupId>org.projectlombok</groupId>
34 <artifactId>lombok</artifactId>
35 <optional>true</optional>
36 </dependency>
37 </dependencies>
38
39 <build>
40 <plugins>
41 <plugin>
42 <groupId>org.springframework.boot</groupId>
43 <artifactId>spring-boot-maven-plugin</artifactId>
44 </plugin>
45 </plugins>
46 </build>
47
48 </project>
過濾器配置
1 package com.cjs.example.config;
2
3 import ch.qos.logback.classic.helpers.MDCInsertingServletFilter;
4 import org.springframework.boot.web.servlet.FilterRegistrationBean;
5 import org.springframework.context.annotation.Bean;
6 import org.springframework.context.annotation.Configuration;
7
8 import java.util.Arrays;
9
10 /**
11 * @author ChengJianSheng
12 * @date 2019-11-10
13 */
14 @Configuration
15 public class WebConfig {
16
17 @Bean
18 public FilterRegistrationBean registration() {
19 MDCInsertingServletFilter filter = new MDCInsertingServletFilter();
20 FilterRegistrationBean registration = new FilterRegistrationBean(filter);
21 registration.setUrlPatterns(Arrays.asList("/*"));
22 registration.setOrder(1);
23 return registration;
24 }
25
26 // @Bean
27 // public MDCInsertingServletFilter mdcInsertingServletFilter() {
28 // return new MDCInsertingServletFilter();
29 // }
30
31 }
定義一個攔截器
1 package com.cjs.example.interceptor;
2
3 import org.slf4j.MDC;
4 import org.springframework.web.servlet.HandlerInterceptor;
5 import org.springframework.web.servlet.ModelAndView;
6
7 import javax.servlet.http.HttpServletRequest;
8 import javax.servlet.http.HttpServletResponse;
9 import java.util.UUID;
10
11 /**
12 * @author ChengJianSheng
13 * @date 2019-11-10
14 */
15 public class MyInterceptor implements HandlerInterceptor {
16
17 private final String SESSION_KEY = "sessionId";
18
19 @Override
20 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
21 String sessionId = UUID.randomUUID().toString().replace("-", "");
22 MDC.put(SESSION_KEY, sessionId);
23 return true;
24 }
25
26 @Override
27 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
28
29 }
30
31 @Override
32 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
33 MDC.remove(SESSION_KEY);
34 }
35 }
攔截器配置
1 package com.cjs.example.config;
2
3 import com.cjs.example.interceptor.MyInterceptor;
4 import org.springframework.context.annotation.Configuration;
5 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
6 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
7
8 /**
9 * @author ChengJianSheng
10 * @date 2019-11-10
11 */
12 @Configuration
13 public class InterceptorConfig implements WebMvcConfigurer {
14
15 @Override
16 public void addInterceptors(InterceptorRegistry registry) {
17 registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
18 }
19 }
Controller和Service
1 package com.cjs.example.controller;
2
3 import com.cjs.example.service.HelloService;
4 import lombok.extern.slf4j.Slf4j;
5 import org.springframework.beans.factory.annotation.Autowired;
6 import org.springframework.beans.factory.annotation.Qualifier;
7 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
8 import org.springframework.web.bind.annotation.GetMapping;
9 import org.springframework.web.bind.annotation.RequestMapping;
10 import org.springframework.web.bind.annotation.RequestParam;
11 import org.springframework.web.bind.annotation.RestController;
12
13 /**
14 * @author ChengJianSheng
15 * @date 2019-11-10
16 */
17 @Slf4j
18 @RestController
19 @RequestMapping("/hello")
20 public class HelloController {
21
22 @Autowired
23 private HelloService helloService;
24
25 @Autowired
26 @Qualifier("taskExecutor")
27 private ThreadPoolTaskExecutor taskExecutor;
28
29 @GetMapping("/sayHello")
30 public String sayHello(@RequestParam("name") String name) {
31 log.info("收到請求:name={}", name);
32 String str = "Hello, " + name;
33 log.info(str);
34 helloService.print();
35
36 // Map<String, String> ctx = MDC.getCopyOfContextMap();
37 // new Thread(()->{
38 // MDC.setContextMap(ctx);
39 // log.info("1111");}).start();
40
41 taskExecutor.submit(()->{log.info("1234234");});
42
43 return str;
44 }
45
46 }
1 package com.cjs.example.service;
2
3 import lombok.extern.slf4j.Slf4j;
4 import org.springframework.scheduling.annotation.Async;
5 import org.springframework.stereotype.Service;
6
7 /**
8 * @author ChengJianSheng
9 * @date 2019-11-10
10 */
11 @Slf4j
12 @Service
13 public class HelloService {
14
15 /**
16 * 使用@Aysnc異步執行任務時,雖然指定了Executor可是在真正執行時卻不會調咱們重寫的這個submit或execute方法,於是沒法將父線程MDC拷貝到子線程中
17 * 實踐代表,必須手動顯式調用submit或execute才行,這就至關於咱們本身重寫了任務的Runnable
18 */
19 @Async("taskExecutor")
20 public void print() {
21 log.info("This is apple");
22 }
23 }
線程池配置(可選)
1 package com.cjs.example.config;
2
3 import org.slf4j.MDC;
4 import org.springframework.context.annotation.Bean;
5 import org.springframework.context.annotation.Configuration;
6 import org.springframework.scheduling.annotation.EnableAsync;
7 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
8
9 import java.util.Map;
10 import java.util.concurrent.Future;
11 import java.util.concurrent.ThreadPoolExecutor;
12
13 /**
14 * http://logback.qos.ch/manual/mdc.html#MDC And Managed Threads
15 * @author ChengJianSheng
16 * @date 2019-11-10
17 */
18 @EnableAsync(proxyTargetClass = true)
19 @Configuration
20 public class ThreadPoolConfig {
21
22 @Bean(name = "taskExecutor")
23 public ThreadPoolTaskExecutor taskExecutor() {
24 // 對於使用@Async方式提交的異步任務不會生效
25 ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(){
26 @Override
27 public void execute(Runnable task) {
28 Map<String, String> contextMap = MDC.getCopyOfContextMap();
29 super.execute(()->{
30 MDC.setContextMap(contextMap);
31 try {
32 task.run();
33 } finally {
34 MDC.clear();
35 }
36 });
37 }
38
39 @Override
40 public Future<?> submit(Runnable task) {
41 Map<String, String> contextMap = MDC.getCopyOfContextMap();
42 return super.submit(()->{
43 MDC.setContextMap(contextMap);
44 try {
45 task.run();
46 } finally {
47 MDC.clear();
48 }
49 });
50 }
51 };
52
53 executor.setCorePoolSize(10);
54 executor.setMaxPoolSize(20);
55 executor.setQueueCapacity(1000);
56 executor.setKeepAliveSeconds(60);
57 executor.setThreadNamePrefix("cjsTaskExecutor-");
58 executor.setWaitForTasksToCompleteOnShutdown(true);
59 executor.setAwaitTerminationSeconds(10);
60 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
61
62 return executor;
63 }
64 }
logback配置
1 <?xml version="1.0" encoding="UTF-8"?>
2 <configuration>
3
4 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
5 <layout class="ch.qos.logback.classic.PatternLayout">
6 <Pattern>%d{HH:mm:ss.SSS} [%thread] [%X{req.remoteHost}] [%X{req.requestURI}] [%X{sessionId}] %-5level %logger{36} - %msg%n</Pattern>
7 </layout>
8 </appender>
9
10 <root level="INFO">
11 <appender-ref ref="STDOUT" />
12 </root>
13 </configuration>
運行效果
訪問http://localhost:8080/hello/sayHello?name=zhangsan
17:09:08.524 [http-nio-8080-exec-1] [0:0:0:0:0:0:0:1] [/hello/sayHello] [551f5632eddc4a1ca4a387464e954143] INFO c.c.e.controller.HelloController - 收到請求:name=zhangsan 17:09:08.525 [http-nio-8080-exec-1] [0:0:0:0:0:0:0:1] [/hello/sayHello] [551f5632eddc4a1ca4a387464e954143] INFO c.c.e.controller.HelloController - Hello, zhangsan 17:09:15.343 [cjsTaskExecutor-2] [0:0:0:0:0:0:0:1] [/hello/sayHello] [551f5632eddc4a1ca4a387464e954143] INFO c.c.e.controller.HelloController - 1234234 17:09:15.348 [cjsTaskExecutor-1] [] [] [] INFO com.cjs.example.service.HelloService - This is apple
其它
http://logback.qos.ch/manual/mdc.html
此處沒能實現@Async方式的異步任務MDC拷貝,有些遺憾,另外,跨服務調用的TraceId傳遞也是一個值得思考的問題,不知道Spring Cloud Sleuth的鏈路跟蹤是怎麼作的