日志输出-第二章-接口级出入参的实现

在这里插入图片描述

日志输出-第二章-接口级出入参的实现

上一章的内容为:日志输出指南

一、概述

上一章已经对日志输出的大概注意事项做了阐述,本章将介绍对应的一些统一出入参的内容

ps:仅代表个人经验嗷

一般我在使用的时候,会接入到 SkyWalking 或者是 ELK,但是不接入使用也不是不行,只是需要上服务器去抓日志文件,没那么方便。

1.1、单机时代

这个时候的出入参记录一般是分为两种粒度

  • 接口级的出入参记录
  • 方法级的出入参记录

根据不同的情况来选择不同的记录方式,以方便排查问题。

接口级:

一般是必备的,知道了请求的入参,这个时候在生产排查问题的时候,就可以通过入参来溯源,简化了排查的难度。

方法级:

一般是不会有的,主要问题在于如果你有一个比较优雅的编程习惯的话,正常情况下逻辑应该是非常清晰的。也就是说正常情况下你拿到了 Controller 的请求参数,然后去看逻辑的话应该是很简单方便的,除非是老代码,逻辑判断非常复杂各个方法耦合非常严重,才需要采用这种方式。

1.2、微服务时代

在微服务时代,由于存在多服务之间互相调用的问题,一般会在流量入口处(比如说 GateWay)多加一个日志。

也就是说这种情况下分为三种粒度:

  • 网关级的出入参记录
  • 接口级的出入参记录
  • 方法级的出入参记录

二、实现接口级出入参记录

接口级的具体实现方式一般分为三种:

  • 通过 HandlerInterceptor 的方式进行拦截
  • 通过 Spring 的切面
  • 通过 Filter

这三种的相对来说 Filter 的性能是最好的,因为它是流量的出入口,下面将采用 Nacos+Filter 来实现。

2.1、Nacos 配置类


import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * @ClassName AutoLogConfig
 * @Author lizelin
 * @Description 日志 Nacos 动态刷新配置
 * @Date 2023/11/3 18:01
 * @Version 1.0
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "auto.log")
public class AutoLogConfig {

    /**
     * 是否开启日志开关
     */
    private Boolean enabled = false;
    /**
     * 配置需要输出的 headers
     */
    private String[] headers;
    /**
     * 是否打印所有请求头
     */
    private Boolean printAllHeaders = false;
    /**
     * 排除前缀 Url
     */
    private String[] excludePrefixUrls;
}

yaml 不会写的话,可以参照 yaml书写指南

2.2、日志实现


import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;

/**
 * @ClassName LogHandlerInterceptor
 * @Author lizelin
 * @Description 日志请求入参过滤器
 * @Date 2023/11/3 12:15
 * @Version 1.0
 */
@Slf4j
@Component
@AllArgsConstructor
public class LogRecordRequestFilter implements Filter, Ordered {

    private final AutoLogConfig autoLogConfig;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        Boolean enabled = autoLogConfig.getEnabled();
        if (Boolean.TRUE.equals(enabled)) {
			//排除前缀
            String[] excludePrefixUrls = autoLogConfig.getExcludePrefixUrls();
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String requestURI = httpServletRequest.getRequestURI();

            for (String item : excludePrefixUrls) {
                if (StrUtil.startWith(requestURI,item,true)) {
                    //不处理,直接向下传递
                    chain.doFilter(request, response);
                    return;
                }
            }

            StringBuilder builder = new StringBuilder();
            builder.append(" LogRecordRequestFilter ");
            String method = httpServletRequest.getMethod();
            builder.append(method).append(" ");
            //1.对 GET 处理
            handlerBefore(builder, httpServletRequest);
            //2.对 header 处理
            handlerHeader(builder, httpServletRequest);
            //输出 request 日志
            log.info(builder.toString());

            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            chain.doFilter(httpServletRequest, httpServletResponse);

            //输出 response 日志处理
            responseLog(httpServletResponse, builder);
        } else {
            //如果不处理,则直接向下传递
            chain.doFilter(request, response);
        }

    }

    /**
     * @Param responseWrapper
     * @Param builder
     * @Return void
     * @Description 输出 response 日志
     * @Author lizelin
     * @Date 2023/11/6 12:15
     **/
    private void responseLog(HttpServletResponse response, StringBuilder builder) throws IOException {
        int status = response.getStatus();
        builder.append(" HTTP status: ");
        builder.append(status);
        builder.append(" ContentType: ");
        builder.append(response.getContentType());
        //输出 response 日志
        log.info(builder.toString());
    }

    /**
     * @Param
     * @Return void
     * @Description 前置处理
     * @Author lizelin
     * @Date 2023/11/3 18:51
     **/
    private void handlerBefore(StringBuilder builder, HttpServletRequest requestWrapper) {
        //生成完整 URL
        String requestURI = requestWrapper.getRequestURI();
        String queryString = requestWrapper.getQueryString();
        builder.append("URL : ").append(requestURI);
        if (StrUtil.isNotEmpty(queryString)) {
            builder.append("?");
            builder.append(queryString);
        }
        builder.append("");
    }

    /**
     * @Param builder
     * @Param request
     * @Return void
     * @Description 对 header 进行处理
     * @Author lizelin
     * @Date 2023/11/3 18:57
     **/
    private void handlerHeader(StringBuilder builder, HttpServletRequest request) {
        String[] headers = autoLogConfig.getHeaders();
        Boolean printAllHeaders = autoLogConfig.getPrintAllHeaders();
        //打印所有
        if (Boolean.TRUE.equals(printAllHeaders)) {
            builder.append(" header : ");
            Enumeration<String> headerNames = request.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                String headerName = headerNames.nextElement();
                String header = request.getHeader(headerName);
                builder.append(headerName).append("=").append(header).append(", ");
            }
            builder.append(" ");
        }else if (ArrayUtil.isNotEmpty(headers)) {
            //打印指定
            builder.append(" header : ");
            for (String item : headers) {
                String header = request.getHeader(item);
                if (StrUtil.isNotEmpty(header)) {
                    builder.append(item).append("=").append(header).append(", ");
                }
            }
            builder.append(" ");
        }
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

三、注意事项以及问题分析

3.1、关于 Response 信息的输出

我上面给出的代码并没有对 Response 的 body 进行输出。

主要原因在于,异常情况毕竟是少数,百分之九十九的情况下,异常情况会抛出异常,极少数才是不抛出异常的错误返回,所以我没必要为了百分之一的可能去拉低百分之九十九的性能。(日志输出也是会消耗性能的,并且 body 由于数据比较大,会更加消耗性能)。

也就是说,我上述的方案实际上是通用场景下,在不影响性能的前提条件下,尽可能的输出更多的信息。

3.2、关于 Request 信息的输出

上述代码实际上只对 header 与 queryString 进行了输出,而对 body 的内容没有做处理,也就是实际上只能覆盖到 GET 请求。

这一块实际上也是和 3.1 一样的问题,在所有流量中,读的比重是会比写的比重大的。

并且相当于读请求来讲,写请求相对来说逻辑不会太过于复杂,出问题的概率也会少很多(因为如果是写请求直接报错的话,你连测试都过不了,更多的情况应该是:写入数据后导致了脏数据 -> 在读取的时候发现数据有问题)。

后续会再更新 网关级的出入参记录 以及如何输出上述的 response 和 request 中的 body 数据。

虽然在日志处理的时候用不了,但是你能做日志处理,实际上是可以对数据做加解密处理的,步骤都相差不大。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值