详解Java中日志跟踪的简单实现
作者:noknow 发布时间:2023-03-28 00:18:48
一、前言
在编码过程中,常常需要写打印日志语句,我们期望的是同一个业务的日志都在一块,在出问题的时候好根据日志来排查问题。而现实是在应用运行中,日志的输出常常来自不同线程,甚至是在不同微服务中,各种日志记录往往彼此穿插,很难串起来。所以往往在日志中手动增加一些关键字,来对接口的调用链路来进行跟踪。但这种手动增加关键字或唯一标识的做法在微服务场景下,很难在上下游应用的开发人员的编码风格形成统一的规范,并且手动编写也很难称得上优雅。
二、MDC介绍
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,MDC 中包含的内容可以被同一线程中执行的代码所访问。
MDC中的键值对是可以直接被日志框架所使用(即“打印”)的,只需要配置相应日志pattern。例如pattern如下:
%d{HH:mm:ss.SSS} [%thread] [%X{TraceId}] %-5level %logger{50} - %msg%n
代码如下:
public class MDCTest {
private static final Logger log = LoggerFactory.getLogger(MDCTest.class);
@Test
void test() {
MDC.put("TraceId", "123456789");
log.info("hello {}", "world");
}
}
此时控制台将输出:
21:16:04.342 [main] [123456789] INFO com.nk.MDCTest - hello world
三、实现方案
1、基本思路
修改日志pattern,并在业务开始的时候将trace id放入到MDC,在业务结束时去除MDC的trace id。这样的好处便是代码简洁,不需要手动写trace id,日志风格也能保持统一。
业务开始的时机一般是应用收到HTTP请求,所以可以用Filter或SpringMVC的Interceptor来对MDC中trace id进行初始化和清除。在Dubbo调用的时候也可以通过类似功能的Filter来对MDC中trace id进行操作,从而达到trace id传递的作用。
2、实现(以SpringBoot为例)
2.1 修改log pattern
在SpringBoot中,直接修改application.properties即可:
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{TraceId}] %-5level %logger{50} - %msg%n
重点在于%X{TraceId},其中TraceId需要作为key出现在MDC里。
2.2 业务开始
TraceId工具类,封装MDC关于trace id的基础操作:
public final class TraceIdUtil {
private static final String TRACE_ID_KEY = "TraceId";
private TraceIdUtil() {
}
public static void putIfAbsent() {
if (StrUtil.isBlank(get())) {
put(UUID.randomUUID().toString());
}
}
public static void remove() {
if (get() != null) {
MDC.remove(TRACE_ID_KEY);
}
}
public static String get() {
return MDC.get(TRACE_ID_KEY);
}
public static void put(String traceId) {
MDC.put(TRACE_ID_KEY, traceId);
}
}
Filter方式和Interceptor二选其一既可,其基本思想是一样的。
Filter方式
@Component
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
TraceIdUtil.putIfAbsent();//生成trace id放入MDC中
try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
TraceIdUtil.remove();//移除MDC中的trace id
}
}
}
Interceptor
@Configuration
public class LogInterceptor implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AsyncHandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
TraceIdUtil.putIfAbsent();//生成trace id放入MDC中
return AsyncHandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) throws Exception {
TraceIdUtil.remove();//移除MDC中的trace id
AsyncHandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
});
WebMvcConfigurer.super.addInterceptors(registry);
}
}
2.3 业务中使用
正常使用logger,无需关心trace id。例如:
@RestController
@RequestMapping("/api/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{userId}")
public UserDto queryUser(@PathVariable Long userId) {
log.info("query user by id:{}", userId);
UserDto user = userService.query(userId);
log.info("query user result:{}", user);
return user;
}
}
请求该接口将输出如下的日志样式:
2022-04-05 09:40:17.638 [http-nio-8080-exec-1] [a02b13d81c224e49956afd4efbb85ca8] INFO com.nk.webapp.controller.UserController - ready to query user by id:1
2022-04-05 09:40:17.670 [http-nio-8080-exec-1] [a02b13d81c224e49956afd4efbb85ca8] INFO com.nk.webapp.controller.UserController - query result:UserDto(userId=1, username=zhang3, age=23, email=abc@example.com)
四、总结
日志链路的跟踪核心是使用MDC作为trace id载体,在业务开始阶段一般通过 * 就生成trace id并放入到MDC中,并根据MDC的相关特性将trace id投射到日志文本中,从而实现在同一个业务调用链路中的日志具有唯一标识。
来源:https://www.51cto.com/article/717419.html
猜你喜欢
- 一、回传协议接口和TCP方式实现:1.接口:import java.nio.channels.SelectionKey; import ja
- 如何加载权限表达式我们在上章内容中画了一张图,里面有三个分项,用户 角色 权限;那么接下来我们就要思考一个问题了,这三张表中的数据要从何而来
- C#写入xml文件1、XmlDocument1。我认为是最原始,最基本的一种:利用XmlDocument向一个XML文件里写节点,然后再利用
- 本文实例为大家分享了java金额数字转中文工具类的具体代码,供大家参考,具体内容如下java金额数字转中文工具类ConvertNum.jav
- <%@ page language="java" contentType="text/html; cha
- 什么是委托?之前写了事件的介绍:https://www.jb51.net/article/59461.htm这里也把委托相关知识也总结一下。
- 一、输入输出流对象cout:标准输出流cerr:标准出凑 和cout(只是用于如果是错误时要输出的)cin :&nb
- 知识准备Timer和ScheduledExecutorService是JDK内置的定时任务方案,而业内还有一个经典的定时任务的设计叫时间轮(
- 背景众所周知,所有被打开的系统资源,比如流、文件或者Socket连接等,都需要被开发者手动关闭,否则随着程序的不断运行,资源泄露将会累积成重
- 基本流foreach 实现了外部迭代,它是建立于集合的基础上;stream 流实现的是内部迭代中间操作与终端操作对任意集合使用 stream
- 一、任务和执行策略之间的隐性耦合Executor可以将任务的提交和任务的执行策略解耦只有任务是同类型的且执行时间差别不大,才能发挥最大性能,
- 1.使用usb口输入的扫描枪,这里实现使用了winform首先创建一个CS文件using System;using System.Colle
- 一:在springboot中使用事物遇到的坑1.我们知道spring中的事物分为两种:一种是编程式事物,一种是声明式事物。顾名思义,编程式事
- 前情提要:本demo是基于springboot+mybatis-plus实现加密,加密为主,全局异常处理,日志处理为辅,而登录密码加密是每个
- java8的stream流能完美解对象集合去重问题. List<UserCar> list1 = new ArrayList()
- 环境:apache-tomcat-8.5.15jdk1.8.0_172IDEA建立一个maven-webapp项目:Create New P
- 简介线段树是一种二叉搜索树,是用来维护区间信息的数据结构。可以在O(logN)的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区
- 示例接口定义为了方便笔者先自己写了个简单的接口用于展示,返回当前时间,具体如下/// <summary>/// GetToday
- Dubbo服务暴露机制前言在进行服务暴露机制的分析之前,必须谈谈什么是URL,在Dubbo服务暴露过程中URL是无处不在的,贯穿了整个过程。
- 在开发中,遇到了sql语句报错,但是并没有回滚的情况。经过几天的排查,终于找到了事务没有回滚的原因。原来的项目用的是informix的数据库