MDC实现日志打印请求唯一标识

在每次请求中打出的每条日志中添加统一的请求唯一标识。通过搜索日志唯一标识,这样就可以非常高效精准排查问题。

例如:

2018-12-21 10:21:26.329 [http-nio-8080-exec-2] [my-server-logtag] [] INFO  org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'dispatcherServlet': initialization completed in 18 ms 
2018-12-21 10:21:26.363 [http-nio-8080-exec-2] [my-server-logtag] [aed35d43-3fa2-46e1-bb72-ce9f50c3fe4d] ERROR com.code3.modules.controller.MyController - 日志1 
2018-12-21 10:21:26.367 [http-nio-8080-exec-2] [my-server-logtag] [aed35d43-3fa2-46e1-bb72-ce9f50c3fe4d] ERROR com.code3.advice.GlobalExceptionHandler - 系统异常,{} 
com.code3.base.BusinessException: 异常信息1
    at com.code3.modules.controller.MyController.logtag(MyController.java:18)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at 

通过日志标识:aed35d43-3fa2-46e1-bb72-ce9f50c3fe4d,就可以精准定位

一、简单的解决思路

想要跟踪请求,第一个想到的就是当请求来时生成一个traceId放在ThreadLocal里,然后打印时去取就行了。但在不改动原有输出语句的前提下自然需要日志框架的支持了,搜索的一番发现主流日志框架都提供了MDC功能。

MDC 介绍

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。

MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。

MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

二、拦截器实现唯一标识

1、自定义日志标识生成拦截器

LogInterceptor.java

package com.interceptors.logtag.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

/**
 * 

日志拦截器

* * @author zetting * @date 2018/8/27 11:18 */
@Component public class LogInterceptor extends HandlerInterceptorAdapter { private Logger log = LoggerFactory.getLogger(this.getClass()); /** * 日志跟踪标识 */ private static final String TRACE_ID = "TRACE_ID"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String traceId = UUID.randomUUID().toString(); if (StringUtils.isEmpty(MDC.get(TRACE_ID))) { MDC.put(TRACE_ID, traceId); } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { MDC.remove(TRACE_ID); } }

2、使用日志标识生成拦截器

  1. 在WebMvc配置加上LogInterceptor
package com.interceptors.logtag.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.ruoyi.framework.interceptor.LogInterceptor;

/**
 * MVC配置
 *
 * @author zet
 * @date 2018-12-21 22:30
 */
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
    @Autowired
    private LogInterceptor logInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(logInterceptor);
    }
}
  1. 在logback文件加上[%X{TRACE_ID}]

<configuration>
    <jmxConfigurator/>

    <property name="app-name" value="my-server" />
    <property name="filename" value="server" />
    <property name="module-name" value="logtag" />

    <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [${app-name}-${module-name}] [%X{TRACE_ID}] %highlight(%-5level) %cyan(%logger{50}) - %highlight(%msg) %npattern>
        encoder>
    appender>

    <appender name="fileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/data/log-center/${app-name}/${module-name}/${filename}.logfile>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>/data/log-center/${app-name}/${module-name}/${filename}.%d{yyyy-MM-dd}.logfileNamePattern>
            <maxHistory>30maxHistory>
        rollingPolicy>
        <encoder>
            <pattern>[%date{ISO8601}] [%level] %logger{80} [%thread] [${app-name}-${module-name}][%X{TRACE_ID}] ${app-name}-${module-name} ==> %msg%npattern>
        encoder>
    appender>

    <root level="INFO" additivity="false">
        <appender-ref ref="fileAppender"/>
        <appender-ref ref="consoleAppender"/>
    root>
configuration>

3、演示

img

打出的日志标识.jpg

4、附

gitee源码:https://gitee.com/zetting/my-gather/tree/master/springboot-interceptors-logtag

原创:https://www.jianshu.com/p/b74ade8bbb57

三、ServletComponentScan注解实现唯一标识

SpringBootApplication使用@ServletComponentScan注解作用

  • Servlet可以直接通过@WebServlet注解自动注册
  • Filter可以直接通过@WebFilter注解自动注册
  • Listener可以直接通过@WebListener 注解自动注册

1、自定义LogbackFilter

import java.io.IOException;
import java.util.UUID;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;

import org.slf4j.MDC;

@WebFilter(filterName = "logbackFilter", urlPatterns = "/*")
public class LogbackFilter implements Filter {

	public void init(FilterConfig filterConfig) throws ServletException {
	}

	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
			throws IOException, ServletException {
		UUID uuid = UUID.randomUUID();
		MDC.put("traceid", uuid.toString().replace("-", ""));
		try {
			filterChain.doFilter(servletRequest, servletResponse);
		} finally {
			MDC.remove("traceid");// 保证一次请求一个唯一标识
		}
	}

	@Override
	public void destroy() {
	}
}

2、logback.xml配置


