分布式系统「全链路日志追踪」实战之 RestTemplate & Feign

(图片来源于 Google Dapper 的一篇论文,这是链路追踪理论基础的鼻祖)这张图看上去感觉很高大上的样子 ,但精髓在于日志追踪架构设计思维。即设计思维很重要!设计思维很重要!设计思维很重要!设计思维很重要![重要的话说四遍 ] —— 思路和方案设计指导可落地的开发实现

1. 摘 要:

由于早期微服务项目赶进度,对日志这块没有进行详细的设计和规范,每个微服务都是自己管自己的服务内部日志信息。这也对后面的线上问题排查定位带来了很大的困难,特别是微服务之间的相互调用,问题定位特别的困难。现在我们想实现从请求开始,到请求结束的全链路日志追踪。需求很简单,实现思路也不难,只需要在请求过程中添加一个全局唯一的 traceId 即可。

本文通过构建三个 Spring Boot 轻量级微服务系统,一个网关服务和两个下游接口服务,Step By Step 模拟实现分布式系统跨服务调用全链路日志追踪。

2. 全链路日志追踪架构与服务搭建

2.1 日志链路追踪架构图解

前后端分离模式下,前端直接访问对应的接口服务,微服务架构中很少见这种,第一种架构(简化)图示如下所示:

前后端分离模式下,前端直接访问网关(接口服务统一入口),由网关路由到具体的下游服务接口,这种微服务架构较常见。第二种架构(简化)图示如下所示:

2.2 微服务划分与搭建

可直接参考这篇文章【小白都能看得懂的服务调用链路追踪设计与实现(未实现跨服务链路追踪)】,复制成 3 份 log-track 工程,分别将各个工程命名为logtrack-1(Service-A)、logtrack-2(Service-B)、log-zuul(网关),工程结构如下图所示:

3. 分布式服务全链路日志追踪实践

3.1 服务的注册与发现

3.1.1 环境准备:

  • MacOS + IDEA 2019.3 + SpringBoot 2.1.1 + SpringCloud Greenwich.SR1 + Zookeeper 3.4.13

