排查日志困难?阿里云 SLS+TraceID 教你一招鲜吃遍天

1. 日志上云工具

阿里云 SLS + Aliyun Logback Appender

Logback 是由 log4j 创始人设计的又一个开源日志组件。通过使用 Logback,您可以控制日志信息输送的目的地是控制台、文件、GUI 组件、甚至是套接口服务器、NT 的事件记录器、UNIX Syslog 守护进程等;您也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,您能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

Aliyun Logback Appender

通过Aliyun Log Logback Appender,您可以控制日志的输出目的地为阿里云日志服务,写到日志服务中的日志的样式如下:

vbnet 复制代码
level: ERROR
location: com.aliyun.openservices.log.logback.example.LogbackAppenderExample.main(LogbackAppenderExample.java:18)
message: error log
throwable: java.lang.RuntimeException: xxx
thread: main
time: 2018-01-02T03:15+0000
log: 2018-01-02 11:15:29,682 ERROR [main] com.aliyun.openservices.log.logback.example.LogbackAppenderExample: error log
__source__: xxx
__topic__: yyy
  • level: 日志级别。
  • location: 日志打印语句的代码位置,可以通过配置关闭此选项。
  • message: 日志内容。
  • throwable: 日志异常信息(只有记录了异常信息,这个字段才会出现)。
  • thread: 线程名称。
  • time: 日志打印时间(可以通过 timeFormat 或 timeZone 配置 time 字段呈现的格式和时区)。
  • log: 自定义日志格式(只有设置了 encoder,这个字段才会出现)。
  • source: 日志来源,用户可在配置文件中指定。
  • topic: 日志主题,用户可在配置文件中指定。

Aliyun Logback Appender 的功能优势

  • 日志不落盘:产生数据实时通过网络发给服务端。
  • 无需改造:对已使用logback应用,只需简单配置即可采集。
  • 异步高吞吐:高并发设计,后台异步发送,适合高并发写入。
  • 上下文查询:服务端除了通过关键词检索外,给定日志能够精确还原原始日志文件上下文日志信息。

2. SpringBoot 整合 Aliyun Logback Appender

阿里云 SLS 日志服务

如图所示,创建一个 project:

进入 project,创建一个日志库 log store:

Maven 工程中引入依赖

xml 复制代码
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>2.5.0</version>
</dependency>
<dependency>
    <groupId>com.aliyun.openservices</groupId>
    <artifactId>aliyun-log-logback-appender</artifactId>
    <version>0.1.25</version>
</dependency>

添加 Logback 配置文件

如图所示,在 SpirngBoot 的 resources 下,建立 logstore 目录,创建名为 logback-{环境}.xml 的配置文件:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!--为了防止进程退出时,内存中的数据丢失,请加上此选项-->
    <shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg %X{THREAD_ID} %n</pattern>
        </encoder>
    </appender>

    <appender name="aliyun" class="com.aliyun.openservices.log.logback.LoghubAppender">

        <!--必选项-->
        <!-- 账号及网络配置 -->
        <endpoint>这里写你自己的endpoint</endpoint>
        <accessKeyId>这里写你自己的accessKeyId</accessKeyId>
        <accessKeySecret>这里写你自己的accessKeySecret</accessKeySecret>

        <!-- sls 项目配置 -->
        <project>这里写你自己的project</project>
        <logStore>这里写你自己的logStore</logStore>
        <!--必选项 (end)-->

        <!-- 可选项 详见 '参数说明'-->
        <totalSizeInBytes>104857600</totalSizeInBytes>
        <maxBlockMs>0</maxBlockMs>
        <ioThreadCount>8</ioThreadCount>
        <batchSizeThresholdInBytes>524288</batchSizeThresholdInBytes>
        <batchCountThreshold>4096</batchCountThreshold>
        <lingerMs>2000</lingerMs>
        <retries>10</retries>
        <baseRetryBackoffMs>100</baseRetryBackoffMs>
        <maxRetryBackoffMs>50000</maxRetryBackoffMs>

        <!--只打印级别含INFO及以上的日志-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>

        <!-- 可选项 设置时间格式 -->
        <timeZone>Asia/Shanghai</timeZone>
        <timeFormat>yyyy-MM-dd HH:mm:ss</timeFormat>
        <mdcFields>traceID</mdcFields>
    </appender>

    <root>
        <level value="INFO"/>
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="aliyun"/>
    </root>

