前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >不会吧,你还不会用RequestId看日志 ?

不会吧,你还不会用RequestId看日志 ?

作者头像
IT大咖说
发布于 2021-11-26 02:08:14
发布于 2021-11-26 02:08:14
1.6K00
代码可运行
举报
文章被收录于专栏:IT大咖说IT大咖说
运行总次数:0
代码可运行

◆ 引言

在日常的后端开发工作中,最常见的操作之一就是看日志排查问题,对于大项目一般使用类似ELK的技术栈统一搜集日志,小项目就直接把日志打印到日志文件。那不管对于大项目或者小项目,查看日志都需要通过某个关键字进行搜索,从而快速定位到异常日志的位置来进一步排查问题。

对于后端初学者来说,日志的关键字可能就是直接打印某个业务的说明加上业务标识,如果出现问题直接搜索对应的说明或者标识。例如下单场景,可能就直接打印:创建订单,订单编号:xxxx,当有问题的时候,则直接搜索订单编号或者创建订单。在这种方式下,经常会搜索出多条日志,增加问题的排查时长。

所以,今天我们就来说一说这个关键字的设计,这里我们使用RequestId进行精确定位问题日志的位置从而解决问题。

◆ 需求

目标:帮助开发快速定位日志位置

思路:当前端进行一次请求的时候,在进行业务逻辑处理之前我们需要生成一个唯一的RequestId,在业务逻辑处理过程中涉及到日志打印我们都需要带上这个RequestId,最后响应给前端的数据结构同样需要带上RequestId。这样,每次请求都会有一个RequestId,当某个接口异常则通过前端反馈的RequestId,后端即可快速定位异常的日志位置。

总结下我们的需求:

  • 一次请求生成一次RequestId,并且RequestId唯一
  • 一次请求响应给前端,都需要返回RequestId字段,接口正常、业务异常、系统异常,都需要返回该字段
  • 一次请求在控制台或者日志文件打印的日志,都需要显示RequestId
  • 一次请求的入参和出参都需要打印
  • 对于异步操作,需要在异步线程的日志同样显示RequestId

◆ 实现

1. 实现生成和存储RequestId的工具类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class RequestIdUtils {
    private static final ThreadLocal<UUID> requestIdHolder = new ThreadLocal<>();
    private RequestIdUtils() {
    }
    public static void generateRequestId() {
        requestIdHolder.set(UUID.randomUUID());
    }
    public static void generateRequestId(UUID uuid) {
        requestIdHolder.set(uuid);
    }
    public static UUID getRequestId() {
        return (UUID)requestIdHolder.get();
    }
    public static void removeRequestId() {
        requestIdHolder.remove();
    }
}

因为我们一次请求会生成一次RequestId,并且RequestId唯一,所以这里我们使用使用UUID来生成RequestId,并且用ThreadLocal进行存储。

2. 实现一个AOP,拦截所有的Controller的方法,这里是主要的处理逻辑

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Aspect
@Order
@Slf4j
public class ApiMessageAdvisor {


    @Around("execution(public * org.anyin.gitee.shiro.controller..*Controller.*(..))")
    public Object invokeAPI(ProceedingJoinPoint pjp) {
        String apiName = this.getApiName(pjp);
        // 生成RequestId
        String requestId = this.getRequestId();
        // 配置日志文件打印 REQUEST_ID
        MDC.put("REQUEST_ID", requestId);
        Object returnValue = null;
        try{
            // 打印请求参数
            this.printRequestParam(apiName, pjp);
            returnValue = pjp.proceed();
            // 处理RequestId
            this.handleRequestId(returnValue);
        }catch (BusinessException ex){
            // 业务异常
            returnValue = this.handleBusinessException(apiName, ex);
        }catch (Throwable ex){
            // 系统异常        
            returnValue = this.handleSystemException(apiName, ex);
        }finally {
            // 打印响应参数
            this.printResponse(apiName, returnValue);
            RequestIdUtils.removeRequestId();
            MDC.clear();
        }
        return returnValue;
    }

