参与的公司项目中对每个请求都加上了request-id写入响应头,同时在springboot项目中使用logback打印到日志,非常方便追踪问题,项目中没有使用到分布式微服务,但是好处已经非常明显。在排查问题的时候可以根据浏览器控制台查看收到的request-id请求头去搜索服务器日志文件,快速定位问题。
一、为什么要使用request-id?
使用request-id是为了解决下面的问题
问题一: 客户端访问的Web服务时,如何将客户端请求与服务端日志关联
问题二: 微服务架构下,访问日志如何查询
问题三: 不同项目交互出现异常,如何做日志关联
没有requsest-id的项目,如果项目是分布式微服务来实现的,代码层层封装后,无法通过日志关键与用户请求关联。
微服务架构下,用户请求逻辑层分解多个子任务给下层服务处理,下层服务无法与用户请求关联。
不同项目交互,如何在并发,错误重试,参数相同的情况下,无法通过关键字,时间来确定日志
使用request-id的好处
- 当前项目,根据request id 可以找到所有与请求相关的日志
- 不同项目,可以根据request id 确定唯一的请求
- 用户请求与日志关联,项目间请求日志也可以关联
使用request-id
- 要有配套日志记录系统
- 周边系统支持,保持统一
- request id 每次用户请求,必须保证唯一
- 多服务间日志聚合、调用关系分析
- 日志分析
二、request-id实现思路
使用springboot开发分布式应用,很多都微服务化,当请求过来,可能需要调用多个服务来完成请求动作。在查询日志时,特别是请求量大的情况下,日志多,很难找到对应请求的日志,造成定位异常难,日志难以追踪等问题。针对此类问题,logback 提供了 MDC ( Mapped Diagnostic Contexts 诊断上下文映射 ),MDC可以让开发人员可以在 诊断上下文 中放置信息,这些消息是内部使用了 ThreadLocal实现了线程与线程之间的数据隔离,管理每个线程的上下文信息 。而在日志输出时,可以通过标识符%X{key}
来输出MDC中的设置的内容。因此,在分布式应用在追踪请求时,实现思路如下:
- web应用中,添加拦截器,在请求进入时,添加唯一id作为request-id,以标识此次请求。
- 添加此 request-id 到MDC中
- 若需要调用其它服务,把此request-id作为 header 参数
- 在日志输出时,添加此request-id的输出作为标识
- 请求结束后,清除此request-id
三、代码实现
1.添加拦截器
通过拦截器,实现在请求前添加request-id放到 MDC 中,请求完成后清除的动作。类定义如下:
@Component
public class RequestIdFilter extends OncePerRequestFilter {
private static final String KEY_REQUEST_ID = "X-Request-Id";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
try {
String requestId = request.getHeader(KEY_REQUEST_ID);
if (StringUtils.isBlank(requestId)) {
requestId = UUID.randomUUID().toString();
}
//将生成的request-id放入MDC中,允许打印日志时获取
MDC.put("RequestId", requestId);
//将request-id写入响应头
response.addHeader(KEY_REQUEST_ID, requestId);
//传入其他过滤器
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
附一个过滤器:CommonsRequestLoggingFilter
继承这个类,可以针对请求打印日志
2.添加RequestLoggingFilter
@Slf4j
@Component
public class RequestLoggingFilter extends CommonsRequestLoggingFilter {
private static final String KEY_REQUEST_START_TIME = "request_logging_request_start_time";
private boolean ignoreStatic;
private AntPathMatcher pathMatcher;
private Collection<String> ignorePathPatterns;
private Collection<String> ignorePaths;
private final ResourceUrlProvider resourceUrlProvider;
@Autowired
public RequestLoggingFilter(ResourceUrlProvider resourceUrlProvider) {
this.resourceUrlProvider = resourceUrlProvider;
this.pathMatcher = new AntPathMatcher();
this.ignorePathPatterns = new LinkedHashSet<>();
this.ignorePaths = new LinkedHashSet<>();
}
@Override
protected void beforeRequest(HttpServletRequest request, String message) {
if (!shouldSkip(request)) {
//对不该跳过的请求添加属性,记录请求开始的时刻
request.setAttribute(KEY_REQUEST_START_TIME, Instant.now());
//调用父类CommonsRequestLoggingFilter的方法,点进去看实现是打印message(请求路径)
super.beforeRequest(request, message);
}
}
@Override
protected void afterRequest(HttpServletRequest request, String message) {
if (!shouldSkip(request)) {
Instant startTime = (Instant) (request.getAttribute(KEY_REQUEST_START_TIME));
long elapse = Duration.between(startTime, Instant.now()).toMillis();
//打印请求耗时
message = String.format("%s, roundtrip elapse %d ms.", message, elapse);
super.afterRequest(request, message);
}
}
public void setIgnoreStatic(boolean ignoreStatic) {
this.ignoreStatic = ignoreStatic;
}
private boolean shouldSkip(HttpServletRequest request) {
if (!ignoreStatic) {
return false;
}
String path = request.getRequestURI();
boolean isStatic = resourceUrlProvider.getForLookupPath(path) != null;
if (isStatic) {
return true;
}
if (ignorePaths.contains(path)) {
return true;
}
for (String pattern : ignorePathPatterns) {
if (pathMatcher.match(pattern, path)) {
ignorePaths.add(path);
return true;
}
}
return false;
}
public void addIgnorePathPattern(String... patterns) {
for (String p : patterns) {
if (!pathMatcher.isPattern(p)) {
throw new IllegalArgumentException(String.format("invalid pattern: %s", p));
}
}
Collections.addAll(this.ignorePathPatterns, patterns);
}
public void addIgnorePath(String... paths) {
Collections.addAll(this.ignorePaths, paths);
}
}
3.针对以上俩过滤器的配置类
@Configuration
public class RequestLoggingFilterConfig {
private final RequestLoggingFilter requestLoggingFilter;
private final RequestIdFilter requestIdFilter;
@Autowired
public RequestLoggingFilterConfig(RequestLoggingFilter requestLoggingFilter, RequestIdFilter requestIdFilter) {
this.requestLoggingFilter = requestLoggingFilter;
this.requestIdFilter = requestIdFilter;
}
@Bean
public FilterRegistrationBean registerRequestIdFilter() {
FilterRegistrationBean<RequestIdFilter> bean = new FilterRegistrationBean<>(requestIdFilter);
bean.setFilter(requestIdFilter);
//设置过滤器类的执行顺序,这里设为最高,确保request-id的过滤器第一个执行
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
@Bean
public FilterRegistrationBean registerRequestLoggingFilter() {
requestLoggingFilter.setIncludeQueryString(true);
requestLoggingFilter.setIncludePayload(true);
requestLoggingFilter.setMaxPayloadLength(10000);
requestLoggingFilter.setIncludeHeaders(false);
requestLoggingFilter.setIgnoreStatic(true);
requestLoggingFilter.addIgnorePath("/", "/v2/api-docs", "/csrf");
requestLoggingFilter.addIgnorePathPattern("/swagger-resources/*");
FilterRegistrationBean<RequestLoggingFilter> bean = new FilterRegistrationBean<>(requestLoggingFilter);
bean.setFilter(requestLoggingFilter);
bean.setOrder(1);
return bean;
}
}
springboot中默认使用logback作为日志实现,最后在application-xxx.yml文件中简单配置即可:
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: DEBUG
org.springframework.jdbc.core.JdbcTemplate: DEBUG
org.springframework.jdbc.core.StatementCreatorUtils: TRACE
org.springframework.web.filter.CommonsRequestLoggingFilter: DEBUG
-- 上面两个过滤器所在包路径
cn.xxx.xxx.core.common.filter.*: DEBUG
pattern:
-- 获取MDC中的属性并打印
level: "%X{RequestId} %5p"
网友评论