</configuration>

配置中的 endpoint accessKeyId accessKeySecret 如何获取 ?参考阿里云SLS帮助: help.aliyun.com/zh/sls/deve...

配置文件参数说明

properties 复制代码
#日志服务的 project 名,必选参数
project = [your project]
#日志服务的 logstore 名,必选参数
logStore = [your logStore]
#日志服务的 HTTP 地址,必选参数
endpoint = [your project endpoint]
#用户身份标识,必选参数
accessKeyId = [your accesskey id]
accessKeySecret = [your accessKeySecret]

#单个 producer 实例能缓存的日志大小上限,默认为 100MB。
totalSizeInBytes=104857600
#如果 producer 可用空间不足,调用者在 send 方法上的最大阻塞时间,默认为 60 秒。为了不阻塞打印日志的线程,强烈建议将该值设置成 0。
maxBlockMs=0
#执行日志发送任务的线程池大小,默认为可用处理器个数。
ioThreadCount=8
#当一个 ProducerBatch 中缓存的日志大小大于等于 batchSizeThresholdInBytes 时,该 batch 将被发送,默认为 512 KB,最大可设置成 5MB。
batchSizeThresholdInBytes=524288
#当一个 ProducerBatch 中缓存的日志条数大于等于 batchCountThreshold 时,该 batch 将被发送,默认为 4096,最大可设置成 40960。
batchCountThreshold=4096
#一个 ProducerBatch 从创建到可发送的逗留时间,默认为 2 秒,最小可设置成 100 毫秒。
lingerMs=2000
#如果某个 ProducerBatch 首次发送失败,能够对其重试的次数,默认为 10 次。
#如果 retries 小于等于 0,该 ProducerBatch 首次发送失败后将直接进入失败队列。
retries=10
#该参数越大能让您追溯更多的信息,但同时也会消耗更多的内存。
maxReservedAttempts=11
#首次重试的退避时间,默认为 100 毫秒。
#Producer 采样指数退避算法,第 N 次重试的计划等待时间为 baseRetryBackoffMs * 2^(N-1)。
baseRetryBackoffMs=100
#重试的最大退避时间,默认为 50 秒。
maxRetryBackoffMs=50000

#指定日志主题,默认为 "",可选参数
topic = [your topic]

#指的日志来源,默认为应用程序所在宿主机的 IP,可选参数
source = [your source]

#输出到日志服务的时间的格式,默认是 yyyy-MM-dd'T'HH:mmZ,可选参数
timeFormat = yyyy-MM-dd'T'HH:mmZ

#输出到日志服务的时间的时区,默认是 UTC,可选参数(如果希望 time 字段的时区为东八区,可将该值设定为 Asia/Shanghai)
timeZone = UTC
#是否要记录 Location 字段(日志打印位置),默认为 true,如果希望减少该选项对性能的影响,可以设为 false
includeLocation = true
#当 encoder 不为空时,是否要包含 message 字段,默认为 true
includeMessage = true

进入 SLS 查看日志信息

注意:聪明的小伙伴会发现,我们的每条日志中都多了 traceID 这个字段,我们下文会重点讲这个问题。此处的 traceID 是我们自己实现并加入的,这将会成为了我们排查每次请求整个生命周期的日志的重要索引

3. SpringBoot 项目加入基础 traceID

公共的请求返回对象中加入 traceID

java 复制代码
@Data
@ApiModel("统一返回实体")
public final class ApiResult<T> implements Serializable {

    private static final long serialVersionUID = -5907790295620098443L;

    @ApiModelProperty("状态码")
    private int code = 200;

    @ApiModelProperty("数据对象")
    private T data;

    @ApiModelProperty("错误信息")
    private String error;

    @ApiModelProperty("请求状态")
    private boolean success = true;

    @ApiModelProperty("链路追踪ID")
    private String traceId;


    public static final String TRACE_ID = "traceID";

    private ApiResult() {
        this.traceId = MDC.get(ApiResult.TRACE_ID);
    }

    private ApiResult(T data) {
        this.data = data;
        this.traceId = MDC.get(ApiResult.TRACE_ID);
    }