    /**
     * 处理系统异常
     * @param apiName 接口名称
     * @param ex 系统异常
     * @return 返回参数
     */
    private Response handleSystemException(String apiName, Throwable ex){
        log.error("@Meet unknown error when do " + apiName + ":" + ex.getMessage(), ex);
        Response response = new Response(BusinessCodeEnum.UNKNOWN_ERROR.getCode(), BusinessCodeEnum.UNKNOWN_ERROR.getMsg());
        response.setRequestId(RequestIdUtils.getRequestId().toString());
        return response;
    }

    /**
     * 处理业务异常
     * @param apiName 接口名称
     * @param ex 业务异常
     * @return 返回参数
     */
    private Response handleBusinessException(String apiName, BusinessException ex){
        log.error("@Meet error when do " + apiName + "[" + ex.getCode() + "]:" + ex.getMsg(), ex);
        Response response = new Response(ex.getCode(), ex.getMsg());
        response.setRequestId(RequestIdUtils.getRequestId().toString());
        return response;
    }

    /**
     * 填充RequestId
     * @param returnValue 返回参数
     */
    private void handleRequestId(Object returnValue){
        if(returnValue instanceof Response){
            Response response = (Response)returnValue;
            response.setRequestId(RequestIdUtils.getRequestId().toString());
        }
    }

    /**
     * 打印响应参数信息
     * @param apiName 接口名称
     * @param returnValue 返回值
     */
    private void printResponse(String apiName, Object returnValue){
        if (log.isInfoEnabled()) {
            log.info("@@{} done, response: {}", apiName, JSONUtil.toJsonStr(returnValue));
        }
    }

    /**
     * 打印请求参数信息
     * @param apiName 接口名称
     * @param pjp 切点
     */
    private void printRequestParam(String apiName, ProceedingJoinPoint pjp){
        Object[] args = pjp.getArgs();
        if(log.isInfoEnabled() && args != null&& args.length > 0){
            for(Object o : args) {
                if(!(o instanceof HttpServletRequest) && !(o instanceof HttpServletResponse) && !(o instanceof CommonsMultipartFile)) {
                    log.info("@@{} started, request: {}", apiName, JSONUtil.toJsonStr(o));
                }
            }
        }
    }

    /**
     * 获取RequestId
     * 优先从header头获取,如果没有则自己生成
     * @return RequestId
     */
    private String getRequestId(){
        // 因为如果有网关,则一般会从网关传递过来,所以优先从header头获取
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if(attributes != null && StringUtils.hasText(attributes.getRequest().getHeader("x-request-id"))) {
            HttpServletRequest request = attributes.getRequest();
            String requestId = request.getHeader("x-request-id");
            UUID uuid = UUID.fromString(requestId);
            RequestIdUtils.generateRequestId(uuid);
            return requestId;
        }
        UUID existUUID = RequestIdUtils.getRequestId();
        if(existUUID != null){
            return existUUID.toString();
        }
        RequestIdUtils.generateRequestId();
        return RequestIdUtils.getRequestId().toString();
    }

    /**
     * 获取当前接口对应的类名和方法名
     * @param pjp 切点
     * @return apiName
     */
    private String getApiName(ProceedingJoinPoint pjp){
        String apiClassName = pjp.getTarget().getClass().getSimpleName();
        String methodName = pjp.getSignature().getName();
        return apiClassName.concat(":").concat(methodName);
    }
}

简单说明:

  • 对于RequestId的获取方法 getRequestId,我们优先从header头获取,有网关的场景下一般会从网关传递过来;其次判断是否已经存在,如果存在则直接返回,这里是为了兼容有过滤器并且在过滤器生成了RequestId的场景;最后之前2中场景都未找到RequestId,则自己生成,并且返回
  • MDC.put("REQUEST_ID", requestId) 在我们生成RequestId之后,需要设置到日志系统中,这样子日志文件才能打印RequestId
  • printRequestParam 和 printResponse 是打印请求参数和响应参数,如果是高并发或者参数很多的场景下,最好不要打印
  • handleRequestId 、 handleBusinessException 、 handleSystemException 这三个方法分别是在接口正常、接口业务异常、接口系统异常的场景下设置RequestId

