Logback MDC


Mapped Diagnostic Contexts (MDC)   (譯:診斷上下文映射)

Logback的設計目標之一是審計和調試復雜的分布式應用程序。大多數實際的分布式系統需要同時處理來自多個客戶端的請求。為了區分開每個客戶端的日志,也為了能夠快速定位某個請求日志來自哪個客戶端,最簡單地方式是,給每個客戶端的每個日志請求一個唯一標記。

為了對每個請求進行惟一的標記,用戶將上下文信息放入MDC中。

MDC類只包含靜態方法。它允許開發人員將信息放在診斷上下文中,然后通過某些logback組件檢索這些信息。MDC在每個線程的基礎上管理上下文信息。通常,在開始服務新的客戶端請求時,開發人員會將相關的上下文信息(如客戶端id、客戶端IP地址、請求參數等)插入到MDC中。如果配置得當,Logback組件將自動在每個日志條目中包含此信息。另外請注意,子線程不會自動繼承其父線程的映射診斷上下文的副本。

下面是一個例子:

假設Logback配置文件是這樣配置的:

運行這段程序,將會看到以下輸出:

請注意,在PatternLayout轉換模式中使用%X

補充一句,如果不知道這些格式字符串怎么寫,可以參加 ch.qos.logback.classic.PatternLayout

 

通常,服務器用多個線程為多個客戶端提供服務。盡管MDC類中的方法是靜態的,但是診斷上下文是在每個線程的基礎上進行管理的,允許每個服務器線程都帶有不同的MDC戳記。諸如put()和get()之類的MDC操作只影響當前線程的MDC和當前線程的子線程。其他線程中的MDC不受影響。由於MDC信息是在每個線程的基礎上管理的,所以每個線程都有自己的MDC副本。因此,開發人員在使用MDC編程時不需要擔心線程安全或同步問題,因為它可以安全、透明地處理這些問題。

正如我們所看到的,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的鏈路跟蹤是怎么做的

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM