logback打印json格式日志和MDC的使用

264 阅读3分钟

上面日志规范里已经给出了logback的配置文件,这里重点说下怎么打印json格式日志

1、为什么要打印json格式日志

打印json格式的日志,是为了接入ELK,通过logstash解析日志文件,可以直接将json格式日志自动索引存入es,然后在kibana可以更方便高效的查询自定义日志字段:

如上图,我们在业务代码中打印了traceId和merCode,如果不打json格式日志,这两个字段都会在message中,es语法查询只能模糊匹配,而后续的图形化展示大盘也会依赖这里的自定义埋点字段。

2、配置json格式日志

那么如何打印json格式的日志呢?
2.1 配置依赖jar包
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>4.11</version>
</dependency>
2.2 logback.xml增加配置
<!-- 按照天生成json格式日志文件 -->
<appender name="JSON-LOG"  class="ch.qos.logback.core.rolling.RollingFileAppender">
    <File>${catalina.home}/${log.path}/${log.appName}-json.log</File>
    <!-- 打印json格式日志 -->
    <encoder class="net.logstash.logback.encoder.LogstashEncoder" >
                <!-- 解决打印json格式日志中文乱码问题 -->
        <JsonFactoryDecorator class = "com.trans.common.tracer.utils.MyJsonFactoryDecorator"/>
                <!-- 自定义埋点字段 可以不配置,不配置自动输入日志文件 -->
        <customFields>{"service_name":"${log.appName}"}</customFields>
        <includeMdcKeyName>traceId</includeMdcKeyName>
        <includeMdcKeyName>merCode</includeMdcKeyName>
        <includeMdcKeyName>orderNo</includeMdcKeyName>
        <includeMdcKeyName>timestamp</includeMdcKeyName>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <!--日志文件输出的文件名  按照小时切割  yyyy-MM-dd这样是按照天滚动保存日志-->
        <FileNamePattern>${catalina.home}/${log.path}/${log.appName}-json_%d{yyyy-MM-dd}_%i.log</FileNamePattern>
        <!--控制最多保留多少数量 如果是按日滚动,那么一天有1个日志文件,30表示保存30天的日志文件-->
        <MaxHistory>30</MaxHistory>
        <!--日志文件切割大小-->
        <maxFileSize>${log.max.size}</maxFileSize>
    </rollingPolicy>
</appender>

3、MDC的使用

3.1 MDC原理
MDC 底层就是ThreadLocal,每个线程维护自己对象,MDCAdapter提供了操作对象的方法:

使用时,在当前请求开始放入MDC,调用完释放就可以了。

3.2 摘要日志MDC的使用
3.2.1 公共字段埋点
可以自定义系统过滤器或拦截器,拦截所有请求,在请求前放入链路id,可以参考下面的过滤器TraceFilter
/**
 * @author : admin
 * @version : v1.0
 * @description : 增加日志链路追踪tracId
 * @date: 2021/12/22 16:33
 */
@Component
public class TraceFilter extends OncePerRequestFilter {


    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        //put traceId
        try {
            MDC.put(TraceConstant.TRACE_ID, this.getTraceId(httpServletRequest));
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } finally {
            MDC.remove(TraceConstant.TRACE_ID);
        }
    }

    /**
     * 获取链路traceId
     * @param httpServletRequest
     * @return
     */
    private String getTraceId(HttpServletRequest httpServletRequest) {
        String traceId0 = httpServletRequest.getHeader(TraceConstant.TRACE_ID);
        // 获取请求对象是否包含参数orderId
        String orderId = getParamFromHttpRequest(httpServletRequest);
        //生成 traceId
        String traceId = TraceIdGenerator.generate();
        return StringUtils.isBlank(traceId0) ? (StringUtils.isBlank(orderId) ? traceId : orderId) : traceId0;
    }

    /**
     * 从httpServletRequest获取目标参数
     * @param httpServletRequest
     * @return
     */
    private String getParamFromHttpRequest(HttpServletRequest httpServletRequest) {
        // 判断是否携带orderId参数
        Map<String, String> param = ParameterUtils.getRequestParameters(httpServletRequest);
        String orderId = param.get(TraceConstant.ORDER_ID);
        if (StringUtils.isNotBlank(orderId)) {
            return orderId;
        }
        String requestId = param.get(TraceConstant.REQUEST_ID);
        if (StringUtils.isNotBlank(requestId)) {
            return requestId;
        }
        TbPyTranOrderInfo tranOrderInfo = JSONArray.parseObject(param.get(TraceConstant.TRAN_ORDER_INFO), TbPyTranOrderInfo.class);
        orderId = ObjectUtils.isEmpty(tranOrderInfo) ? "" : tranOrderInfo.getMerOrderNo();
        return orderId;
    }
}
3.2.2 自定义日志埋点代码示例
    @PostMapping(value = "/upload")
    public ServerResponse onMessage(OrderMessage message) {
        try {
            if (message == null) {
                log.info("参数[message]为空.");
                return ServerResponse.errorResponse(Response.ILLEGAL_PARAM_ERROR);
            }
            MDC.put("orderNo", message.getOrderNo());
            MDC.put("merCode", message.getMerCode());
            MDC.put("timestamp", String.valueOf(message.getTimestamp()));
            log.info("收到消息: [{}]", message.getValue());
            long start = System.currentTimeMillis();
            // 校验sign
            Response check = message.check();
        } catch (Exception e) {
            log.error("请求处理异常message: {}, e: {}", message, e);
            return ServerResponse.errorResponse(Response.ERROR);
        } finally {
            MDC.remove("orderNo");
            MDC.remove("merCode");
            MDC.remove("timestamp");
        }
    }
