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

1. 日志上云工具

阿里云 SLS + Aliyun Logback Appender

image.png

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:

image.png

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

image.png

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 查看日志信息

image.png

注意:聪明的小伙伴会发现,我们的每条日志中都多了 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" }

image.png

注意:聪明的小伙伴可能会发现,这里的改造只是最基础的日志监控。真实项目应用中,我们可能存在 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" }

image.png

image.png

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;

image.png

image.png

image.png

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

image.png

image.png

7. 项目中使用 RocketMQ 加入 traceID

使用的阿里云的 RocketMQ 4.0版

image.png

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

微信图片_20240522164121.png

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

微信图片_20240522164125.png

微信图片_20240522164136.png

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

image.png

image.png

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

  • 16
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值