    private ApiResult(int code, String error) {
        this.code = code;
        this.error = error;
        this.success = false;
        this.traceId = MDC.get(ApiResult.TRACE_ID);
    }

    private ApiResult(int code, T data, String error) {
        this.code = code;
        this.data = data;
        this.error = error;
        this.success = false;
        this.traceId = MDC.get(ApiResult.TRACE_ID);
    }

    public static <T> ApiResult<T> ok() {
        return new ApiResult<>();
    }

    public static <T> ApiResult<T> ok(T data) {
        return new ApiResult<>(data);
    }

    public static <T> ApiResult<T> error(int code, String error) {
        return new ApiResult<>(code, error);
    }

    public static <T> ApiResult<T> error(int code, T data, String error) {
        return new ApiResult<>(code, data, error);
    }

}

创建一个上下文对象持有 traceID

java 复制代码
@Data
public class TraceSession implements Serializable {

    private static final long serialVersionUID = -1545421111337427237L;

    /**
     * 当前环境
     */
    private String env;

    /**
     * 登录用户ID
     */
    private String userId;

    /**
     * 请求追踪ID
     */
    private String traceId;

    /**
     * 请求语言信息
     */
    private String language;

    /**
     * 请求平台信息
     */
    private String platform;

    /**
     * 请求渠道信息
     */
    private String channel;

    /**
     * 请求版本信息
     */
    private String version;

}

这里先引入一下这个包,后文会具体解释:

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.2</version>
</dependency>
java 复制代码
public class BelifeContext {

    private static final ThreadLocal<TraceSession> THREAD_LOCAL = new TransmittableThreadLocal<>();

    public static void initSession(TraceSession traceSession) {
        THREAD_LOCAL.set(traceSession);
    }

    public static TraceSession getSession() {
        return THREAD_LOCAL.get();
    }

    public static void clearSession() {
        THREAD_LOCAL.remove();
    }

    public static String getTraceId() {
        TraceSession traceSession = getSession();
        if (traceSession == null) return null;
        return traceSession.getTraceId();
    }

}

利用拦截器来生成和处理这个 traceID

java 复制代码
@Component
public class TraceInterceptor implements HandlerInterceptor {

    @Value("${spring.profiles.active}")
    private String profile;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String traceID = UUID.randomUUID().toString().replace("-", "");
        MDC.put(ApiResult.TRACE_ID, traceID);

        TraceSession traceSession = buildTraceSession(traceID);
        BelifeContext.initSession(traceSession);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        MDC.remove(ApiResult.TRACE_ID);
        BelifeContext.clearSession();
    }

    private TraceSession buildTraceSession(String traceID) {
        TraceSession traceSession = new TraceSession();
        traceSession.setEnv(profile);
        traceSession.setTraceId(traceID);
        // 用户信息和请求头信息这里省略 ...
        return traceSession;
    }

}

建立一个测试请求来测试日志和返回值

java 复制代码
@Slf4j
@RestController
@RequestMapping("/v1/app/version")
@Api(tags = {"APP版本接口"})
public class VersionController {

    @ApiOperation("版本信息")
    @GetMapping("/info")
    public ApiResult<String> versionInfo() {
        log.info("测试一下APP版本信息");
        return ApiResult.ok();
    }

}
bash 复制代码
### APP版本信息  
GET http://localhost:8081/v1/app/version/info
Content-Type: application/json

我们得到的返回值中的 traceID,然后去日志库中查询相关日志信息:

yaml 复制代码
HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 22 May 2024 07:42:45 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "code": 200,
  "data": null,
  "error": null,
  "success": true,
  "traceId": "4f267ab7deaa4149acca19cac69b3912"
}

注意:聪明的小伙伴可能会发现,这里的改造只是最基础的日志监控。真实项目应用中,我们可能存在 Java 运行报错,业务日志中还包括 SQL 语句日志,还有一步线程池运行机制,设置还有 MQ 相关的日志,这时 traceID 会不会丢失,还能不能起到预想中的效果呢?别急,后文会主要实现这里提到的这一系列场景问题。

4. 项目中运行报错加入 traceID

定义一个自己的义务异常 BizException

java 复制代码
@Data
public class BizException extends RuntimeException {

    private static final long serialVersionUID = -3697924501642645015L;

    private int code;

    public BizException(String message) {
        super(message);
    }

    public BizException(int code, String message) {
        super(message);
        this.code = code;
    }

}