<configuration>
   	
	<contextName>ruoyi-admincontextName>
    
	<property name="log.pattern" value="%-5level [%contextName] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %logger - traceId:%X{traceid} - %msg%n" />
   	<include resource="org/springframework/boot/logging/logback/defaults.xml" />

   	<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
      	<encoder>
         	<pattern>${log.pattern}pattern>
         	<charset>utf8charset>
      	encoder>
   	appender>

   <root level="INFO">
      <appender-ref ref="CONSOLE" />
   root>
configuration>

3、演示

DEBUG [ruoyi-admin] 2020-04-14 14:34:02.402 [http-nio-80-exec-2] com.ruoyi.system.mapper.SysConfigMapper.selectConfigList_COUNT - traceId:a8309218ae64434d856f922a1abac3b6 - ==>  Preparing: SELECT count(0) FROM sys_config 
DEBUG [ruoyi-admin] 2020-04-14 14:34:02.404 [http-nio-80-exec-2] com.ruoyi.system.mapper.SysConfigMapper.selectConfigList_COUNT - traceId:a8309218ae64434d856f922a1abac3b6 - ==> Parameters: 
DEBUG [ruoyi-admin] 2020-04-14 14:34:02.440 [http-nio-80-exec-2] com.ruoyi.system.mapper.SysConfigMapper.selectConfigList_COUNT - traceId:a8309218ae64434d856f922a1abac3b6 - <==      Total: 1
DEBUG [ruoyi-admin] 2020-04-14 14:34:02.444 [http-nio-80-exec-2] com.ruoyi.system.mapper.SysConfigMapper.selectConfigList - traceId:a8309218ae64434d856f922a1abac3b6 - ==>  Preparing: SELECT config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark FROM sys_config order by create_time desc LIMIT ? 
DEBUG [ruoyi-admin] 2020-04-14 14:34:02.445 [http-nio-80-exec-2] com.ruoyi.system.mapper.SysConfigMapper.selectConfigList - traceId:a8309218ae64434d856f922a1abac3b6 - ==> Parameters: 10(Integer)
DEBUG [ruoyi-admin] 2020-04-14 14:34:02.479 [http-nio-80-exec-2] com.ruoyi.system.mapper.SysConfigMapper.selectConfigList - traceId:a8309218ae64434d856f922a1abac3b6 - <==      Total: 4

4、tips

  1. 自定义 LogbackFilter 需要实现 Filter

    • springboot方式需要在 LogbackFilter 类加上@WebFilter(filterName = “logbackFilter”, urlPatterns = “/*”)注解
    • 启动类 ApplicationBootstrap 加上@ServletComponentScan注解
  2. %cyan(%X{wtraceid}) 自定义traceid的颜色

    日志颜色设置参考:

    https://www.cnblogs.com/sxdcgaq8080/p/7885340.html

    https://blog.csdn.net/qq_34313827/article/details/80394325

    注:这个部署线上环境时候切记要注释掉!

原创:https://www.cnblogs.com/xiao-xin-xin/p/10729280.html

封装参考:https://blog.csdn.net/hanchao5272/article/details/92107651

四、线程池实现唯一标识

前两种实现的问题:

  • 线程池中的线程会打印错误的traceId
  • 调用下游服务后会生成新的traceId,无法继续跟踪

MDC使用的InheritableThreadLocal只是在线程被创建时继承,但是线程池中的线程是复用的,后续请求使用已有的线程将打印出之前请求的traceId。这时候就需要对线程池进行一定的包装,在线程在执行时读取之前保存的MDC内容。不仅自身业务会用到线程池,spring项目也使用到了很多线程池,比如@Async异步调用,zookeeper线程池、kafka线程池等。不管是哪种线程池都大都支持传入指定的线程池实现。

详细参考:

https://blog.csdn.net/yangcheng33/article/details/80796129

https://github.com/TiantianUpup/springboot-log/tree/master/springboot-trace

https://www.jianshu.com/p/ae855779f30b

五、手写分布式链路追踪框架

分布式环境下,由于系统不再是单点,一个业务可能会有多个系统参与执行。这就涉及到请求在多个系统之间来回转发,如果没有一个全局的标识能够追踪单个业务调用,那么就会在发生业务故障时盲目查日志,造成问题定位困难。
如果我们能在系统间透明的传递一个全局唯一ID,将请求在分布式系统中的流转路径聚合并在使用中保存和传递该id,最后通过页面进行可视化的展示,让开发、运维等人员能够轻松的发现系统故障,那么对问题的快速定位,系统的快速恢复有着非凡的意义。

实现目标

  1. 基于HTTP同步调用,能实现TraceId的传递,SpanId的生成及传递,ParentSpanId的获取
  2. 应用层无感知,业务请求无需显示传递链路信息
  3. 支持Spring框架

本文我们就根据谷歌的dapper论文的模型,自己开发一个最简的分布式链路追踪框架。工程上传至github,可以自行下载源码研究。

详细参考:自己写分布式链路追踪框架1–TraceId/SpanId/ParentSpanId生成及传递

你可能感兴趣的:(方案)