其中Spring Cloud 和 Spring Boot 版本的对应关系见下图所示(Spring 官网 https://spring.io/projects/spring-cloud#overview):

3.1.2 选择 Zookeeper 作为分布式服务注册中心:

  • 关于 MacOS 软件安装 Zookeeper 小结(Windows 略过,也可自行百度):
Mac 使用 brew 包管理器安装软件
// 安装 Homebrew 在终端输入:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)”
// 卸载 Homebrew 在终端输入:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/uninstall)"
brew 常用命令:
brew install    // 安装
brew uninstall  // 卸载
brew list       // 列出已安装的软件
brew update     // 更新brew
brew home       // 用浏览器打开brew的官方网站
brew info       // 显示软件信息
brew deps       // 显示包依赖
brew --version  // 显示版本号
安装 Zookeeper 注册中心
// 查看zookeeper可用版本信息:
brew info zookeeper
// 利用brew 安装命令:
brew install zookeeper
// 利用brew 卸载命令:
brew uninstall zookeeper
// 安装成功后查看 zookeeper 信息:
brew info zookeeper
// 进入安装目录:
cd /usr/local/etc/zookeeper/
// 启动命令:
zkServer start
// 查看服务状态:
zkServer status
// 停止服务:
zkServer stop
// zookeeper 后台客户端:
zkCli
// 退出客户端:
quit

查看本机安装的 Zookeeper(以下简称 zk)版本号命令如下:

echo stat|nc localhost 2181
  • 本地 zk 版本号是 3.4.13,如下图所示:

  • 本地 zk 服务启动命令如下所示:
# 启动 zk
zkServer start
# 停止 zk
zkServer stop

下图表示本地 zk 服务启动成功:

  • zk 管理系统之 zkui 安装与使用

通过网址下载zkui(https://github.com/DeemOpen/zkui)

下载后解压 zkui-master.zip,如下图所示:

解压后进入zkui-master 文件夹,文件夹中内容展示如下所示:

查看 config.cfg 配置文件内容说明如下:

启动 zkui 服务,即启动 zkui.sh 文件,启动、停止命令如下:

# 启动
sh zkui.sh start
# 停止
sh zkui.sh stop

或者

# 解压从github下载的zkui-master.zip
unzip zkui-master.zip
# 进入zkui-master目录
cd zkui-master
# 编译注册
mvn clean install
# 复制config.cfg文件到target目录下
cp config.cfg target/
# 进入target目录
cd target/
# 后台启动zkui命令
nohup java -jar zkui-2.0-SNAPSHOT-jar-with-dependencies.jar &

在浏览器中输入网址:http://localhost:9090/,访问 zkui 管理界面,如下所示:

登录的用户名和密码默认是:admin、manager

登录 zkui home 首页之后,可以看到如下所示界面,zk 根节点还没有其他节点信息:

如果启动我们的 logtrack-1 和 logtrack-2 服务,可以观察到 zkui-home页面出现了新的 logservices 服务节点,该节点下注册了两个服务,服务的名称正好对应正在启动两个应用服务名。

点击其中一个服务名,进去可以看到该服务在zk注册的详细信息,如下图所示:

修改 logtrack-1 工程的 pom.xml 文件,新增 zk 注册中心服务发现依赖,如下:

<!-- zk 服务注册与发现 -->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
      <exclusions>
        <exclusion>
          <groupId>org.apache.zookeeper</groupId>
          <artifactId>zookeeper</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.apache.zookeeper</groupId>
      <artifactId>zookeeper</artifactId>
      <!-- 与本地或服务器安装的 zk 版本相同 -->
      <version>3.4.13</version>
      <exclusions>
        <exclusion>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

与 Spring Cloud 整合,在 pom.xml 文件新增依赖如下所示:

<!-- SpringCloud 所有依赖管理的坐标 -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Greenwich.SR1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

完整的 pom.xml 文件截图如下:

修改服务A(logtrack-1)的 application.properties 为 application.yml 文件,文件内容如下所示:

spring:
  application:
    # 应用服务名
    name: logtrack-1
  cloud:
    zookeeper:
      # zk服务地址
      connect-string: localhost:2181
      discovery:
        enabled: true
        register: true
        # 以ip形式注册到zk服务上
        instance-host: ${spring.cloud.client.ip-address}
        # 在zk根节点下创建服务注册路径
        root: /logservices
server:
  # 应用端口号
  port: 9001

在 logtrack-1 服务的启动类上加上注解 @EnableDiscoveryClient,然后启动该服务,就能将该服务注册到 zk 上,登录 zkui 管理系统即可查看相应服务注册情况,如下图所示:

同上,修改 logtrack-2 工程(参考这篇文章搭建工程即可:小白都能看得懂的服务调用链路追踪设计与实现),与 logtrack-1 工程的 pom.xml 文件相同,application.yml 文件内容修改如下:

spring:
  application:
    # 应用服务名
    name: logtrack-2
  cloud:
    zookeeper:
      # zk服务地址
      connect-string: localhost:2181
      discovery:
        enabled: true
        register: true
        # 以ip形式注册到zk服务上
        instance-host: ${spring.cloud.client.ip-address}
        # 在zk根节点下创建服务注册路径
        root: /logservices
server:
  # 应用端口号
  port: 9002

3.2 日志追踪案例一

3.2.1 以不经过网关直接请求 logtrack-1 和 logtrack-2 服务为例

日志链路图解如下所示:

① RestTemplate 客户端实现日志链路追踪

1)RestTemplate 常被用作 REST API 接口请求的客户端,而实际发起每次请求之前都把请求头 Header 相关信息添加到 HttpEntity / RequestEntity 中,这样实现的编码会显得十分冗余。

正好,Spring 为我们提供了 ClientHttpRequestInterceptor 接口,可以对请求进行拦截,并在其被发送至被调用方接口服务之前修改请求或是增强相应的信息。比如在 Header 中增加自定义字段 trace-id 等。

2)创建自定义的 RestTrackInterceptor.class 拦截器类 实现 ClientHttpRequestInterceptor 接口,然后将自定义拦截器类加到 RestTemplate 客户端中。

  • RestTrackInterceptor.class 的编码实现如下所示:
package com.smart4j.core.logtrack.interceptor;
import org.springframework.http.HttpHeaders;
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;
import java.util.Optional;
import java.util.UUID;
/**
 * @Description:  自定义请求拦截器
 * @Param:
 * @return:
 * @Author: Mr.Zhang
 * @Date: 2020/4/15
 */
@Component
public class RestTrackInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        HttpHeaders headers = request.getHeaders();
        // 从线程局部共享变量 TRACE_ID_THREAD_LOCAL 中获取traceId,如无则生成UUID替换
        String traceId = Optional.ofNullable(LogTrackInterceptor.getTraceId()).orElse(UUID.randomUUID().toString().replaceAll("-",""));
        // 请求头传递参数:trace-id
        headers.add("trace-id", traceId);
        // 保证请求继续被执行
        return execution.execute(request, body);
    }
}

该类对应的包位置如下图所示:

3)创建自己的 RestTemplate 客户端 Bean,然后将前面创建的自定义请求拦截器类(RestTrackInterceptor.class)添加到 RestTemplate 中,并为了使其发送请求时具有负载均衡的能力,即加上@LoadBalanced 注解。

RestTemplateConfig 配置类代码如下:

package com.smart4j.core.logtrack.config;
import com.smart4j.core.logtrack.interceptor.RestTrackInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import java.util.Collections;
/**
 * @Description:  RestTemplate配置
 * @Param:
 * @return:
 * @Author: Mr.Zhang
 * @Date: 2020/4/15
 */
@Configuration
public class RestTemplateConfig {
    @Autowired
    private RestTrackInterceptor restTrackInterceptor;
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        // 把自定义的RestTrackInterceptor添加到RestTemplate,这里可添加多个
        restTemplate.setInterceptors(Collections.singletonList(restTrackInterceptor));
        return restTemplate;
    }
}

该类对应的包位置下图所示:

4)创建接口测试类 RestTemplateController.class,其中调用 logtrack-2 服务时,通过 zk 注册的服务名进行调用即可,代码如下:

package com.smart4j.core.logtrack.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
 * @Description:  跨服务日志追踪测试
 * @Param:
 * @return:
 * @Author: Mr.Zhang
 * @Date: 2020/4/15
 */
@Slf4j
@RestController
@RequestMapping("/rest")
public class RestTemplateController {
    @Autowired
    private RestTemplate restTemplate;
    @GetMapping("/log")
    public String logTrack(){
        log.info("-----> Hi, info<-----");
        log.warn("-----> Hi, warn <-----");
        log.error("-----> Hi, error <-----");
        String reqUrl = "http://logtrack-2/test/log";
        log.info("-----> 调服务接口{}开始 <-----", reqUrl);
        ResponseEntity<String> entity = restTemplate.getForEntity(reqUrl, String.class);
        log.info("-----> 调服务接口{}结束 <-----", reqUrl);
        return null;
    }
}

5)基于 RestTemplate 客户端的日志链路追踪基本功能就已经实现了。然后在 logtrack-2 工程中也新增自定义拦截器和创建自定义的 RestTemplate 的Bean,和 logtrack-1 工程代码及结构一样,最后的工程结构如下图所示:

logtrack-2 工程的接口测试类 RestTemplateController.class 代码如下:

package com.smart4j.core.logtrack.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
 * @Description:  跨服务日志追踪测试
 * @Param:
 * @return:
 * @Author: Mr.Zhang
 * @Date: 2020/4/15
 */
@Slf4j
@RestController
@RequestMapping("/rest")
public class RestTemplateController {
    @Autowired
    private RestTemplate restTemplate;
    @GetMapping("/log")
    public String logTrack(){
        log.info("-----> Hi, info<-----");
        log.warn("-----> Hi, warn <-----");
        log.error("-----> Hi, error <-----");
        String reqUrl = "http://logtrack-1/test/log";
        log.info("-----> 调服务接口{}开始 <-----", reqUrl);
        ResponseEntity<String> entity = restTemplate.getForEntity(reqUrl, String.class);
        log.info("-----> 调服务接口{}结束 <-----", reqUrl);
        return null;
    }
}

6)分别启动 logtrack-1 和 logtrack-2 服务,启动及注册成功见下面 3 张图:

7)通过 Postman 模拟浏览器请求

点击 Send 后请求的日志如下图所示:

  • 在 logtrack-1 服务中打印的日志信息
[[[2020-04-19 16:21:11 | INFO  | 1234567890 | http-nio-9001-exec-3 | RestTemplateController.java:27 | com.smart4j.core.logtrack.controller.RestTemplateController : -----> Hi, info<-----]]]
[[[2020-04-19 16:21:11 | WARN  | 1234567890 | http-nio-9001-exec-3 | RestTemplateController.java:28 | com.smart4j.core.logtrack.controller.RestTemplateController : -----> Hi, warn <-----]]]
[[[2020-04-19 16:21:11 | ERROR | 1234567890 | http-nio-9001-exec-3 | RestTemplateController.java:29 | com.smart4j.core.logtrack.controller.RestTemplateController : -----> Hi, error <-----]]]
[[[2020-04-19 16:21:11 | INFO  | 1234567890 | http-nio-9001-exec-3 | RestTemplateController.java:31 | com.smart4j.core.logtrack.controller.RestTemplateController : -----> 调服务接口http://logtrack-2/test/log开始 <-----]]]
[[[2020-04-19 16:21:11 | INFO  | 1234567890 | http-nio-9001-exec-3 | RestTemplateController.java:33 | com.smart4j.core.logtrack.controller.RestTemplateController : -----> 调服务接口http://logtrack-2/test/log结束 <-----]]]
  • 在 logtrack-2 服务中打印的日志信息
[[[2020-04-19 16:21:11 | INFO  | 1234567890 | http-nio-9002-exec-7 | LogTrackController.java:22 | com.smart4j.core.logtrack.controller.LogTrackController : -----> Nice meeting you, too, info <-----]]]
[[[2020-04-19 16:21:11 | WARN  | 1234567890 | http-nio-9002-exec-7 | LogTrackController.java:23 | com.smart4j.core.logtrack.controller.LogTrackController : -----> Nice meeting you, too, warn <-----]]]
[[[2020-04-19 16:21:11 | ERROR | 1234567890 | http-nio-9002-exec-7 | LogTrackController.java:24 | com.smart4j.core.logtrack.controller.LogTrackController : -----> Nice meeting you, too, error <-----]]]

从上面的两个服务的日志打印信息可以看出,实现了跨服务的日志链路追踪效果。

② SpringCloud Feign — 申明式服务调用日志链路追踪实现

1)虽然 RestTemplate 客户端已经可以将请求拦截来实现对依赖服务的接口调用,并对 Http 请求进行封装处理,形成一套模板化的调用方法,但是对服务依赖的调用可能不只一处,一个接口都会被多次调用,所以我们会像前面那样针对各个微服务字形封装一些客户端接口调用类来包装这些依赖服务的调用。

由于 RestTemplate 的封装,几乎每一个调用都是简单的模板化内容,Feign 在此基础上做了进一步的封装,由它来帮助我们定义和实现依赖服务接口的定义。

在服务消费者创建服务调用接口,通过 @FeignClient 注解指定服务名来绑定服务,然后再使用 SpringMVC 的注解来绑定具体该服务提供的 REST API 接口。

2)为了支持 feign 声明式调用,在 pom.xml 文件中加入相应的依赖如下:

<!-- feign -->
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-openfeign</artifactId>
 </dependency>

3)这里以 logtrack-1 工程作为 feign 的消费者,logtrack-2 工程作为 feign 服务的提供者。

  • 在 logtrack-2 工程下新建 FeignController 类作为 feign 服务提供者,代码如下:
package com.smart4j.core.logtrack.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * @Description:  feign-跨服务日志追踪测试
 * @Param:
 * @return:
 * @Author: Mr.Zhang
 * @Date: 2020/4/15
 */
@Slf4j
@RestController
@RequestMapping("/feign")
public class FeignController {
    @GetMapping("/log")
    public String logTrack(){
        log.info("-----> Hi,feign, info<-----");
        log.warn("-----> Hi,feign, warn <-----");
        log.error("-----> Hi,feign, error <-----");
        return "hello world";
    }
}

  • SpringCloud 应用中,通过 feign 的方式实现 http 的调用,可以通过实现 feign.RequestInterceptor 接口在 feign 执行后进行拦截,对请求头等信息进行修改。在 logtrack-1 工程中新增 feign 声明式调用的自定义拦截器(FeignTrackInterceptor.class),代码如下所示:
package com.smart4j.core.logtrack.interceptor;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import java.util.Optional;
import java.util.UUID;
/**
 * @Description:  feign请求拦截器
 * @Param:
 * @return:
 * @Author: Mr.Zhang
 * @Date: 2020/4/15
 */
public class FeignTrackInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        // 从TRACE_ID_THREAD_LOCAL中获取traceId,如无则生成UUID替换
        String traceId = Optional.ofNullable(LogTrackInterceptor.getTraceId()).orElse(UUID.randomUUID().toString().replaceAll("-",""));
        requestTemplate.header("trace-id", traceId);
    }
}
  • 在 logtrack-1 工程的 com.smart4j.core.logtrack.service 包下新建 FeignClient 请求的客户端(FeignService.class),代码如下所示:
package com.smart4j.core.logtrack.service;
import com.smart4j.core.logtrack.interceptor.FeignTrackInterceptor;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
 * @Description:  FeignClient客户端
 * @Param:
 * @return:
 * @Author: Mr.Zhang
 * @Date: 2020/4/15
 */
@FeignClient(value = "logtrack-2", configuration = FeignTrackInterceptor.class)
public interface FeignService {
    @GetMapping("/feign/log")
    String logTrack();
}

其中 @FeignClient 注解的 configuration = FeignTrackInterceptor.class 表示使用自定义的 FeignTrackInterceptor 拦截器。

  • 最后在 logtrack-1 工程的启动类上加上 @EnableFeignClients 注解,开启声明式调用,截图如下:

  • 重新启动 logtrack-1 和 logtrack-2 服务,通过 Postman 模拟发送请求验证 feign 客户端请求的日志链路追踪,见下图:

备注:postman请求时没有传参数 trace-id 也可跟踪请求

点击 Send 后发送 http 请求的日志如下图所示:

  • 在 logtrack-1 服务中打印的日志信息
[[[2020-04-19 16:47:11 | INFO  | d333d549b9264ac68c1fa72910d25087 | http-nio-9001-exec-4 | FeignController.java:26 | com.smart4j.core.logtrack.controller.FeignController : -----> Hi, info<-----]]]
[[[2020-04-19 16:47:11 | WARN  | d333d549b9264ac68c1fa72910d25087 | http-nio-9001-exec-4 | FeignController.java:27 | com.smart4j.core.logtrack.controller.FeignController : -----> Hi, warn <-----]]]
[[[2020-04-19 16:47:11 | ERROR | d333d549b9264ac68c1fa72910d25087 | http-nio-9001-exec-4 | FeignController.java:28 | com.smart4j.core.logtrack.controller.FeignController : -----> Hi, error <-----]]]
[[[2020-04-19 16:47:11 | INFO  | d333d549b9264ac68c1fa72910d25087 | http-nio-9001-exec-4 | FeignController.java:29 | com.smart4j.core.logtrack.controller.FeignController : -----> 调logtrack-2 feign服务接口开始 <-----]]]
[[[2020-04-19 16:47:11 | INFO  | d333d549b9264ac68c1fa72910d25087 | http-nio-9001-exec-4 | FeignController.java:31 | com.smart4j.core.logtrack.controller.FeignController : -----> 调logtrack-2 feign服务接口结束,结果为: hello world <-----]]]
  • 在 logtrack-2 服务中打印的日志信息
[[[2020-04-19 16:47:11 | INFO  | d333d549b9264ac68c1fa72910d25087 | http-nio-9002-exec-4 | FeignController.java:21 | com.smart4j.core.logtrack.controller.FeignController : -----> Hi,feign, info<-----]]]
[[[2020-04-19 16:47:11 | WARN  | d333d549b9264ac68c1fa72910d25087 | http-nio-9002-exec-4 | FeignController.java:22 | com.smart4j.core.logtrack.controller.FeignController : -----> Hi,feign, warn <-----]]]
[[[2020-04-19 16:47:11 | ERROR | d333d549b9264ac68c1fa72910d25087 | http-nio-9002-exec-4 | FeignController.java:23 | com.smart4j.core.logtrack.controller.FeignController : -----> Hi,feign, error <-----]]]

3.2 日志追踪案例二

3.2.1 经过网关路由访问 logtrack-1 和 logtrack-2 服务为例

1)日志链路图解如下所示:

2)网关服务的搭建,整合 zuul 网关,在 pom.xml 文件中添加 zuul 依赖:

<!-- zuul 网关-->
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

完整的 pom.xml 文件内容见下图所示:

修改 application.yml 文件内容如下:

log-zuul 工程和 logtrack-1、logtrack-2 工程结构内容,见下图:

其中多了两个过滤器类 TrackFilter 和 PostFilter,都继承了 ZuulFilter;一个 CorsConfig 类,是请求跨域配置类;一个常量类 TrackConstants,即 TRACE_ID;一个获取去除“-”的 UUID 工具类 CommonUtils.class;去除了拦截器 LogTrackInterceptor 和 CustomInterceptorConfig 相关设置。

新增的各个类的编码如下:

  • 过滤器 TrackFilter.class,该过滤器是为了获取前端应用的请求头参数 trace-id,如无则生成 UUID 替换,具体编码实现见截图:

这里使用过滤器代替了之前拦截器实现 traceId 日志追踪功能

  • 代码清单如下:
package com.smart4j.core.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.smart4j.core.zuul.constants.TrackConstants;
import com.smart4j.core.zuul.util.CommonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
/**
 * @Description: traceId过滤器
 * @Param:
 * @return:
 * @Author: Mr.Zhang
 * @Date: 2020/4/15
 */
