使用MDC标注日志上下文
最近在研究ELK,想通过ELK来统一管理日志,并简单分析系统的一些功能,比如:机构下的交易量,交易成功/失败的比例,单位时间内某种交易的笔数,访问系统前50IP……,但是苦于无法建立统一的分析标准,无法实施,想法是把一些业务参数打印到日志中,进行分析统计。
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的
一种方便在多线程条件下记录日志的功能
。某些应用程序采用多线程的方式来处理多个用户的请求。在一个用户的使用过程中,可能有多个不同的线程来进行处理。典型的例子是 Web 应用服务器。当用户访问某个页面时,应用服务器可能会创建一个新的线程来处理该请求,也可能从线程池中复用已有的线程。在一个用户的会话存续期间,可能有多个线程处理过该用户的请求。这使得比较难以区分不同用户所对应的日志。当需要追踪某个用户在系统中的相关日志记录时,就会变得很麻烦。
一种解决的办法是
采用自定义的日志格式,把用户的信息采用某种方式编码在日志记录中
。这种方式的问题在于要求在每个使用日志记录器的类中,都可以访问到用户相关的信息。这样才可能在记录日志时使用。这样的条件通常是比较难以满足的。MDC 的作用是解决这个问题。
MDC 可以看成是一个与当前线程绑定的哈希表
,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。
-
如果你的系统已经上线,突然有一天老板说我们增加一些用户数据到日志里分析一下。如果没有MDC我猜此时此刻你应该处于雪崩状态。MDC恰到好处的让你能够实现在日志上突如其来的一些需求
-
如果你是个代码洁癖,封装了公司LOG的操作,并且将处理线程跟踪日志号也封装了进去,但只有使用了你封装日志工具的部分才能打印跟踪日志号,其他部分(比如hibernate、mybatis、httpclient等等)日志都不会体现跟踪号。当然我们可以通过linux命令来绕过这些困扰。
-
使代码简洁、日志风格统一
-
spirng: import org.slf4j.MDC;
-
springboot: import org.apache.log4j.MDC;
@WebFilter(urlPatterns = "/*")
@Order(1)
public class MyFilter implements Filter {
private static Logger logger = LoggerFactory.getLogger(MyFilter.class);
private static ObjectMapper mapper = new ObjectMapper();
@Override
public void init(FilterConfig filterConfig) throws ServletException {
@SuppressWarnings("unchecked")
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String body = IOUtils.toString(request.getInputStream());
Map<String, Object> reqMap = mapper.readValue(body, Map.class);
MDC.put("service_name", "my_service1");
MDC.put("trcode", "123456");
MDC.put("chid", "999999");
ResponseWrapper wrapperResponse = new ResponseWrapper((HttpServletResponse) response);
ParameterRequestWrapper wrapRequest = new ParameterRequestWrapper(req, reqMap);
chain.doFilter(wrapRequest, wrapperResponse);
byte[] content = wrapperResponse.getContent();
String str = new String(content, "UTF-8");
Map<String, Object> rspMap = JSONUtil.jsonToObj(str, Map.class);
Map<String, String> rspHeadMap = (Map<String, String>) rspMap.get("rspHead");
MDC.put("rspcode", rspHeadMap.get("rspcode"));
MDC.clear();
* (non-Javadoc)
* @see javax.servlet.Filter#destroy()
@Override
public void destroy() {
* 返回值输出代理类
public class ResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer;
private ServletOutputStream out;
public ResponseWrapper(HttpServletResponse httpServletResponse) {
super(httpServletResponse);
buffer = new ByteArrayOutputStream();
out = new WrapperOutputStream(buffer);
@Override
public ServletOutputStream getOutputStream() throws IOException {
return out;
@Override
public void flushBuffer() throws IOException {
if (out != null) {
out.flush();
public byte[] getContent() throws IOException {
flushBuffer();
return buffer.toByteArray();
class WrapperOutputStream extends ServletOutputStream {
private ByteArrayOutputStream bos;
public WrapperOutputStream(ByteArrayOutputStream bos) {
this.bos = bos;
@Override
public void write(int b) throws IOException {
bos.write(b);
@Override
public boolean isReady() {
return false;
@Override
public void setWriteListener(WriteListener arg0) {
- 拦截器(暂时未实现获取controller中的返回值)
* 拦截器
* @author Fan.W
* @since 1.8
public class MDCInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(MDCInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
MDC.put("user_name", "fan wei");
MDC.put("user_id", "123456");
return true;
@Override
public void postHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
*这个方法的主要作用是用于清理资源的,当然这个方法也只能在当前这个Interceptor的preHandle方法的返回值为true时才会执行。
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
MDC.clear();
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd"
default-lazy-init="false">
<mvc:interceptors>
<bean class="com.seeker.interceptor.MDCInterceptor"/>
</mvc:interceptors>
</beans>
异常通知要先于controller中ExceptionHandler捕获异常!!!!!
@Aspect
@Component
public class MyInterceptor {
private static Logger logger = LoggerFactory.getLogger(MyInterceptor.class);
* 拦截类的入口--拦截所有controller类
@Pointcut("execution(public * com.seeker.controller..*.*(..)) ")
public void pointCut() {
* 方法调用之前调用
* @param joinPoint
@Before(value = "pointCut()")
public void doBefore(JoinPoint joinPoint) {
* 环绕通知
* @param pjp
* @return
* @throws Throwable
@SuppressWarnings({ "rawtypes", "unchecked" })
@Around("pointCut()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
String className = pjp.getTarget().getClass().getName();
String methodName = pjp.getSignature().getName() + "()";
Object[] args = pjp.getArgs();
MDC.put("service_name", "my_service1");
MDC.put("trcode", "123456");
MDC.put("chid", "999999");
Object obj = pjp.proceed();
MDC.put("rspcode", "0000");
return obj;
* 异常通知:pjp.proceed();跑出异常即捕获,先于@ExceptionHandler中捕获到
* @param joinPoint
* @param e
@AfterThrowing(pointcut = "pointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
if (e instanceof BizException) {
MDC.put("rspcode", ((BizException) e).messageCode);
} else {
MDC.put("rspcode", "9999");
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:aspectj-autoproxy expose-proxy="true"></aop:aspectj-autoproxy>
</beans>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>[%X{service_name}] %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{chid}] [%X{trcode}] [%X{rspcode}] - %m%n</pattern>
</encoder>
使用MDC标注日志上下文背景最近在研究ELK,想通过ELK来统一管理日志,并简单分析系统的一些功能,比如:机构下的交易量,交易成功/失败的比例,单位时间内某种交易的笔数,访问系统前50IP……,但是苦于无法建立统一的分析标准,无法实施,想法是把一些业务参数打印到日志中,进行分析统计。简介 MDC(Mapped Diagnostic Context,映射调试上下文)是 lo...
一、为什么会有MDC(MappedDiagnosticContext)?
审计和调试分布式应用是logback的设计目标之一,在多线程环境中,不同的线程会处理不同的客户端,为了区分不同客户端日志输出,一种轻量级但不可取的做法是为每个实例化一个新的、完全分离日志记录器,这种方法会产生很多的日志记录器而且难以管理。
更轻量级的做法是唯一标记每个来自客户端的日志请求,为了唯一标记每个请求,用户把上下...
可以通过线程 ID 来区分,但线程是可复用的。
解决方法是 MDC。
通过添加 filter,请求到达时,把 traceId 放到 MDC,这样每次请求生命周期,打出的日志就可以带上唯一标识。
1. 实现 TraceInterceptor
import java.util.UUI...
我们在调试代码时会打很多日志,这些错综复杂的日志往往混杂在一起,很难筛选出某个请求链路的日志。我们就希望在每个请求到来时生成一个唯一的traceid,可以set到请求头信息中,打日志时带上就可以快速筛选请求链路的日志了。slf4j提供了这样的功能(MDC),slf4j用ThreadLocal来存储traceid。
首先注册一个拦截器,在preHandle方法中调用MDC.put()方法,保存随机生成的id。在afterCompletion方法中删除。
public class TraceIntercep
如今,在 Java 开发中,日志的打印输出是必不可少的,Slf4j + LogBack 的组合是最通用的方式。
关于 Slf4j 的介绍,请参考本博客http://ketao1989.github.io/posts/Java-slf4j-Introduce.html
有了日志之后,我们就可以追踪各种线上问题。但是,在分布式系统中,各种无关日志穿行其中,导致我们可能无法直接定
文章目录目标诊断上下文映射 (MDC)引入MDC 的目的MDC 的作用用法示例高级用法代码示例一些问题MDC 与线程管理代码示例MDCInsertingServletFilter注意点
了解MDC基本概念和用法
参考:MDC官方文档 推荐参考
阅读前需要先了解logback的基本内容。以下内容,都是从官方文档引用,只是增加了一些示例情况。
诊断上下文映射 (MDC)
引入MDC 的目的
在一个多线程程序中,不同线程处理不同客户端的请求,如果对每个客户端都实例化一个新的且独立的 logger对象
MDC(Mapped Diagnostic Context)是log4j提供的一种机制,用于在日志输出中添加自定义的上下文信息。MDC的使用方式比较简单,只需要在代码中设置MDC的键值对,然后在log4j的输出模板中使用对应的键名即可。例如:
```java
import org.apache.log4j.Logger;
import org.apache.log4j.MDC;
public class TestLog {
private static final Logger logger = Logger.getLogger(TestLog.class);
public static void main(String[] args) {
MDC.put("user", "Tom");
logger.info("Hello, world!");
MDC.remove("user");
在上面的例子中,我们使用MDC添加了一个名为"user"的键值对,然后在log4j的输出模板中使用"%X{user}"来输出该键的值。
如果您在使用MDC时出现了报错,可能是由于以下原因:
1. MDC没有正确导入。请确保您的项目中已经正确导入了log4j的依赖,并且在代码中正确导入了MDC的类。
2. MDC的键名或键值不合法。请确保您使用的键名和键值都是合法的字符串,并且没有使用特殊字符或空格。
3. MDC的键名或键值包含了log4j的占位符。请注意,log4j的输出模板中使用的占位符是以"%xxx"的形式表示的,如果您的键名或键值中包含了这种形式的字符串,可能会导致log4j解析出错。
4. MDC的键名或键值超过了log4j的限制。请注意,log4j默认限制MDC的键名和键值的长度分别为100和1000,如果您的键名或键值超过了这个限制,可能会导致log4j出现异常。
如果您仍然无法解决报错问题,请提供具体的报错信息和代码片段,我可以帮您进一步分析。