使用全局异常捕获器处理 BizException

java 复制代码
@Slf4j
@ControllerAdvice
public class ExceptionAdvisor {

    @ResponseBody
    @ExceptionHandler(BizException.class)
    public ApiResult<?> exceptionHandler(HttpServletRequest request, BizException ex) {
        log.error(ex.getMessage(), ex);
        ApiResult<?> apiResult = ApiResult.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage());
        return apiResult;
    }

    @ResponseBody
    @ExceptionHandler(Exception.class)
    public ApiResult<?> exceptionHandler(HttpServletRequest request, Exception ex) {
        log.error(ex.getMessage(), ex);
        ApiResult<?> apiResult = ApiResult.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Internal Server Error");
        return apiResult;
    }

}

建立一个测试请求来测试日志和返回值

java 复制代码
@Slf4j
@RestController
@RequestMapping("/v1/app/version")
@Api(tags = {"APP版本接口"})
public class VersionController {

    @ApiOperation("版本信息")
    @GetMapping("/info")
    public ApiResult<String> versionInfo() {
        throw new BizException("测试一下APP版本信息-接口报错");
        // return ApiResult.ok();
    }

}
bash 复制代码
### APP版本信息  
GET http://localhost:8081/v1/app/version/info
Content-Type: application/json

我们得到的返回值中的 traceID,然后去日志库中查询相关日志信息:

yaml 复制代码
HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 22 May 2024 08:03:16 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "code": 500,
  "data": null,
  "error": "测试一下APP版本信息-接口报错",
  "success": false,
  "traceId": "8f883ce3b7a845bca5ea83b348b5113a"
}

5. 项目中 Mybatis-Plus 的 SQL 日志加入 traceID

Maven 工程中引入依赖

xml 复制代码
<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.9.1</version>
</dependency>

项目中新加入配置文件 spy.properties

JAVA 复制代码
@PropertySource(value = "classpath:spy.properties")

appender 选择 com.p6spy.engine.spy.appender.Slf4JLogger 就自动接入了我们项目中的 Aliyun Logback Appender 体系:

properties 复制代码
# 模块列表,根据版本选择合适的配置
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory

# 自定义日志格式
# (替换 P6spyFormatConfig) logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
logMessageFormat=org.belife.domain.config.P6spyFormatConfig

# 日志输出到控制台
# (替换 Slf4JLogger) appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
appender=com.p6spy.engine.spy.appender.Slf4JLogger

# 取消JDBC驱动注册
deregisterdrivers=true

# 使用前缀
useprefix=true

# 排除的日志类别
excludecategories=info,debug,result,commit,resultset

# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss

# 实际驱动列表
# driverlist=org.h2.Driver

# 开启慢SQL记录
outagedetection=true

# 慢SQL记录标准(单位:秒)
outagedetectioninterval=2

自定义 SQL 日志格式化器

java 复制代码
public class P6spyFormatConfig implements MessageFormattingStrategy {

    @Override
    public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql, String url) {
        return StringUtils.isNotBlank(sql) ? DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss") + " | " + elapsed + " ms | " + sql.replaceAll("[\\s]+", StringUtils.SPACE) + ";" : "";
    }

}

修改 SpringBoot 数据源连接的配置

yml 复制代码
spring:
  datasource:
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver
    url: jdbc:p6spy:mysql://(写你自己的连接地址):3306/test_belife?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&useSSL=true&connectTimeout=60000&socketTimeout=60000&allowMultiQueries=true
    username: (写你自己的账号)
    password: (写你自己的密码)

    druid:
      initial-size: 3
      min-idle: 5
      max-active: 30
      max-wait: 60000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 50

这里为了方便对比,放出原先的 druid 数据源配置:

yml 复制代码
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://(写你自己的连接地址):3306/prd_belife?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&useSSL=true&connectTimeout=60000&socketTimeout=60000&allowMultiQueries=true
    username: (写你自己的账号)
    password: (写你自己的密码)

    druid:
      initial-size: 3
      min-idle: 5
      max-active: 30
      max-wait: 60000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 50

建立一个测试请求来测试日志和返回值

java 复制代码
@Slf4j
@RestController
@RequestMapping("/v1/app/version")
@Api(tags = {"APP版本接口"})
public class VersionController {

