Spring Cloud 全链路日志追踪实现
基本实现原理:对于不携带Request-No的请求,则生成并添加请求头,添加请求头需要包装请求对象包装请求对象:经过请求头过滤器那么所有请求都会携带上Request-No请求头, 响应也会携带上Request-No对于线程池中执行的任务还是不能携带MDC和请求对象,因为RequestContextHolder也只能在当前线程与子线程中使用Request对象;其原理就是任务执行前复制好变量,结束之后
·
一、Spring Cloud 全链路日志追踪实现
基本实现原理:
- 过滤所有请求:有Request-No请求头则获取请求号,没有请求头则设置请求头
- feign远程调用添加过滤器,自动赋值请求头
- 以上两步能够完成当前线程与子线程 参数传递,对于线程池则无能为力。对于线程池则需要手动设置于释放
1. 过滤并设置请求头
对于不携带Request-No的请求,则生成并添加请求头,添加请求头需要包装请求对象
package com.aimilin.common.security.filter;
import com.aimilin.common.core.consts.CommonConstant;
import com.aimilin.common.core.context.requestno.RequestNoContext;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
/**
* 对请求生成唯一编码
*
*/
public class RequestNoFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
// 生成唯一请求号uuid
String requestNo = this.getRequestNo(request);
request = this.setRequestHeader(request, requestNo);
// 增加响应头的请求号
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.addHeader(CommonConstant.REQUEST_NO_HEADER_NAME, requestNo);
// 临时存储
RequestNoContext.set(requestNo);
MDC.put(CommonConstant.TRACE_ID, requestNo);
// 放开请求
chain.doFilter(request, response);
} finally {
// 清除临时存储的唯一编号
RequestNoContext.clear();
MDC.remove(CommonConstant.TRACE_ID);
}
}
/**
* 添加请求头请求号
* @param request 请求对象
* @param requestNo 编号
* @return 结果
*/
private ServletRequest setRequestHeader(ServletRequest request, String requestNo) {
// 没有包含请求号,则将请求号添加到请求头中
if(request instanceof HttpServletRequest && StringUtils.isBlank(((HttpServletRequest) request).getHeader(CommonConstant.REQUEST_NO_HEADER_NAME))){
HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper((HttpServletRequest) request);
requestWrapper.addHeader(CommonConstant.REQUEST_NO_HEADER_NAME, requestNo);
return requestWrapper;
}
return request;
}
/**
* 获取请求编号
* @return String
*/
private String getRequestNo(ServletRequest request){
if(request instanceof HttpServletRequest){
String requestNo = ((HttpServletRequest) request).getHeader(CommonConstant.REQUEST_NO_HEADER_NAME);
if(StringUtils.isNotBlank(requestNo)){
return requestNo;
}
}
return UUID.randomUUID().toString();
}
@Override
public void destroy() {
}
}
包装请求对象:
package com.aimilin.common.security.filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.*;
/**
* 添加请求头的请求对象
*
*/
public class HeaderMapRequestWrapper extends HttpServletRequestWrapper {
public HeaderMapRequestWrapper(HttpServletRequest request) {
super(request);
}
/**
* 请求头对象封装
*/
private Map<String, String> headerMap = new HashMap<String, String>();
/**
* 自定义添加请求头
* @param name key
* @param value value
*/
public void addHeader(String name, String value) {
headerMap.put(name, value);
}
/**
* 获取请求头,先获取原始请求头信息
* @param name 名称
* @return 结果
*/
@Override
public String getHeader(String name) {
String headerValue = super.getHeader(name);
if (headerMap.containsKey(name)) {
headerValue = headerMap.get(name);
}
return headerValue;
}
/**
* 获取所有请求头
* @return Enumeration
*/
@Override
public Enumeration<String> getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
names.addAll(headerMap.keySet());
return Collections.enumeration(names);
}
/**
* 获取指定请求头
* @param name 名称
* @return 结果
*/
@Override
public Enumeration<String> getHeaders(String name) {
List<String> values = Collections.list(super.getHeaders(name));
if (headerMap.containsKey(name)) {
values.add(headerMap.get(name));
}
return Collections.enumeration(values);
}
}
经过请求头过滤器那么所有请求都会携带上Request-No请求头, 响应也会携带上Request-No
2. Feigin远程调用,自动复制请求头
package com.aimilin.common.feign.inteceptor;
import com.aimilin.common.core.utils.JsonUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.Target;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.Objects;
import java.util.Optional;
/**
* 模块间调用转移请求头
* @classname : FeignRequestInterceptor
* @description : Feign请求拦截器
*/
@Slf4j
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
Optional.ofNullable(headerNames).ifPresent(headers -> {
while (headers.hasMoreElements()) {
String name = headers.nextElement();
String value = request.getHeader(name);
// 跳过 content-length
if (name.equals("content-length")){
continue;
}
requestTemplate.header(name, value);
log.trace(">>> feign header set {}:{}", name, value);
}
});
}
printLog(requestTemplate);
}
private void printLog(RequestTemplate requestTemplate) {
if (log.isInfoEnabled()) {
Target<?> target = requestTemplate.feignTarget();
log.info("Feign Request:\n app: {}\n class: {}\n method: {}\n url: {}\n param: {}\n",
target.name(), target.type().getName(), requestTemplate.method(),
requestTemplate.url(),
this.getParam(requestTemplate)
);
}
}
private String getParam(RequestTemplate requestTemplate) {
if (RequestMethod.GET.name().equals(requestTemplate.method())) {
return JsonUtil.toJson(requestTemplate.queries());
}
return Objects.isNull(requestTemplate.body()) ? "" : new String(requestTemplate.body());
}
}
对于线程池中执行的任务还是不能携带MDC和请求对象,因为RequestContextHolder也只能在当前线程与子线程中使用Request对象;
3. 线程池中使用MDC与Request对象
其原理就是任务执行前复制好变量,结束之后再删除变量。这里使用了alibaba TransmittableThreadLocal 线程池支持库;
import com.alibaba.ttl.TtlCallable;
import com.alibaba.ttl.TtlRunnable;
import lombok.NonNull;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import static java.util.Optional.ofNullable;
/**
* 线程池传递参数
*
*/
public class TtlThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
@NonNull
@Override
public void execute(@NonNull Runnable task) {
super.execute(wrap(task));
}
@NonNull
@Override
public Future<?> submit(@NonNull Runnable task) {
return super.submit(wrap(task));
}
@NonNull
@Override
public <T> Future<T> submit(@NonNull Callable<T> task) {
return super.submit(wrap(task));
}
@NonNull
@Override
public ListenableFuture<?> submitListenable(@NonNull Runnable task) {
return super.submitListenable(wrap(task));
}
@NonNull
@Override
public <T> ListenableFuture<T> submitListenable(@NonNull Callable<T> task) {
return super.submitListenable(wrap(task));
}
/**
* 保存MDC服务类
* @param task 任务
* @return 结果
* @param <T> 泛型
*/
private <T> Callable<T> wrap(Callable<T> task) {
Optional<RequestAttributes> requestOptional = ofNullable(RequestContextHolder.getRequestAttributes());
Optional<Map<String, String>> mdcOptional = ofNullable(MDC.getCopyOfContextMap());
return () -> {
try {
requestOptional.ifPresent(RequestContextHolder::setRequestAttributes);
mdcOptional.ifPresent(MDC::setContextMap);
return TtlCallable.get(task).call();
} finally {
MDC.clear();
RequestContextHolder.resetRequestAttributes();
}
};
}
/**
* 包装MDC Runnable
* @param task 任务
* @return 结果
*/
private Runnable wrap(Runnable task) {
Optional<RequestAttributes> requestOptional = ofNullable(RequestContextHolder.getRequestAttributes());
Optional<Map<String, String>> mdcOptional = ofNullable(MDC.getCopyOfContextMap());
return () -> {
try {
requestOptional.ifPresent(RequestContextHolder::setRequestAttributes);
mdcOptional.ifPresent(MDC::setContextMap);
TtlRunnable.get(task).run();
} finally {
MDC.clear();
RequestContextHolder.resetRequestAttributes();
}
};
}
}
异步现成初始化
/**
* 异步线程池构建
* @param builder 构建起
* @return 线程池执行器
*/
@Lazy
@Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME,
AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME })
public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
ThreadPoolTaskExecutor threadPoolTaskExecutor = builder.configure(new TtlThreadPoolTaskExecutor());
// CALLER_RUNS:调用者所在的线程来执行
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return threadPoolTaskExecutor;
}
经过上面几部,基本完成链路日志追踪,接下来是设置MDC
4. 设置日志MDC
添加配置:主要是设置控制台日志格式与文件日志格式。
<?xml version="1.0" encoding="UTF-8"?>
<!--
Default logback configuration provided for import
-->
<included>
<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(%3.3line){cyan} [%clr(%X{trace_id}){blue}] %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<property name="FILE_LOG_PATTERN" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} %3.3line [%X{trace_id}] : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
</included>
使用方式:
参考:https://docs.spring.io/spring-boot/docs/2.6.x/reference/htmlsingle/#using
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 我们自定义的日志输出格式 -->
<include resource="com/aimilin/logging/logback/my-defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
<logger name="org.springframework.web" level="DEBUG"/>
</configuration>
打印出来日志格式:
更多推荐
已为社区贡献3条内容
所有评论(0)