什么是logback

logback 继承自 log4j,它建立在有十年工业经验的日志系统之上。它比其它所有的日志系统更快并且更小,包含了许多独特并且有用的特性。

什么是MDC

(1)概念

logback 设计的目标之一是审计与调试复杂的分布式应用。大部分的分布式系统需要同时处理多个客户端。在一个系统典型的多线程实现中,不同的线程处理不同的客户端。一种可能但是不建议的方式是在每个客户端实例化一个新的且独立的 logger,来区分一个客户端与另一个客户端的日志输出。这种方式会导致 logger 急剧增加并且会增加维护成本。

一种轻量级的技术是给每个为客户端服务的 logger 打一个标记。Neil Harrison 在 Patterns for Logging Diagnostic Messages in Pattern Languages of Program Design 3, edited by R. Martin, D. Riehle, and F. Buschmann (Addison-Wesley, 1997) 这本书中描述了这种方法。logback 在 SLF4J API 利用了这种技术的变体:诊断上下文映射 (MDC)。

为了给每个请求打上唯一的标记,用户需要将上下文信息放到 MDC (Mapped Diagnostic Context 的缩写) 中。下面列出了 MDC 类中主要的部分。完成的方法列表请查看 MDC javadocs

package org.slf4j;

public class MDC {
  // 将上下文的值作为 MDC 的 key 放到线程上下的 map 中
  public static void put(String key, String val);

  // 通过 key 获取上下文标识
  public static String get(String key);

  // 通过 key 移除上下文标识
  public static void remove(String key);

  // 清除 MDC 中所有的 entry
  public static void clear();
}

MDC 在客户端服务器架构中最为重要。通常,服务器上的多个线程为多个客户端提供服务。尽管 MDC 中的方法是静态的,但是是以每个线程为基础来进行管理的,允许每个服务线程都打上一个 MDC 标记。MDC中的 put() 与 get() 操作仅仅只影响当前线程中的 MDC。其它线程中 MDC 不会受到影响。给定的 MDC 信息在每个线程的基础上进行管理。每个线程都有一份 MDC 的拷贝。因此,在对 MDC 进行编程时,开发人员没有必要去考虑线程安全或者同步问题。它自己会安全的处理这些问题。

(2)线程安全原因

线程安全的原因,我继续看MDC进行put的实现时,用的时ThreadLocal,上篇文章我们讲过ThreadLocal,大家可以翻开看一下。源码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package ch.qos.logback.classic.util;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.slf4j.spi.MDCAdapter;

public class LogbackMDCAdapter implements MDCAdapter {
    final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal();
    private static final int WRITE_OPERATION = 1;
    private static final int MAP_COPY_OPERATION = 2;
    final ThreadLocal<Integer> lastOperation = new ThreadLocal();

    public LogbackMDCAdapter() {
    }

    private Integer getAndSetLastOperation(int op) {
        Integer lastOp = (Integer)this.lastOperation.get();
        this.lastOperation.set(op);
        return lastOp;
    }

    private boolean wasLastOpReadOrNull(Integer lastOp) {
        return lastOp == null || lastOp.intValue() == 2;
    }

    private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
        Map<String, String> newMap = Collections.synchronizedMap(new HashMap());
        if (oldMap != null) {
            synchronized(oldMap) {
                newMap.putAll(oldMap);
            }
        }

        this.copyOnThreadLocal.set(newMap);
        return newMap;
    }

    public void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        } else {
            Map<String, String> oldMap = (Map)this.copyOnThreadLocal.get();
            Integer lastOp = this.getAndSetLastOperation(1);
            if (!this.wasLastOpReadOrNull(lastOp) && oldMap != null) {
                oldMap.put(key, val);
            } else {
                Map<String, String> newMap = this.duplicateAndInsertNewMap(oldMap);
                newMap.put(key, val);
            }

        }
    }

    public void remove(String key) {
        if (key != null) {
            Map<String, String> oldMap = (Map)this.copyOnThreadLocal.get();
            if (oldMap != null) {
                Integer lastOp = this.getAndSetLastOperation(1);
                if (this.wasLastOpReadOrNull(lastOp)) {
                    Map<String, String> newMap = this.duplicateAndInsertNewMap(oldMap);
                    newMap.remove(key);
                } else {
                    oldMap.remove(key);
                }

            }
        }
    }

    public void clear() {
        this.lastOperation.set(Integer.valueOf(1));
        this.copyOnThreadLocal.remove();
    }

    public String get(String key) {
        Map<String, String> map = (Map)this.copyOnThreadLocal.get();
        return map != null && key != null ? (String)map.get(key) : null;
    }

    public Map<String, String> getPropertyMap() {
        this.lastOperation.set(Integer.valueOf(2));
        return (Map)this.copyOnThreadLocal.get();
    }

    public Set<String> getKeys() {
        Map<String, String> map = this.getPropertyMap();
        return map != null ? map.keySet() : null;
    }

    public Map<String, String> getCopyOfContextMap() {
        Map<String, String> hashMap = (Map)this.copyOnThreadLocal.get();
        return hashMap == null ? null : new HashMap(hashMap);
    }

    public void setContextMap(Map<String, String> contextMap) {
        this.lastOperation.set(Integer.valueOf(1));
        Map<String, String> newMap = Collections.synchronizedMap(new HashMap());
        newMap.putAll(contextMap);
        this.copyOnThreadLocal.set(newMap);
    }
}

 

MDC使用

(1)使用场景

 在项目开发过程中,我们需要将一个请求打印出一串唯一标识(TraceId ),方便日志追踪(TraceId也是链路追踪的基础),方便我们日志查询。我们就需要将生成的TraceId放到MDC里,然后打印出来。这样我们通过TraceId就可以过滤出从请求进入系统到响应全过程的日志,处理线上问题非常方便。

(2)使用

  • 配置拦截器
package com.vipcode.config.aspect;

import com.vipcode.common.utils.RandomUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;

@Aspect
@Component
public class ReqIdLogAspect {
    private static final String REQID_KEY = "reqId";

    @Pointcut("execution(public * com.aldeo.modules.*.mq..*.*(..))|| execution(public * com.aldeo.modules.*.controller..*.*(..))")
    public void controllerCall() {
    }

    @Before("controllerCall()")
    public void logInfoBefore(JoinPoint jp) throws UnsupportedEncodingException {
        MDC.put(REQID_KEY, RandomUtil.MixString(10));
    }

    @AfterReturning(returning = "req", pointcut = "controllerCall()")
    public void logInfoAfter(JoinPoint jp, Object req) throws Exception {
        MDC.remove(REQID_KEY);
    }
}

AOP或Filter拦截需要加入TraceId的入口,这里以AOP为例,下面拦截的是controller和mq的日志。

TraceId生成规则可以参考:https://tech.antfin.com/docs/2/46947

  • 日志输出配置

logbak-spring.xml文件输出格式中加上:%X{reqId} 即可

  • 输出结果