    @Autowired
    private VersionService versionService;

    @ApiOperation("版本信息")
    @GetMapping("/info")
    public ApiResult<VersionDO> versionInfo() {
        log.info("测试一下APP版本信息");
        VersionDO latest = versionService.findLatest("Android");
        return ApiResult.ok(latest);
    }

}
bash 复制代码
### APP版本信息  
GET http://localhost:8081/v1/app/version/info
Content-Type: application/json

我们得到的返回值中的 traceID,然后去日志库中查询相关日志信息:

javascript 复制代码
HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 22 May 2024 08:26:55 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "code": 200,
  "data": {
    "id": "44",
    "platform": "Android",
    "channel": null,
    "version": "1.7.0",
    "forces": 0,
    "chNotice": "中文",
    "enNotice": "更新",
    "fileUrl": null,
    "fileSize": null,
    "status": 1,
    "remark": ""
  },
  "error": null,
  "success": true,
  "traceId": "7c5016ae8a9147f89f0b73e7c093bce4"
}
sql 复制代码
16:26:55.268 [http-nio-8081-exec-1] INFO  p6spy - 2024-05-22 16:26:55 | 79 ms | SELECT * FROM app_version WHERE platform = 'Android' AND status = 1 ORDER BY gmt_created DESC LIMIT 0,1;  

6. 项目中异步线程池运行加入 traceID

引入阿里的 TTL 工具包

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.2</version>
</dependency>

具体实现原理可以参考作者的其他文章:

TransmittableThreadLocal 线程池内异步线程值传递解决方案

实现一个抽象的线程池处理器

java 复制代码
@Slf4j
public abstract class TtlPoolManager {

    ExecutorService executorService = initExecutorService();

    ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(executorService);

    protected abstract ExecutorService initExecutorService();

    public Future<?> submit(Runnable task) {
        return ttlExecutorService.submit(() -> {
            try {
                MDC.put(ApiResult.TRACE_ID, getTraceId());
                TtlRunnable.get(task).run();
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            } finally {
                MDC.remove(ApiResult.TRACE_ID);
            }
        });
    }

    public <T> Future<T> submit(Callable<T> task) {
        return ttlExecutorService.submit(() -> {
            try {
                MDC.put(ApiResult.TRACE_ID, getTraceId());
                return TtlCallable.get(task).call();
            } finally {
                MDC.remove(ApiResult.TRACE_ID);
            }
        });
    }

    public static String getTraceId() {
        return BelifeContext.getTraceId();
    }

}

这里的 traceID 就从我们前文实现的 BelifeContext.getTraceId() 中获取。 如果我们要使用线程池,下边就给出一个简单的参考:

scala 复制代码
@Component
public class MessagePoolManager extends TtlPoolManager {

    @Override
    protected ExecutorService initExecutorService() {
        // 这里初始化自己想要的线程池
        return Executors.newFixedThreadPool(10);
    }

}

建立一个测试请求来测试日志和返回值

java 复制代码
@Slf4j
@RestController
@RequestMapping("/v1/app/version")
@Api(tags = {"APP版本接口"})
public class VersionController {

    @Autowired
    private MessagePoolManager messagePoolManager;

    @ApiOperation("版本信息")
    @GetMapping("/info")
    public ApiResult<String> versionInfo() {
        log.info("测试一下APP版本信息-主线程");
        messagePoolManager.submit(() -> log.info("测试一下APP版本信息-异步线程"));
        return ApiResult.ok();
    }

}
bash 复制代码
### APP版本信息  
GET http://localhost:8081/v1/app/version/info
Content-Type: application/json

我们得到的返回值中的 traceID,然后去日志库中查询相关日志信息:

yaml 复制代码
HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 22 May 2024 08:43:05 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "code": 200,
  "data": null,
  "error": null,
  "success": true,
  "traceId": "90e1d2878280462084a6145c8ce1e00f"
}

7. 项目中使用 RocketMQ 加入 traceID

使用的阿里云的 RocketMQ 4.0版

以 Message 的 Key 作为 traceID 改造生产者和消费者

这里的代码只是一种改造思路(代码涉及到业务会有很多,只贴了重要的一部分),每个人封装的 RocketMQ 方式不同:

java 复制代码
@Slf4j
@Component
public final class SyncMessageProducer {

