Java项目分布式/集群部署,接口请求日志流程跟踪
由于公司的项目使用了SpringCloud微服务,并且各个模块都是集群部署,日志分布在不同的服务器,当线上请求出现问题,日志查询和跟踪是一个很麻烦的事情,大量日志输出导致很难筛出指定请求的全部相关日志,以及下游服务调用对应的日志。
解决思路
网关服务接收到请求,然后给每个请求分配一个唯一的请求ID(traceId),再将traceId放到请求头当中,这样该请求再调用下游服务时,下游服务便可以从请求头中获取到这个traceId,微服务之间就可以衔接起来。
日志打印
String traceId = request.getHeader("traceId");//从Header中获取traceId
log.info("接收到请求,traceId:{}",traceId);//打印日志
使用上面的方式打印日志,虽然可以达到目的,但是这意味着打印每一条日志,都需要获取traceId,而同一个请求的traceId是不变的,这样操作显然不是很好的处理方式。由于我处理日志是使用的logback,通过网上查询资料,logback日志框架都提供了MDC功能。
MDC
MDC 介绍
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。
简而言之,MDC就是日志框架提供的一个InheritableThreadLocal
,项目代码中可以将键值对放入其中,然后使用指定方式取出打印即可。
在logback 的取值方式为:
%X{traceId}
实现方案
gateway网关层
新增一个拦截器,拦截request请求,生成traceId并放入request的请求头。
import com.caiyi.sport.core.constant.HeaderConstant;
import com.caiyi.sport.core.constant.LogConstant;
import com.caiyi.sport.core.domain.App;
import com.caiyi.sport.core.utils.IPUtils;
import com.caiyi.sport.core.utils.UniqueStrCreator;
import com.caiyi.sport.core.utils.UserSourceMapUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Enumeration;
@Component
@Slf4j
@WebFilter(urlPatterns = {"/"}, filterName = "headerFilter")
public class HeaderFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper(request);
//生成唯一的字符串traceId
String traceId = "traceId_" + UniqueStrCreator.getRandomStr();
//将traceId放到Header中
requestWrapper.addHeader(LogConstant.TRACE_ID, traceId);
//MDC加入相关参数,便于日志打印
MDC.put(LogConstant.TRACE_ID, traceId);
MDC.put("ip", IPUtils.getIpFromRequest(request));
MDC.put("uri", request.getRequestURI());
filterChain.doFilter(requestWrapper, servletResponse);
}
@Override
public void destroy() {
}
}
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.*;
@Slf4j
public class HeaderMapRequestWrapper extends HttpServletRequestWrapper {
/**
* construct a wrapper for this request
*
* @param request
*/
public HeaderMapRequestWrapper(HttpServletRequest request) {
super(request);
}
private Map<String, String> headerMap = new HashMap<>();
/**
* add a header with given name and value
*
* @param name
* @param value
*/
public void addHeader(String name, String value) {
headerMap.put(name, value);
}
@Override
public String getHeader(String name) {
//log.info("getHeader --->{}",name);
String headerValue = super.getHeader(name);
if (headerMap.containsKey(name)) {
headerValue = headerMap.get(name);
}
return headerValue;
}
/**
* get the Header names
*/
@Override
public Enumeration<String> getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
for (String name : headerMap.keySet()) {
names.add(name);
}
return Collections.enumeration(names);
}
@Override
public Enumeration<String> getHeaders(String name) {
List<String> values = Collections.list(super.getHeaders(name));
if (headerMap.containsKey(name)) {
values = Arrays.asList(headerMap.get(name));
}
return Collections.enumeration(values);
}
}
下游模块
新增一个拦截器或者切面,从request中取出traceId并放入MDC
//从Header中获取traceId
String traceId = request.getHeader("traceId");
MDC.put(LogConstant.TRACE_ID, traceId);
模块之间调用使用feign,需要新增一个Request拦截器
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
/**
* 拦截请求Request
*
*/
@Slf4j
@Configuration
public class FeignConfiguration implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
//有可能是获取不到attributes
return;
}
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String values = request.getHeader(name);
template.header(name, values);
}
//log.info("feign interceptor header:{}", template);
}
}
}
logback配置
<appender name="INFO_LOG"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${path}/%d{yyyy-MM-dd}/info_%i.log
</FileNamePattern>
<MaxHistory>365</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!--文件达到 最大128MB时会被切割 -->
<maxFileSize>128 MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>%date [%X{traceId}] [%thread] %-5level %logger{0} [%file:%line] [%X{userId}] - %msg%n
</pattern>
<charset>utf8</charset>
</encoder>
</appender>
参考文章:https://blog.csdn.net/yangcheng33/article/details/80796129