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();
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();
54 executor.setMaxPoolSize();
55 executor.setQueueCapacity();
56 executor.setKeepAliveSeconds();
57 executor.setThreadNamePrefix("cjsTaskExecutor-");
58 executor.setWaitForTasksToCompleteOnShutdown(true);
59 executor.setAwaitTerminationSeconds();
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的链路跟踪是怎么做的

Logback MDC的更多相关文章

  1. logback MDC(Mapped Diagnostic Context)与分布式系统的跟踪系统

    logback MDC(Mapped Diagnostic Context)与分布式系统的跟踪系统 logback官方文档中第8章Mapped Diagnostic Context给我们提供了一些分布 ...

  2. logback MDC 使用

    有时候想在logback日志中打印请求IP.流水号这些信息,可以通过MDC(Mapped Diagnostic Contexts)实现: MDC.put("requestNo", ...

  3. Slf4j MDC 使用和 基于 Logback 的实现分析

    前言 如今,在 Java 开发中,日志的打印输出是必不可少的, 关于  有了日志之后,我们就可以追踪各种线上问题.但是,在分布式系统中,各种无关日志穿行其中,导致我们可能无法直接定位整个操作流程.因此 ...

  4. SpringBoot-总结

    SpringBoot一站式开发 官网:https://spring.io/projects/spring-boot Spring Boot可以轻松创建独立的.基于Spring的生产级应用程序,它可以让 ...

  5. LogBack sl4j 通过MDC实现日志记录区分用户Session[以Spring mvc为例]

    1.首先实现一个interceptor,在请求开始的时候MDC put一个Session标志,interceptor结束的时候remove掉   import javax.servlet.http.H ...

  6. logback输出json格式日志(包括mdc)发送到kafka

    1,pom.xml <!-- kafka --> <dependency> <groupId>com.github.danielwegener</groupI ...

  7. 在SpringBoot项目中添加logback的MDC

    在SpringBoot项目中添加logback的MDC     先看下MDC是什么 Mapped Diagnostic Context,用于打LOG时跟踪一个“会话“.一个”事务“.举例,有一个web ...

  8. logback转义符与MDC

    关于MDC的使用,可以结合filter一块使用,将需要串联的上下文的关键信息,通过header进行传递,然后通过配置%X{userId}将信息打印出来. MDC.put("userId&qu ...

  9. LogBack通过MDC实现日志记录区分用户Session

    1.首先实现一个interceptor,在请求开始的时候MDC put一个Session标志,interceptor结束的时候remove掉 public class SessionIntercept ...

随机推荐

  1. Java 学习笔记之 线程isInterrupted方法

    线程isInterrupted方法: isInterrupted()是Thread对象的方法,测试线程是否已经中断. public class ThreadRunMain { public stati ...

  2. 【实战】 elasticsearch 写入速度提升的案例分享

    文章首发投稿至InfoQ,[侠梦的开发笔记]公众号,欢迎关注 https://www.infoq.cn/article/t7b52mbzxqkwrrdpVqD2 基本配置 基本配置,5台配置为 24C ...

  3. RDD基础-笔记

    RDD编程 基础Spark中的RDD是一个不可变的分布式对象集合.每个RDD都被分为多个分区,这些分区运行在集群中的不同节点上.RDD可以包含Python.java.Scala中任意类型的对象,甚至可 ...

  4. Mysql Hash索引和B-Tree索引区别(Comparison of B-Tree and Hash Indexes)

    上篇文章中说道,Mysql中的Btree索引和Hash索引的区别,没做展开描述,今天有空,上Mysql官方文档找到了相关答案,看完之后,针对两者的区别做如下总结: 引用维基百科上的描述,来解释一下这两 ...

  5. 从零开始的 phpstorm+wamp 组合下的debug环境搭建(纯小白向)

    本文主要是为了帮自己记住每次重装系统后需要干点啥,如果能帮到你,烦请给个好评 环境说明: 1. windows10 64bit 2. wampservers 3.0.6(x86) apache2.4. ...

  6. CSDN VIP如何添加引流自定义栏目

    几个月前我也开始在csdn上开了博客,一来给自己加几个少的可怜的流量,再者,让公众号的原创文章获得更多的曝光,让有需要的同学看到. 写过csdn博客的同学都知道,默认只有打赏c币功能:也没有专门广告位 ...

  7. 自己写的Weblogic的poc

    """ 暂时只试用于Linux,先试试用一下反弹shell CVE-2017-10271的EXp """ import requests i ...

  8. Python操作三大主流数据库☝☝☝

    Python操作三大主流数据库☝☝☝ Python 标准数据库接口为 Python DB-API,Python DB-API为开发人员提供了数据库应用编程接口. Python 数据库接口支持非常多的数 ...

  9. C#版本websocket及时通信协议实现(教程示例)

    1:Websocket有java.nodejs.python.PHP.等版本 ,我现在使用的是C3版本,服务器端是Fleck.客户端和服务器端来使用websocket的,下面开始讲解如何使用: 2:在 ...

  10. opencv::源码编译

    环境:win10.vs2017.cmake .java.python3.7默认安装. opencv源码:opencv-.zip opencv拓展库源码:opencv_contrib-.zip (注意: ...