    @Autowired
    private ProducerBean producer;

    public void sendNormalMessage(String content, String topic, MqBizTags tags) {
        Message message = new Message();
        message.setTopic(topic);
        message.setTag(tags.name());
        message.setKey(BelifeContext.getTraceId());
        message.setBody(content.getBytes(StandardCharsets.UTF_8));
        try {
            SendResult sendResult = producer.send(message);
            assert sendResult != null;
            log.info(sendResult + " Text: {}", content);
        } catch (ONSClientException e) {
            log.error("MQ发送失败, Text: {}", content);
            // 出现异常意味着发送失败,为了避免消息丢失,建议缓存该消
        }
    }

    public void sendTimingMessage(String content, String topic, MqBizTags tags, Date deliverTime) {
        Message message = new Message();
        message.setTopic(topic);
        message.setTag(tags.name());
        message.setKey(BelifeContext.getTraceId());
        message.setBody(content.getBytes(StandardCharsets.UTF_8));
        message.setStartDeliverTime(deliverTime.getTime());
        try {
            SendResult sendResult = producer.send(message);
            assert sendResult != null;
            log.info(sendResult + " Text: {}, Time: {}", content, deliverTime);
        } catch (ONSClientException e) {
            log.error("MQ发送失败, Text: {}, Time: {}", content, deliverTime);
            // 出现异常意味着发送失败,为了避免消息丢失,建议缓存该消
        }
    }

}

这里的 traceID 就从我们前文实现的 BelifeContext.getTraceId() 中获取。

java 复制代码
@Slf4j
public abstract class AbsMessageConsumer implements MessageListener {

    public abstract MqBizTags support();

    @Override
    public Action consume(Message message, ConsumeContext context) {
        MDC.put(ApiResult.TRACE_ID, message.getKey());
        String messageBody = new String(message.getBody());
        log.info("Receive Message: {}", messageBody);
        return consume(messageBody);
    }

    protected abstract Action consume(String messageBody);

}
java 复制代码
@Slf4j
@Service
public class OrderCloseConsumer extends AbsMessageConsumer {

    @Autowired
    private PaymentFacadeService paymentFacadeService;

    @Override
    public MqBizTags support() {
        return MqBizTags.CLOSE_ORDER;
    }

    @Override
    protected Action consume(String messageBody) {
        String orderSn = messageBody;
        try {

            paymentFacadeService.dealWithMqConsumer(orderSn);

        } catch (BizException bizEx) {
            log.error(bizEx.getMessage(), bizEx);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return Action.ReconsumeLater;
        }
        return Action.CommitMessage;
    }

}

建立一个测试请求来测试日志和返回值

测试代码涉及业务代码会有很多,简单描述下业务场景:

顾客下单未支付,15分钟后会将未支付的订单关闭,利用 RocketMQ 的延迟消息实现。

通过 traceID 查询到我们对应的请求的订单相关的消息:

去日志库中查询相关日志信息,即使相隔15分钟,依然能追溯完整的生命周期

8. 项目中的请求 HttpRequest 快照加入 traceID

利用 Spring 的 AOP 处理控制器的方法

java 复制代码
@Slf4j
@Aspect
@Component
public class WebLogAspect {


    @Value("${be-life-app.token-header:belife-app-token}")
    private String tokenHeader;
    @Value("${be-life-app.version-header:belife-app-version}")
    private String versionHeader;
    @Value("${be-life-app.platform-header:belife-app-platform}")
    private String platformHeader;
    @Value("${be-life-app.channel-header:belife-app-channel}")
    private String channelHeader;
    @Value("${be-life-app.language-header:belife-app-language}")
    private String languageHeader;