3. 日志文件配置

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<contextName>logback</contextName>
<springProperty scope="context" name="level" source="logging.level.root"/>
<springProperty scope="context" name="path" source="logging.file.path"/>


<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    <Target>System.out</Target>
   <filter class="ch.qos.logback.classic.filter.ThresholdFilter" >
        <level>DEBUG</level>
    </filter>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{REQUEST_ID}] [%thread] [%-5level] [%logger{0}:%L] : %msg%n</pattern>
    </encoder>
</appender>

<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${path}</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>${path}.%d{yyyy-MM-dd}.zip</fileNamePattern>
    </rollingPolicy>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{REQUEST_ID}] [%thread] [%-5level] [%logger{0}:%L] : %msg%n</pattern>
    </encoder>
</appender>

<root level="${level}">
    <appender-ref ref="console"/>
    <appender-ref ref="file"/>
</root>

这里是一个简单的日志格式配置文件,主要是关注[%X{REQUEST_ID}], 这里主要是把RequestId在日志文件中打印出来

4. 解决线程异步场景下RequestId的打印问题

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MdcExecutor implements Executor {
    private Executor executor;
    public MdcExecutor(Executor executor) {
        this.executor = executor;
    }
    @Override
    public void execute(Runnable command) {
        final String requestId = MDC.get("REQUEST_ID");
        executor.execute(() -> {
            MDC.put("REQUEST_ID", requestId);
            try {
                command.run();
            } finally {
                MDC.remove("REQUEST_ID");
            }
        });
    }
}

这里是一个简单的代理模式,代理了Executor,在真正执行的run方法之前设置RequestId到日志系统中,这样子异步线程的日志同样可以打印我们想要的RequestId

◆ 测试效果

  • 登录效果
  • 正常的业务处理
  • 发生业务异常
  • 发生系统异常
  • 异步线程

◆ 最后

通过以上骚操作,同学,你知道怎么使用RequestId看日志了吗?

来源:

https://www.toutiao.com/a7029930086522438182/?log_from=9170ea0cd7718_1637112388742

