1、简介
????????MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。
????????当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。
2、API说明
- clear() :移除所有MDC
- get (String key) :获取当前线程MDC中指定key的值
- getContext() :获取当前线程MDC的MDC
- put(String key, Object o) :往当前线程的MDC中存入指定的键值对
- remove(String key) :删除当前线程MDC中指定的键值对
3、作用
帮助开发快速定位日志位置。
- 用户请求日志关联
- 项目间请求日志关联
- 多服务间日志聚合
- 调用关系分析
- 日志分析
4、要求与实现
4.1 要求
- 一次请求生成一次RequestId,并且RequestId唯一
- 一次请求响应给前端,都需要返回RequestId字段,接口正常、业务异常、系统异常,都需要返回该字段
- 一次请求在控制台或者日志文件打印的日志,都需要显示RequestId
- 一次请求的入参和出参都需要打印
- 对于异步操作,需要在异步线程的日志同样显示RequestId
4.2 实现
4.2.1 接口
????????前端或上一个服务节点调用当前服务节点时。
- header中获取RequestId,如果不存在,说明是前端,不是上一个服务节点,就生成一个RequestId。
- 将RequestId放到header中,前端或下个服务节点可以获取到RequestId。
- 将RequestId放入MDC中,日志中打印RequestId。
代码实现:
package com.ybwei.log.mdc.demo.interceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ybwei.log.mdc.demo.constant.MDCConstant;
import com.ybwei.log.mdc.demo.util.MyStringUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 接口日志
*
* @author ybwei
* @date 2022/2/17 11:40
**/
@Aspect
@Component
@Slf4j
public class WebLogAspect {
private final ObjectMapper mapper;
ThreadLocal<LogRecord> logRecordThreadLocal = new ThreadLocal<>();
@Autowired
public WebLogAspect(ObjectMapper mapper) {
this.mapper = mapper;
}
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void requestLog() {
}
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void postLog() {
}
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void getLog() {
}
/**
* 请求参数
*
* @param joinPoint
* @return void
* @throws
* @methodName: doBefore
* @author ybwei
* @date 2022/2/17 13:54
*/
@Before("requestLog() || postLog() || getLog()")
public void doBefore(JoinPoint joinPoint) {
//1、获取requestId
String requestId = getRequestId();
//2、往当前线程的MDC中存入指定的键值对
MDC.put(MDCConstant.REQUEST_ID, requestId);
//3、返回值设置requestId
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse httpServletResponse = sra.getResponse();
httpServletResponse.setHeader(MDCConstant.REQUEST_ID, requestId);
//4、打印参数日志
//4.1 接口地址
String name = joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName();
logRecordThreadLocal.set(new LogRecord(System.currentTimeMillis(), name));
for (Object object : joinPoint.getArgs()) {
if (object instanceof MultipartFile || object instanceof HttpServletRequest || object instanceof HttpServletResponse) {
continue;
}
try {
log.info("请求接口:{},请求参数 :{}", name, mapper.writeValueAsString(object));
} catch (Exception e) {
log.info("打印请求参数错误:", e);
}
}
}
/**
* 获取requestId
*
* @methodName: getRequestId
* @return: java.lang.String
* @author: geoffrey
* @date: 2022/5/10
**/
private String getRequestId() {
//1、从header中获取requestId
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest httpServletRequest = sra.getRequest();
String requestId = httpServletRequest.getHeader(MDCConstant.REQUEST_ID);
if (StringUtils.isNotBlank(requestId)) {
return requestId;
}
return MyStringUtils.generateUUIDNoCenterLine();
}
/**
* 返回参数
*
* @param response
* @return void
* @throws
* @methodName: doAfterReturning
* @author ybwei
* @date 2022/2/17 13:54
*/
@AfterReturning(returning = "response", pointcut = "requestLog() || postLog() || getLog()")
public void doAfterReturning(Object response) {
//1、打印日志
LogRecord logRecord = logRecordThreadLocal.get();
log.info("接口所花费时间{}毫秒,接口:{}", System.currentTimeMillis() - logRecord.getStartTime(), logRecord.getName());
logRecordThreadLocal.remove();
if (response != null) {
try {
log.info("返回参数:{}", mapper.writeValueAsString(response));
} catch (Exception e) {
log.info("打印返回参数错误:", e);
}
}
//2、删除当前线程MDC中指定的键值对
MDC.remove(MDCConstant.REQUEST_ID);
}
@AllArgsConstructor
@Getter
class LogRecord {
/**
* 开始时间
*
* @author: ybwei
* @date: 2022/3/22 9:53
*/
private Long startTime;
/**
* 接口地址
*
* @author: ybwei
* @date: 2022/3/22 9:55
*/
private String name;
}
}
4.2.2 异步和定时任务
配置线程池
package com.ybwei.log.mdc.demo.config.thread;
import com.ybwei.log.mdc.demo.constant.MDCConstant;
import com.ybwei.log.mdc.demo.util.MyStringUtils;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
/**
* MDC线程池
* 实现内容传递
*
* @author geoffrey
* @version V1.0
* @className MdcTaskExecutor
* @date 2022/5/9
**/
@Slf4j
public class MdcTaskExecutor extends ThreadPoolTaskExecutor {
@Override
public <T> Future<T> submit(Callable<T> task) {
log.info("mdc thread pool task executor submit");
Map<String, String> context = MDC.getCopyOfContextMap();
return super.submit(() -> {
T result;
if (context != null) {
//将父线程的MDC内容传给子线程
MDC.setContextMap(context);
} else {
//直接给子线程设置MDC
MDC.put(MDCConstant.REQUEST_ID, MyStringUtils.generateUUIDNoCenterLine());
}
try {
//执行任务
result = task.call();
} finally {
try {
MDC.clear();
} catch (Exception e) {
log.warn("MDC clear exception", e);
}
}
return result;
});
}
@Override
public void execute(Runnable task) {
log.info("mdc thread pool task executor execute");
Map<String, String> context = MDC.getCopyOfContextMap();
super.execute(() -> {
if (context != null) {
//将父线程的MDC内容传给子线程
MDC.setContextMap(context);
} else {
//直接给子线程设置MDC
MDC.put(MDCConstant.REQUEST_ID, MyStringUtils.generateUUIDNoCenterLine());
}
try {
//执行任务
task.run();
} finally {
try {
MDC.clear();
} catch (Exception e) {
log.warn("MDC clear exception", e);
}
}
});
}
}
异步方法
添加@Async("commonThreadPool")
@Async("commonThreadPool")
public void async() {
log.info("异步处理");
}
定时任务
@Async(value = "scheduleThreadPool")
@Scheduled(fixedRate = 2000)
public void fixedRate() {
log.info("定时间隔任务 fixedRate = {}", LocalDateTime.now());
}
4.3 调用第三方日志
实现RestTemplate拦截器
package com.ybwei.log.mdc.demo.interceptor;
import com.ybwei.log.mdc.demo.constant.MDCConstant;
import org.slf4j.MDC;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @author geoffrey
* @version V1.0
* @className RestTemplateTraceIdInterceptor
* @date 2022/5/10
**/
public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
String traceId = MDC.get(MDCConstant.REQUEST_ID);
if (traceId != null) {
request.getHeaders().add(MDCConstant.REQUEST_ID, traceId);
}
return execution.execute(request, body);
}
}
为RestTemplate添加拦截器:
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
// boot中可使用RestTemplateBuilder.build创建
RestTemplate restTemplate = new RestTemplate(new BufferingClientHttpRequestFactory(clientHttpRequestFactory));
// 配置请求工厂
// restTemplate.setRequestFactory(clientHttpRequestFactory);
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(new LoggingRequestInterceptor());
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
5、示例代码
share: 分享仓库 - Gitee.com
|