likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 26

1. 日志上云工具

阿里云 SLS + Aliyun Logback Appender

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

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

Aliyun Logback Appender

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

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:

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

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

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

Maven 工程中引入依赖

<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 的配置文件:

logging:  
    config: classpath:logstore/logback-{环境}.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…

配置文件参数说明

#日志服务的 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 查看日志信息

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

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

3. SpringBoot 项目加入基础 traceID

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

@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

@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;

}

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

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.2</version>
</dependency>
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

@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;
    }

}

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

@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();
    }

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

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

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"
}

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

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

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

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

@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

@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;
    }

}

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

@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();
    }

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

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

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"
}

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

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

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

Maven 工程中引入依赖

<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.9.1</version>
</dependency>

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

@PropertySource(value = "classpath:spy.properties")

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

# 模块列表,根据版本选择合适的配置
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 日志格式化器

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 数据源连接的配置

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 数据源配置:

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

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

@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);
    }

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

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

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"
}
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;  

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

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

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

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

引入阿里的 TTL 工具包

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.2</version>
</dependency>

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

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

@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() 中获取。 如果我们要使用线程池,下边就给出一个简单的参考:

@Component
public class MessagePoolManager extends TtlPoolManager {

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

}

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

@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();
    }

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

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

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"
}

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

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

7. 项目中使用 RocketMQ 加入 traceID

使用的阿里云的 RocketMQ 4.0版

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

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

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

@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() 中获取。

@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);

}
@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 查询到我们对应的请求的订单相关的消息:

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

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

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

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

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

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

@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);
    }

}

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

@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();
    }

}
### 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,然后去日志库中查询相关日志信息:

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"
}
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"} 

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

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

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

转载自:https://juejin.cn/post/7371424635894923304
评论
请登录