@Slf4j
@Component
public class TrackFilter extends ZuulFilter {
    /**
     * 存储 traceId 的线程局部共享变量
     */
    private static final ThreadLocal<String> TRACE_ID_THREAD_LOCAL = new ThreadLocal<>();
    /**
     * 设置过滤类型
     * 四种不同生命周期的过滤器类型:pre,routing,error,post
     * pre:主要用在路由映射的阶段是寻找路由映射表的。
     * routing:具体的路由转发过滤器是在routing路由器,具体的请求转发的时候会调用。
     * error:一旦前面的过滤器出错了,会调用error过滤器。
     * post:当routing,error运行完后才会调用该过滤器,是在最后阶段的。
     * @return
     */
    @Override
    public String filterType() {
        // 声明过滤器的类型为Pre
        return FilterConstants.PRE_TYPE;
    }
    @Override
    public int filterOrder() {
        // 自定义过滤器执行的顺序,数值越大越靠后执行,越小就越先执行
        return -1;
    }
    /**
     * 控制过滤器生效不生效,可以在里控制是否执行过滤的具体逻辑
     * @return
     */
    @Override
    public boolean shouldFilter() {
        // 返回true,则执行下面的run()方法;反之,则不执行过滤具体逻辑
        return true;
    }
    /**
     * 执行过滤的具体逻辑:获取请求头中的trace-id(没有则UUID生成)向下游应用服务透传
     * @return
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        // 从上下文中拿到请求对象
        HttpServletRequest request = ctx.getRequest();
        // 获取请求头header中传递的trace-id
        String traceId = request.getHeader(TrackConstants.TRACE_ID);
        log.info("获取请求头header中传递的trace-id:{}", traceId);
        // 若没有traceId,则UUID代替
        traceId = Optional.ofNullable(traceId).orElse(CommonUtils.getUUID());
        // 请求前设置
        TRACE_ID_THREAD_LOCAL.set(traceId);
        // 通过上下文设置请求头trace-id
        ctx.addZuulRequestHeader(TrackConstants.TRACE_ID, traceId);
        log.info("通过上下文设置请求头trace-id:{}", traceId);
        return null;
    }
    /**
     * 获取 TRACE_ID_THREAD_LOCAL 中存储的 traceId
     * @return
     */
    public static String getTraceId() {
        return TRACE_ID_THREAD_LOCAL.get();
    }
    /**
     * 清理 TRACE_ID_THREAD_LOCAL
     * @return
     */
    public static void removeTraceId() {
        TRACE_ID_THREAD_LOCAL.remove();
    }
}
  • PostFilter 过滤器类中手动调用 ThreadLocal 的 remove() 方法,防止内存泄漏,代码如下:
package com.smart4j.core.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
/**
 * @Description: Post过滤器
 * @Param:
 * @return:
 * @Author: Mr.Zhang
 * @Date: 2020/4/15
 */
@Slf4j
@Component
public class PostFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }
    @Override
    public int filterOrder() {
        return -1;
    }
    @Override
    public boolean shouldFilter() {
        return true;
    }
    @Override
    public Object run() throws ZuulException {
        // 移除,防止内存泄漏
        TrackFilter.removeTraceId();
        return null;
    }
}
  • 自定义 TraceIdPatternConverter 日志转换器类(traceId 改成从过滤器 TrackFilter 中获取),代码修改如下:
package com.smart4j.core.zuul.config;
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.smart4j.core.zuul.filter.TrackFilter;
import org.springframework.util.StringUtils;
/**
* @Description:  自定义日志格式化
* @Param:
* @return:
* @Author: Mr.Zhang
* @Date: 2020/4/14
*/
public class TraceIdPatternConverter extends ClassicConverter {
  @Override
  public String convert(ILoggingEvent iLoggingEvent) {
    // 从过滤器中获取TRACE_ID_THREAD_LOCAL存储的 traceId
    String traceId = TrackFilter.getTraceId();
    return StringUtils.isEmpty(traceId) ? "traceId" : traceId;
  }
}
  • 跨域请求配置类 CorsConfig.class(为了统一在网关入口解决前端应用 Ajax 请求浏览器同源策略导致的跨域问题)代码清单如下:
package com.smart4j.core.zuul.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
 * @Description: 跨域配置类
 * @Param:
 * @return:
 * @Author: Mr.Zhang
 * @Date: 2020/4/15
 */
@Configuration
public class CorsConfig {
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许cookie跨域
        corsConfiguration.setAllowCredentials(true);
        // 允许任何域名使用
        corsConfiguration.addAllowedOrigin("*");
        // 允许任何头
        corsConfiguration.addAllowedHeader("*");
        // 允许任何方法(post、get等)
        corsConfiguration.addAllowedMethod("*");
        // 设置跨域缓存时间,单位为秒
        corsConfiguration.setMaxAge(300L);
        return corsConfiguration;
    }
    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 对接口配置跨域设置
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}
  • 常量类 TrackConstants.class,请求头参数变量设置,代码清单如下:
package com.smart4j.core.zuul.constants;
/**
 * @Description: 常量类
 * @Param:
 * @return:
 * @Author: Mr.Zhang
 * @Date: 2020/4/15
 */
public class TrackConstants {
    /**
     * 请求头参数 trace-id
     */
    public static final String TRACE_ID = "trace-id";
}
  • 工具类 CommonUtils.class,代码如下:
package com.smart4j.core.zuul.util;
import java.util.UUID;
/**
 * @Description: 公共工具类
 * @Param:
 * @return:
 * @Author: Mr.Zhang
 * @Date: 2020/4/15
 */
public class CommonUtils {
    /**
     * 获取除去“-”的UUID
     * @return
     */
    public static String getUUID() {
        return UUID.randomUUID().toString().replaceAll("-","");
    }
}
  • log-zuul 工程启动类上添加注解 @EnableZuulProxy,开启 zuul 网关的功能,如下图所示:

  • log-zuul 工程:网关层实现了基本的跨域请求、路由及过滤器处理的相关功能,这里不细说 zuul 网关是什么,怎么用的,这个百度有很多博文介绍。

上面是成功启动 log-zuul 服务的截图,访问 zkui 管理系统,如下:

说明在 zk 上成功注册了 log-zuul、logtrack-1、logtrack-2 三个服务。

现在开始用 Postman 模拟请求测试,通过网关 log-zuul 请求路由到下游服务 logtrack-1、logtrack-2上。

  • Postman 中请求头设置了 trace-id=11223344556677889900,发送GET请求。

