文章目录
日志输出-第二章-接口级出入参的实现
上一章的内容为:日志输出指南
一、概述
上一章已经对日志输出的大概注意事项做了阐述,本章将介绍对应的一些统一出入参的内容
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 数据。
虽然在日志处理的时候用不了,但是你能做日志处理,实际上是可以对数据做加解密处理的,步骤都相差不大。