java 如何实现日志追踪MDC

目录
  • java 日志追踪MDC
    • 简单的demo
  • MDC的介绍及使用
    • 1、MDC是什么?
    • 2、MDC的原理
    • 3、MDC的使用

java 日志追踪MDC

MDC ( Mapped Diagnostic Contexts ) 有了日志之后,我们就可以追踪各种线上问题。

但是,在分布式系统中,各种无关日志穿行其中,导致我们可能无法直接定位整个操作流程。

因此,我们可能需要对一个用户的操作流程进行归类标记,比如使用线程+时间戳,或者用户身份标识等;如此,我们可以从大量日志信息中grep出某个用户的操作流程,或者某个时间的流转记录。其目的是为了便于我们诊断线上问题而出现的方法工具类。

虽然,Slf4j 是用来适配其他的日志具体实现包的,但是针对 MDC功能,目前只有logback 以及 log4j 支持。
MDC

package org.slf4j;
import java.io.Closeable;
import java.util.Map;
import org.slf4j.helpers.NOPMDCAdapter;
import org.slf4j.helpers.BasicMDCAdapter;
import org.slf4j.helpers.Util;
import org.slf4j.impl.StaticMDCBinder;
import org.slf4j.spi.MDCAdapter;
public class MDC {
    static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA";
    static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder";
    static MDCAdapter mdcAdapter; 

    public static class MDCCloseable implements Closeable {
        private final String key;
        private MDCCloseable(String key) {
            this.key = key;
        }
        public void close() {
            MDC.remove(this.key);
        }
    }
    private MDC() {
    }

    static {
        try {
            mdcAdapter = StaticMDCBinder.SINGLETON.getMDCA();
        } catch (NoClassDefFoundError ncde) {
            mdcAdapter = new NOPMDCAdapter();
            String msg = ncde.getMessage();
            if (msg != null && msg.indexOf("StaticMDCBinder") != -1) {
                Util.report("Failed to load class \"org.slf4j.impl.StaticMDCBinder\".");
                Util.report("Defaulting to no-operation MDCAdapter implementation.");
                Util.report("See " + NO_STATIC_MDC_BINDER_URL + " for further details.");
            } else {
                throw ncde;
            }
        } catch (Exception e) {
            // we should never get here
            Util.report("MDC binding unsuccessful.", e);
        }
    }

    public static void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        }
        if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
        }
        mdcAdapter.put(key, val);
    }

    public static MDCCloseable putCloseable(String key, String val) throws IllegalArgumentException {
        put(key, val);
        return new MDCCloseable(key);
    }

    public static String get(String key) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        }

        if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
        }
        return mdcAdapter.get(key);
    }

    public static void remove(String key) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        }

        if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
        }
        mdcAdapter.remove(key);
    } 

    public static void clear() {
        if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
        }
        mdcAdapter.clear();
    }

    public static Map<String, String> getCopyOfContextMap() {
        if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
        }
        return mdcAdapter.getCopyOfContextMap();
    } 

    public static void setContextMap(Map<String, String> contextMap) {
        if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
        }
        mdcAdapter.setContextMap(contextMap);
    }
    public static MDCAdapter getMDCAdapter() {
        return mdcAdapter;
    }
}

简单的demo

package com.alibaba.otter.canal.common;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class LogTest {
    private static final Logger logger = LoggerFactory.getLogger(LogTest.class);
    public static void main(String[] args) {
        MDC.put("THREAD_ID", String.valueOf(Thread.currentThread().getId()));
        logger.info("纯字符串信息的info级别日志");
    }
}

logback.xml 配置

<configuration scan="true" scanPeriod=" 5 seconds">

    <jmxConfigurator />
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{56} %X{THREAD_ID} - %msg%n
            </pattern>
        </encoder>
    </appender>

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

对应的输出日志 可以看到输出了THREAD_ID

2016-12-08 14:59:32.855 [main] INFO com.alibaba.otter.canal.common.LogTest THREAD_ID 1 - 纯字符串信息的info级别日志

slf4j只是起到适配的作用 故查看实现类LogbackMDCAdapter属性

final InheritableThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new InheritableThreadLocal<Map<String, String>>();

InheritableThreadLocal 该类扩展了 ThreadLocal,为子线程提供从父线程那里继承的值:在创建子线程时,子线程会接收所有

可继承的线程局部变量的初始值,以获得父线程所具有的值。通常,子线程的值与父线程的值是一致的;但是,通过重写这个类中的 childValue 方法,子线程的值可以作为父线程值的一个任意函数。

当必须将变量(如用户 ID 和 事务 ID)中维护的每线程属性(per-thread-attribute)自动传送给创建的所有子线程时,应尽可能地采用可继承的线程局部变量,而不是采用普通的线程局部变量

验证一下

package com.alibaba.otter.canal.parse.driver.mysql;
import org.junit.Test;
public class TestInheritableThreadLocal {
    @Test
    public void testThreadLocal() {
        final ThreadLocal<String> local = new ThreadLocal<String>();
        work(local);
    }

    @Test
    public void testInheritableThreadLocal() {
        final ThreadLocal<String> local = new InheritableThreadLocal<String>();
        work(local);
    }
    private void work(final ThreadLocal<String> local) {
        local.set("a");
        System.out.println(Thread.currentThread() + "," + local.get());
        Thread t = new Thread(new Runnable() {  

            @Override
            public void run() {
                System.out.println(Thread.currentThread() + "," + local.get());
                local.set("b");
                System.out.println(Thread.currentThread() + "," + local.get());
            }
        });  

        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread() + "," + local.get());
    }
}
 

分别运行得到的输出结果

ThreadLocal 存贮输出结果
Thread[main,5,main],a
Thread[Thread-0,5,main],null
Thread[Thread-0,5,main],b
Thread[main,5,main],a

InheritableThreadLocal存贮输出结果
Thread[main,5,main],a
Thread[Thread-0,5,main],a
Thread[Thread-0,5,main],b
Thread[main,5,main],a

输出结果说明一切 对于参数传递十分有用 我知道 canal的源码中用到了MDC

在 CanalServerWithEmbedded 中的 start 和stop等方法中都有用到

    public void start(final String destination) {
        final CanalInstance canalInstance = canalInstances.get(destination);
        if (!canalInstance.isStart()) {
            try {
                MDC.put("destination", destination);
                canalInstance.start();
                logger.info("start CanalInstances[{}] successfully", destination);
            } finally {
                MDC.remove("destination");
            }
        }
    }

    public void stop(String destination) {
        CanalInstance canalInstance = canalInstances.remove(destination);
        if (canalInstance != null) {
            if (canalInstance.isStart()) {
                try {
                    MDC.put("destination", destination);
                    canalInstance.stop();
                    logger.info("stop CanalInstances[{}] successfully", destination);
                } finally {
                    MDC.remove("destination");
                }
            }
        }
    }

MDC的介绍及使用

1、MDC是什么?

MDC是(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 支持的一种方便在多线程条件下记录追踪日志的功能。通常打印出的日志会有线程号等信息来标志当前日志属于哪个线程,然而由于线程是可以重复使用的,所以并不能很清晰的确认一个请求的日志范围。处理这种情况一般有两种处理方式:

1)手动生成一个唯一序列号打印在日志中;

2)使用日志控件提供的MDC功能,生成一个唯一序列标记一个线程的日志;

两种方法的区别在于:

方法一只能标记一条日志,线程内其他日志需要人肉去筛选;

方法二标记整个线程的所有日志,方便grep命令查询;

对比可见,使用MDC功能更好。

2、MDC的原理

MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

@RunWith(SpringRunner.class)
@SpringBootTest(classes=CreditAppApplication.class)
publicclassMDCTest{

@Test
publicvoidmdcTest1(){
MDC.put("first","thefirst1");

Loggerlogger=LoggerFactory.getLogger(MDCTest.class);
MDC.put("last","thelast1");

logger.info("checkenclosed.");
logger.debug("themostbeautifultwowordsinenglish.");

MDC.put("first","thefirst2");
MDC.put("last","thelast2");

logger.info("iamnotacrook.");
logger.info("AttributedtotheformerUSpresident.17Nov1973.");
}
}

