排查日志困难?阿里云 SLS+TraceID 教你一招鲜吃遍天
1. 日志上云工具
阿里云 SLS + Aliyun Logback Appender
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:
进入 project,创建一个日志库 log store:
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 查看日志信息
注意:聪明的小伙伴会发现,我们的每条日志中都多了 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"
}
注意:聪明的小伙伴可能会发现,这里的改造只是最基础的日志监控。真实项目应用中,我们可能存在 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"
}
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;
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"
}
7. 项目中使用 RocketMQ 加入 traceID
使用的阿里云的 RocketMQ 4.0版
以 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 查询到我们对应的请求的订单相关的消息:
去日志库中查询相关日志信息,即使相隔15分钟,依然能追溯完整的生命周期:
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"}
总结上文,一个请求进入快照日志 -> 到各种形式的业务处理日志 -> 最后返回请求结果数据,所有的东西都被同一个 traceID 作为总线索引串起来,日志上云也解决了多节点部署和查询上的问题。
转载自:https://juejin.cn/post/7371424635894923304