    @Pointcut("execution(public * org.belife.app.controller..*.*(..))")
    public void webLog() {
    }

    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        long startTime = System.currentTimeMillis();
        Object result = null;
        try {
            result = proceedingJoinPoint.proceed();
            return result;

        } finally {
            if (result instanceof ApiResult) {
                ApiResult<?> apiResult = (ApiResult<?>) result;
                apiResult.setTraceId(MDC.get(ApiResult.TRACE_ID));
            }
            long endTime = System.currentTimeMillis();

            String requestMethod = request.getMethod();
            String requestUri = request.getRequestURI();
            String costTime = (endTime - startTime) + "ms";
            String jsonParams = getParams(proceedingJoinPoint);

            String token = request.getHeader(tokenHeader);
            String version = request.getHeader(versionHeader);
            String platform = request.getHeader(platformHeader);
            String channel = request.getHeader(channelHeader);
            String language = request.getHeader(languageHeader);
            String area = request.getHeader(cnAreaHeader);

            log.info("Method: {}, URL: {}, Version: {}, Platform: {}, Channel: {}, Language: {}, Token: {}, Time: {}, Params: {}",
                    requestMethod, requestUri, version, platform, channel, language, token, costTime, jsonParams);
        }
    }

    /**
     * 获取参数名和参数值
     *
     * @param joinPoint
     * @return 返回JSON结构字符串
     */
    public String getParams(JoinPoint joinPoint) {
        LinkedHashMap<String, Object> map = new LinkedHashMap<>();
        Object[] values = joinPoint.getArgs();
        String[] names = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        for (int i = 0; i < names.length; i++) {
            if (names[i].equals("request") || names[i].equals("response")) continue;
            map.put(names[i], values[i]);
        }
        return JSONObject.toJSONString(map);
    }

}

建立一个测试请求来测试日志和返回值

java 复制代码
@Slf4j
@RestController
@RequestMapping("/v1/app/version")
@Api(tags = {"APP版本接口"})
public class VersionController {

    @Autowired
    private MessagePoolManager messagePoolManager;

    @ApiOperation("版本信息")
    @GetMapping("/info")
    public ApiResult<String> versionInfo() {
        log.info("测试一下APP版本信息-主线程");
        messagePoolManager.submit(() -> log.info("测试一下APP版本信息-异步线程"));
        return ApiResult.ok();
    }

}
makefile 复制代码
### APP版本信息  
  
GET http://localhost:8081/v1/app/version/info?version=1.6.0
belife-app-platform: Android  
belife-app-channel: GooglePlay  
belife-app-version: 1.6.0  
belife-app-language: ZH  
Belife-App-Token: 0M_sv7Ju6TsYTDL5Q_n8mbDuttENRaTYZg__

我们得到的返回值中的 traceID,然后去日志库中查询相关日志信息:

yaml 复制代码
HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 22 May 2024 09:07:09 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "code": 200,
  "data": null,
  "error": null,
  "success": true,
  "traceId": "e2a3c9c2adfa4db8b3e4761f67943802"
}
yaml 复制代码
17:12:51.390 [http-nio-8081-exec-1] INFO  org.belife.app.aspect.WebLogAspect - Method: GET, URL: /v1/app/version/info, Version: 1.6.0, Platform: Android, Channel: GooglePlay, Language: ZH, Area: null, Token: 0M_sv7Ju6TsYTDL5Q_n8mbDuttENRaTYZg__, Time: 6ms, Params: {"version":"1.6.0"} 

总结上文,一个请求进入快照日志 -> 到各种形式的业务处理日志 -> 最后返回请求结果数据,所有的东西都被同一个 traceID 作为总线索引串起来,日志上云也解决了多节点部署和查询上的问题

相关推荐
稚辉君.MCA_P8_Java8 分钟前
玻尔 SpringBoot性能优化
大数据·spring boot·后端·性能优化·kubernetes
星星电灯猴17 分钟前
如何提高 IPA 安全性 面向工程团队的多层安全策略与工具协同方案
后端
CPU NULL25 分钟前
Redis相关知识点总结
java·数据库·spring boot·redis·缓存
晨晖229 分钟前
Spring Boot整合Spring MVC与外部配置完整笔记
java·spring boot·后端
白气急41 分钟前
Web API 参数验证:原生验证 与 FluentValidation 对比
后端
2501_916766541 小时前
【Springboot】主配置文件
java·spring boot·后端
星释1 小时前
Rust 练习册 21:Hello World 与入门基础
开发语言·后端·rust
b***66611 小时前
SpringCloud Gateway 集成 Sentinel 详解 及实现动态监听Nacos规则配置实时更新流控规则
spring cloud·gateway·sentinel
绝无仅有1 小时前
大厂面试题MySQL解析:MVCC、Redolog、Undolog与Binlog的区别
后端·面试·架构
绝无仅有1 小时前
MySQL面试题解析:MySQL读写分离与主从同步
后端·面试·架构