重点问题,父线程如何给子线程传递链路信息MDC?

展开源码

package com.trans.common.threadpool;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.slf4j.MDC;
import org.springframework.util.CollectionUtils;

import java.util.Map;
import java.util.concurrent.*;

/**
 * @author : admin
 * @version : v1.0
 * @description : 自定义线程池
 * @date: 2022/01/04 11:25
 */
public class XlPayThreadPoolExecutor {

    /**
     * 自定义线程名称,方便的出错的时候溯源
     */
    private static final ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("xlPayThreadPoolExecutor-%d").build();

    /**
     * corePoolSize    线程池核心池的大小
     * maximumPoolSize 线程池中允许的最大线程数量
     * keepAliveTime   当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间
     * unit            keepAliveTime 的时间单位
     * workQueue       用来储存等待执行任务的队列
     * threadFactory   创建线程的工厂类
     * handler         拒绝策略类,当线程池数量达到上线并且workQueue队列长度达到上限时就需要对到来的任务做拒绝处理
     */
    private static final ExecutorService executorService = new ThreadPoolExecutor(
            5,
            20,
            60L,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(200),
            namedThreadFactory,
            new ThreadPoolExecutor.CallerRunsPolicy()
    );

    /**
     * 获取线程池
     *
     * @return 线程池
     */
    public static ExecutorService getExecutor() {
        return executorService;
    }

    public static Future<?> submit(Runnable runnable, Map<String, String> context) {
        return executorService.submit(wrap(runnable, context));
    }

    public static Future<?> submit(Callable callable, Map<String, String> context) {
        return executorService.submit(wrap(callable, context));
    }

    /**
     * 使用线程池创建线程并异步执行任务
     * @param r
     * @param context
     */
    public static void execute(Runnable r, Map<String, String> context) {
        executorService.execute(wrap(r, context));
    }

    /**
     * 将父线程对象拷贝到子线程
     * @param runnable
     * @param context
     * @return
     */
    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return new Runnable() {
            @Override
            public void run() {
                Map previous = MDC.getCopyOfContextMap();
                if (context == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(context);
                }
                try {
                    runnable.run();
                } finally {
                    if (previous == null) {
                        MDC.clear();
                    } else {
                        MDC.setContextMap(previous);
                    }
                }
            }
        };
    }

    public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
        return new Callable<T>() {
            @Override
            public T call() throws Exception {
                Map preContext = MDC.getCopyOfContextMap();
                if (CollectionUtils.isEmpty(context)) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(context);
                }
                try {
                    return callable.call();
                } finally {
                    if (CollectionUtils.isEmpty(preContext)) {
                        MDC.clear();
                    }else {
                        MDC.setContextMap(preContext);
                    }
                }
            }
        };
    }
}
里面提供了异步执行方法 execute使用时只需将当前线程MDC对象MDC.getCopyOfContextMap()传进去就行,子线程就能获取到当前线程的MDC对象
XlPayThreadPoolExecutor.execute(new ReceiveMoneyTask(params, merChant), MDC.getCopyOfContextMap());