logback的配置:

3、MDC的使用

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
publicclassGlobalLogTagConfigextendsOncePerRequestFilter{
privatestaticfinalStringGLOBAL_LOG_TAG="GLOG_TAG";
privatestaticStringgenerateSeqNo(){
returnUUID.randomUUID().toString().replace("-","").substring(0,12);
}

@Override
protectedvoiddoFilterInternal(HttpServletRequesthttpServletRequest,HttpServletResponsehttpServletResponse,FilterChainfilterChain)throwsServletException,IOException{
try{
StringseqNo;
if(httpServletRequest!=null){
seqNo=httpServletRequest.getHeader(GLOBAL_LOG_TAG);

if(StringUtils.isEmpty(seqNo)){
seqNo=generateSeqNo();
}
}else{
seqNo=generateSeqNo();
}
MDC.put(GLOBAL_LOG_TAG,seqNo);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}finally{
MDC.remove(GLOBAL_LOG_TAG);
}
}
}

注意:

OncePerRequestFilter的作用是为了让每个请求只经过这个过滤器一次(因为web container的不同,有些过滤器可能被多次执行)

logback配置:

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • SpringBoot 项目添加 MDC 日志链路追踪的执行流程

    目录 1. 线程池配置 2. 拦截器配置 3. 日志文件配置 4. 使用方法示例 4.1. 异步使用 4.2. 定时任务 日志链路追踪的意思就是将一个标志跨线程进行传递,在一般的小项目中也就是在你新起一个线程的时候,或者使用线程池执行任务的时候会用到,比如追踪一个用户请求的完整执行流程. 这里用到MDC和ThreadLocal,分别由下面的包提供: java.lang.ThreadLocal org.slf4j.MDC 直接上代码: 1. 线程池配置 如果你直接通过手动新建线程来执行异步任务,想

  • Dubbo实现分布式日志链路追踪

    技术场景 在日常的开发.测试或运维的过程中,经常存在这样的场景,开发人员在代码中使用日志工具(log4j.slf4j)记录日志,比如请求ID.IP等,方便在线上快速.精准的定位问题,通过完整的日志链路清晰的进行信息定位. 一般的项目都是分层的.分布式的,在众多的日志信息中,如何区分哪些日志信息是同一请求发出来的,详细的实现如下. 技术框架 项目框架:Spring boot 分布式协调:Zookeeper.Dubbo 日志工具:Sf4j 构建工具:Maven 开发工具:IDEA 项目框架 mdc-

  • Java日志API管理最佳实践详解

    概述 对于现在的应用程序来说,日志的重要性是不言而喻的.很难想象没有任何日志记录功能的应用程序运行在生产环境中.日志所能提供的功能是多种多样的,包括记录程序运行时产生的错误信息.状态信息.调试信息和执行时间信息等.在生产环境中,日志是查找问题来源的重要依据.应用程序运行时的产生的各种信息,都应该通过日志 API 来进行记录. 很多开发人员习惯于使用 System.out.println.System.err.println 以及异常对象的 printStrackTrace 方法来输出相关信息.这

  • java 如何实现日志追踪MDC

    目录 java 日志追踪MDC 简单的demo MDC的介绍及使用 1.MDC是什么? 2.MDC的原理 3.MDC的使用 java 日志追踪MDC MDC ( Mapped Diagnostic Contexts ) 有了日志之后,我们就可以追踪各种线上问题. 但是,在分布式系统中,各种无关日志穿行其中,导致我们可能无法直接定位整个操作流程. 因此,我们可能需要对一个用户的操作流程进行归类标记,比如使用线程+时间戳,或者用户身份标识等:如此,我们可以从大量日志信息中grep出某个用户的操作流程

  • 使用log4j MDC实现日志追踪

    目录 log4j MDC实现日志追踪 1.新建线程处理类 ThreadContext 2.添加工具类TraceUtil 3.添加ContextFilter 4.在webConfiguriation注册filter 5.修改log4j日志配置文件,设置日志traceId log4j2实现日志跟踪 日志跟踪 我们可以通过过滤器实现以上的功能 log4j MDC实现日志追踪 MDC 中包含的可以被同一线程中执行的代码所访问内容.当前线程的子线程会继承其父线程中的 MDC 的内容.记录日志时,只需要从

  • java异步写日志到文件中实现代码

    java异步写日志到文件中详解 实现代码: package com.tydic.ESUtil; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.Properties; public class LogWriter { // 日志的配置文件 publi

  • Java虚拟机GC日志分析

    本文研究的主要是Java虚拟机中gc日志的理解问题,具体如下. 一.日志分析 理解GC日志是处理Java虚拟机内存问题的基本技能. 通过在java命令种加入参数来指定对应的gc类型,打印gc日志信息并输出至文件等策略. 1.编写java代码 public class ReferenceCountingGC { public Object instance = null; private static final int ONE_MB = 1024 * 1024; private byte[] b

  • Java 配置log4j日志文件路径 (附-获取当前类路径的多种操作)

    1 日志路径带来的痛点 Java 项目中少不了要和log4j等日志框架打交道, 开发环境和生产环境下日志文件的输出路径总是不一致, 设置为绝对路径的方式缺少了灵活性, 每次变更项目路径都要修改文件, 目前想到的最佳实现方式是: 根据项目位置自动加载并配置文件路径. 本文借鉴 Tomcat 的配置方式 "${catalina.home}/logs/catalina.out", 通过相对路径的方式设置日志的输出路径, 有其他解决方案的小伙伴, 请直接评论区交流哦

  • springboot整合dubbo设置全局唯一ID进行日志追踪的示例代码

    1.新建项目 利用idea创建一个父项目,三个子项目,其中一个项目为生产者,一个项目为消费者,一个为接口等公共服务项目,生产者和消费者需要有web依赖,可以作为tomcat容器启动. 2.项目依赖 <dependencies> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <v

  • elasticsearch+logstash并使用java代码实现日志检索

    为了项目日志不被泄露,数据展示不采用Kibana 1.环境准备 1.1 创建普通用户 #创建用户 useradd querylog #设置密码 passwd queylog #授权sudo权限 查找sudoers文件位置 whereis sudoers #修改文件为可编辑 chmod -v u+w /etc/sudoers #编辑文件 vi /etc/sudoers #收回权限 chmod -v u-w /etc/sudoers #第一次使用sudo会有提示 We trust you have

  • 在Java中使用日志框架log4j的方法

    日志就是记录程序的运行轨迹,方便快速定位问题 如果用System.out.println(),信息是打印在控制台.等到产品上线后没有控制台,如果有报错信息,根本不知道去哪里看,就不知道是哪里出错. 而且开发的时候希望打印输出的内容多.方便排查,上线后只希望打印容易出错的部分.System.out.println()满足不了这个需求 而日志框架可以让错误信息输出到多个指定文件,不同的文件有不同的输出内容.方便排错,定位错误 一.log4j介绍 Log4j有三个主要的组件/对象:Loggers(记录

  • Java接口测试之日志框架Logback的具体使用

    目录 一.引言 二.前言 三.LogBack.Slf4j和Log4j之间的关系 四.默认日志Logback 五.配置详解 1.添加日志依赖 2.配置文件 六.多环境日志输出 七.单元测试 八.工程目录 九.总结 一.引言 对于一个成熟的接口测试框架,日志管理这个是必不可少的.在开发和调试阶段,日志可以帮助我们更快的定位问题:而在测试的运维过程中,日志系统又可以帮助我们记录大部分的异常信息,通 二.前言 Spring Boot 在所有内部日志中使用Commons Logging,但是默认配置也提供

  • java常见log日志的使用方法解析

    目录 前言 1. Java.util.Logger 2. org.apache.logging.log4j 3. org.slf4j.Logger 前言 log日志可以debug错误或者在关键位置输出想要的结果 java日志使用一般有原生logger.log4j.Slf4j等 一般的日志级别都有如下(不同日志不一样的方法参数,注意甄别) 参数 描述 OFF.ON 不输出或者输出所有级别信息,通常使用在setLevel方法中 FATAL 致命错误 ERROR 错误error WARN 告警信息 I

随机推荐