让应用程序之间互相传递RequestId

我想要做的事情

如果服务涉及多个应用,在运行时可能需要查看跨应用的日志,下面是实现方法:
1. 使用Spring Boot生成RequestId,并将其输出到日志文件中。
2. 向其他应用(例如Gin)发送请求。
3. 在Gin端接收RequestId。

框架
Spring Boot
Gin

The environment

    • Java

 

    • Spring Boot

 

    • Go

 

    Gin

在Spring Boot中配置RequestId的设置

确保日志文件中显示RequestId。

使用MDC来为每个请求生成UUID,并将其嵌入在日志中。

import org.slf4j.MDC;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;

public class SampleFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String requestIdKey = "X-REQUEST-ID";
        String uuid = UUID.randomUUID().toString();

        // どこからでもリクエストIDが取得できるように設定しておく
        request.setAttribute(requestIdKey, uuid);

        try {
            // ログに出力できるように設定
            MDC.put(requestIdKey, uuid);
            filterChain.doFilter(request, response);

        } finally {
            MDC.remove(requestIdKey);
        }
    }
}

将Filter注册到Bean中,以便能够使用Filter。

将/api下的路径设置为过滤器所适用的。

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SampleConfiguration {

    @Bean
    public FilterRegistrationBean<SampleFilter> hogeFilter() {
        FilterRegistrationBean<SampleFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new SampleFilter());
        bean.addUrlPatterns("/api/*");
        bean.setOrder(Integer.MIN_VALUE);
        return bean;
    }
}

Logback的设置

在SampleFilter.java中定义的X-REQUEST-ID也要设置为.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml" />
    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
    <include resource="org/springframework/boot/logging/logback/file-appender.xml" />

    <springProperty name="loggingFilePath" source="logging.file.path" />

    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>${loggingFilePath}/spring.log</file>
        <encoder>
            <pattern>[%-5level] [%X{X-REQUEST-ID}] [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%logger{36}] - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>
</configuration>

这次我们只会发布INFO级别以上的内容。

logging.file.path=logs/
logging.level.org.springframework=INFO

提交请求并查看日志

请在 http://localhost:8080/api/ 上创建一个 GET 的端点。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("api")
public class SampleController {

    private static final Logger logger = LoggerFactory.getLogger(SampleController.class);

    @GetMapping
    public String get() {
        logger.info("info!");
        return "get";
    }
}

请求结果的日志文件 de

[INFO ] [2389c607-f5ed-4789-95a3-efd78be1e8d9] [2020-XX-XX 23:26:25.536] [c.e.log.logdemo.SampleController] - info!

看起来像是由2389c607-f5ed-4789-95a3-efd78be1e8d9生成的UUID,好像正常显示出来了。

在使用RestTemplate向其他服务发送请求时,附加RequestId。

将RestTemplate注册为Bean,以便使用。(尽管在这里定义它并不是必需的,但这样做很方便。)

@SpringBootApplication
public class LogDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(LogDemoApplication.class, args);
    }

    @Bean
    public RestTemplate setRestTemplate(){
        return new RestTemplate();
    }
}

为他的应用程序准备一个投放点。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
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;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

@RestController
@RequestMapping("api")
public class SampleController {

    private static final Logger logger = LoggerFactory.getLogger(SampleController.class);

    // 追記
    RestTemplate restTemplate;

    // 追記
    public SampleController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @GetMapping
    public String get() {
        logger.info("info!");
        return "get";
    }

    // 追記
    @GetMapping("to-other-app")
    public String toOtherApp() {

        String requestId = (String) RequestContextHolder
                .getRequestAttributes()
                .getAttribute("X-REQUEST-ID", RequestAttributes.SCOPE_REQUEST);

        logger.info("他のアプリケーションにリクエスト投げます!");

        HttpHeaders headers = new HttpHeaders();
        headers.set("X-REQUEST-ID", requestId);
        HttpEntity<String> entity = new HttpEntity<>("headers", headers);
        ResponseEntity<OtherAppResponse> response = restTemplate.exchange("http://localhost/api", HttpMethod.GET, entity, OtherAppResponse.class);

        return response.getBody().getValue();
    }
}

这样,Spring Boot的设置就完成了!

创建Gin侧的应用程序

我将使用另一篇我自己写的文章进行借用。

只需要运行“docker-compose up -d”,很简单。
访问http://localhost:80/api,只会返回一个 {“key”: “value”} 的API。

彼此互相确认日志。

Spring Boot将在http://localhost:8080/启动。
Gi将在http://localhost:80/启动。

在Spring Boot中处理请求

访问以下地址:http://localhost:8080/api/to-other-app

[INFO ] [549ef61a-44c9-4fe4-9c0f-3d923976d32f] [2020-XX-XX 00:19:00.274] [c.e.log.logdemo.SampleController] - 他のアプリケーションにリクエスト投げます!

我能看到日志
请求ID是 549ef61a-44c9-4fe4-9c0f-3d923976d32f

请检查Gin侧的日志文件。

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api                      --> main.main.func1 (4 handlers)
[GIN-debug] Listening and serving HTTP on :3000
[GIN] 2020/XX/XX - 15:16:44 | 200 |       1.005ms |      172.19.0.3 | GET      "/api"
{"time":"2020-XX-XXT15:19:00.3482677Z","level":"-","prefix":"-","file":"main.go","line":"30","message":"RequestId=549ef61a-44c9-4fe4-9c0f-3d923976d32f"}

收到的请求ID是549ef61a-44c9-4fe4-9c0f-3d923976d32f。
看起来似乎是匹配成功了(日期已隐藏)。

总结

我做了一些简要的工作,像是在Spring Boot中使请求ID在任何地方都可以使用,或者用Gin创建应用程序等等花费了一些时间。虽然在Spring Boot端使用RequestContextHolder之类的方法,但总感觉不太确定,也没有想到其他的方法,所以如果您知道的话,我会非常高兴能告诉我。由于在Gin端只需设置Header即可将其返回给Spring Boot,所以我忽略了这一部分。为什么选择Gin呢?因为我们团队平常是用Spring Boot + NodeJS实现的,但最近我对Go产生了兴趣(Go很有趣)。
如果有难以理解的地方,或者更高效的方法和建议,请务必评论告诉我。

广告
将在 10 秒后关闭
bannerAds