请求结果如下:

log-zuul 服务打印的日志如下:

[[[2020-04-19 22:31:06 | INFO  | traceId | http-nio-9000-exec-7 | TrackFilter.java:72 | com.smart4j.core.zuul.filter.TrackFilter : 获取请求头header中传递的trace-id:11223344556677889900]]]
[[[2020-04-19 22:31:06 | INFO  | 11223344556677889900 | http-nio-9000-exec-7 | TrackFilter.java:80 | com.smart4j.core.zuul.filter.TrackFilter : 通过上下文设置请求头trace-id:11223344556677889900]]]

logtrack-1 服务打印的日志如下:

[[[2020-04-19 22:31:06 | INFO  | 11223344556677889900 | http-nio-9001-exec-1 | RestTemplateController.java:27 | com.smart4j.core.logtrack.controller.RestTemplateController : -----> Hi, info<-----]]]
[[[2020-04-19 22:31:06 | WARN  | 11223344556677889900 | http-nio-9001-exec-1 | RestTemplateController.java:28 | com.smart4j.core.logtrack.controller.RestTemplateController : -----> Hi, warn <-----]]]
[[[2020-04-19 22:31:06 | ERROR | 11223344556677889900 | http-nio-9001-exec-1 | RestTemplateController.java:29 | com.smart4j.core.logtrack.controller.RestTemplateController : -----> Hi, error <-----]]]
[[[2020-04-19 22:31:06 | INFO  | 11223344556677889900 | http-nio-9001-exec-1 | RestTemplateController.java:31 | com.smart4j.core.logtrack.controller.RestTemplateController : -----> 调服务接口http://logtrack-2/test/log开始 <-----]]]
[[[2020-04-19 22:31:06 | INFO  | 11223344556677889900 | http-nio-9001-exec-1 | RestTemplateController.java:33 | com.smart4j.core.logtrack.controller.RestTemplateController : -----> 调服务接口http://logtrack-2/test/log结束 <-----]]]

logtrack-2 服务打印的日志如下:

[[[2020-04-19 22:31:06 | INFO  | 11223344556677889900 | http-nio-9002-exec-6 | LogTrackController.java:22 | com.smart4j.core.logtrack.controller.LogTrackController : -----> Nice meeting you, too, info <-----]]]
[[[2020-04-19 22:31:06 | WARN  | 11223344556677889900 | http-nio-9002-exec-6 | LogTrackController.java:23 | com.smart4j.core.logtrack.controller.LogTrackController : -----> Nice meeting you, too, warn <-----]]]
[[[2020-04-19 22:31:06 | ERROR | 11223344556677889900 | http-nio-9002-exec-6 | LogTrackController.java:24 | com.smart4j.core.logtrack.controller.LogTrackController : -----> Nice meeting you, too, error <-----]]]

3 个服务的日志截图如下:

从以上 3 个服务的结果来看,从前端发送请求到结束,traceId 由网关 log-zuul 服务、到 logtrack-1 服务、再到 logtrack-2 服务,完成了日志链路追踪的功能。

  • Postman 中请求头没有设置 trace-id 参数,发送GET请求。结果如下:

通过以上的结果可知,前端请求头中没有传递 trace-id,后台自动生成 UUID 替换,实现网关到下游服务的全链路追踪。

到这里全链路日志追踪实现和测试案例已经介绍完了,是不是很简单呢

4. 小结

本文构建了三个 Spring Boot 轻量级微服务系统,一个网关服务和两个下游接口服务,Step By Step 模拟实现了两种链路追踪案例,一种直接请求接口服务提供方,另一种是通过网关转发到接口服务提供方(推荐)。这两种方式实现的分布式系统跨服务调用全链路日志追踪的思路差不多。思路很重要!思路很重要!思路很重要!思路很重要!(重要的话说四遍)

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
分布式系统「全链路日志追踪」实战之 RestTemplate & Feign
(图片来源于 Google Dapper 的一篇论文,这是链路追踪理论基础的鼻祖)这张图看上去感觉很高大上的样子 ,但精髓在于日志追踪架构设计思维。即设计思维很...
<<上一篇
下一篇>>