上面日志规范里已经给出了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());