“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-11-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 IT大咖说 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
我们一起来学RabbitMQ 三:RabbiMQ 死信队列,延迟队列,持久化等知识点
fanout exchange 可以做成备份的交换机,因为 fanout 的消息是广播的方式
阿兵云原生
2023/02/16
2810
rabbitmq之rabbitmq工作模型与Java编程(一)
1、跨系统的异步通信 人民银行二代支付系统,使用重量级消息队列 IBM MQ,异步,解耦,削峰都有体现。 2、应用内的同步变成异步 秒杀:自己发送给自己 3、基于Pub/Sub模型实现的事件驱动 放款失败通知、提货通知、购买碎屏保 系统间同步数据 摒弃ELT(比如全量同步商户数据); 摒弃API(比如定时增量获取用户、获取产品,变成增量广播)。 4、利用RabbitMQ实现事务的最终一致性
周杰伦本人
2022/10/25
3970
rabbitmq之rabbitmq工作模型与Java编程(一)
RabbitMQ 基础概念与架构设计及工作机制学习总结
MQ全称为Message Queue,即消息队列. 它也是一个队列,遵循FIFO原则 。RabbitMQ则是一个开源的消息中间件,由erlang语言开发,基于AMQP协议实现的一个软件产品,提供应用程序之间的通信方法,在分布式系统开发中广泛应用。
授客
2024/11/21
4980
RabbitMQ 基础概念与架构设计及工作机制学习总结
消息队列——RabbitMQ的基本使用及高级特性
Rabbit是基于AMQP协议并使用Erlang开发的开源消息队列中间件,它支持多种语言的客户端,也是目前市面上使用比较广泛的一种消息队列,因此学习并掌握它是非常有必要的。本文主要基于Java客户端进行讲解,不涉及环境搭建部分。
夜勿语
2020/09/07
8080
Rabbitmq小书
1.生产者(Publisher): 发布消息到RabbitMQ中的交换机(Exchange)上
大忽悠爱学习
2022/10/04
3.4K0
Rabbitmq小书
Java开发面试--RabbitMQ专区3
RabbitMQ是一个消息中间件,本身并不支持分布式事务。但可以通过以下几种方式来实现分布式事务:
忆愿
2024/09/14
740
Java开发面试--RabbitMQ专区3
消息队列技术选型:这 7 种消息场景一定要考虑!
我们在做消息队列的技术选型时,往往会结合业务场景进行考虑。今天来聊一聊消息队列可能会用到的 7 种消息场景。
jinjunzhu
2023/09/27
6000
消息队列技术选型:这 7 种消息场景一定要考虑!
RabbitMQ
RabbitMQ属于中间件的一种,其实很多东西都是中间件比如说mysql redis都是的 其实中间件是一种概念,只要是实现软件和软件之间沟通连接的软件都可以叫做中间件
xiaozhangStu
2023/05/04
1K0
RabbitMQ 26问,基本涵盖了面试官必问的面试题
**Connection** **极大减少了操作系统建立** **TCP connection** **的开销**
小熊学Java
2022/09/04
5550
高性能消息队列中间件MQ_part2
之前我们使用原生JAVA操作RabbitMQ较为繁琐,接下来我们使用SpringBoot整合RabbitMQ,简化代码编写。
天天Lotay
2023/02/16
4370
高性能消息队列中间件MQ_part2
RabbitMq 总结
不依赖于路由键的匹配规则路由消息,根据发送的消息内容headers属性进行完全匹配(键值对形式)。性能差,基本不使用。
leon公众号精选
2022/04/27
4710
RabbitMq 总结
RabbitMQ---延迟队列,整合springboot
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
大忽悠爱学习
2021/12/07
6550
RabbitMQ---延迟队列,整合springboot
RabbitMQ 超详细入门篇
本人使用的是 阿里云服务器 没有的话也可以使用虚拟机… 事先使用连接工具上传了文件
Java_慈祥
2024/08/06
1.7K0
RabbitMQ 超详细入门篇
消息队列-RabbitMQ
交换机有四种类型:direct exchange、topic exchange、fanout exchange、headers exchange。
lpe234
2021/03/04
1.7K0
基于RabbitMQ实现延迟队列--PHP版
场景一:物联网系统经常会遇到向终端下发命令,如果命令一段时间没有应答,就需要设置成超时。
码农编程进阶笔记
2022/04/08
8450
基于RabbitMQ实现延迟队列--PHP版
RabbitMQ 高频考点
比如有一个订单系统,还要一个库存系统,用户下订单后要调用库存系统来处理,直接调用话,库存系统出现问题咋办呢?
sowhat1412
2022/09/20
6800
RabbitMQ 高频考点
Rabbitmq业务难点
消息生产者如果向交换机发送了一个无法被路由到任何队列上的消息,那么此时交换机会判断消息的mandatory属性值:
大忽悠爱学习
2023/02/26
8420
Rabbitmq业务难点
RabbitMQ
​ MQ(message queue),从字面意思上看,本质是个队列,FIFO 先入先出,只不过队列中存放的内容是 message 而已,还是一种跨进程的通信机制,用于上下游传递消息。在互联网架构中,MQ 是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了 MQ 之后,消息发送上游只需要依赖 MQ,不用依赖其他服务。
OY
2022/03/21
1.8K0
RabbitMQ
RabbitMQ 延迟队列
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望 在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的 元素的队列。
用户9615083
2022/12/25
6460
RabbitMQ 延迟队列
RabbitMQ高级面试题
在生产者投递消息时指定mandatory或者imrnediate参数设为 true 时,RabbitMQ 会把无法投递的消息通过Basic.Return 命令将消息返回给生产者,此时生产者需要调用channel.addReturnListener 来添加 ReturnListener 监昕器实现监听投递失败的消息
Java学习录
2019/07/01
3.9K0
相关推荐
我们一起来学RabbitMQ 三:RabbiMQ 死信队列,延迟队列,持久化等知识点
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文