文章目录
- 需求背景
- 效果图
- 实现思路
- 其他方案对比
- 优缺点分析
- 具体实现
需求背景
- 线上项目出现bug时,可以通过接口的请求参数来排查定位问题。
- 和业务方battle时,能够回怼他是自己操作的问题。
效果图
实现思路
- Spring提供了CommonsRequestLoggingFilter过滤器,该过滤器可以在接口请求前和请求后分别打印日志;
- 通过继承CommonsRequestLoggingFilter过滤器,实现部分自己的逻辑;
其他方案对比
- aop的方式:必须要到controller层,才会打印, 有些接口在 过滤器、拦截器层被拦截。
- 拦截器的方式:request.getInputStream()只能调用一次,解决的方法为copy一份流,个人感觉在代码逻辑层面不太清晰
优缺点分析
缺点
- 无法获取返回值,适合一些不关心返回值的场景。
- 对于Payload参数,内部使用的是ContentCachingRequestWrapper读取的request.getInputStream()流,所以必须要先调用@RequstBody,才能取到值(可以去看下ContentCachingRequestWrapper的具体实现)
优点
- 实现简单,代码层次逻辑清晰,与业务逻辑解耦。
具体实现
继承CommonsRequestLoggingFilter过滤器,实现部分自己的逻辑
package com.yp.basic.log.filter;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.yp.store.base.biz.common.constant.CommonConstant;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.web.filter.CommonsRequestLoggingFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* 自定义的请求日志打印过滤器,打印所有接口请求参数、耗时等日志 <br/>
*
* 对比其他两种方式:<br/>
* <ul>
* <li>1、aop的方式:必须要到controller层,才会打印, 有些接口在 过滤器、拦截器层被拦截</li>
* <li>2、拦截器的方式:request.getInputStream()只能调用一次,解决的方法为copy一份流,个人感觉在代码逻辑层面不太清晰</li>
* </ul>
*
* 缺点:<br/>
* <ul>
* <li>1、无法获取返回值,适合一些不关心返回值的场景。</li>
* <li>2. 内部使用的是ContentCachingRequestWrapper读取的request.getInputStream(),必须要先调用@RequstBody,才能取到值。</li>
* </ul>
*
* 优点:<br/>
* <ul>
* <li>1、 实现简单,代码层次逻辑清晰。</li>
* </ul>
*
* @author: wcong
* @date: 2022/11/25 15:17
*/
@Slf4j
public class RequestLogPrintFilter extends CommonsRequestLoggingFilter {
/**
* 接口日志 开始 请求前缀,建议放在公共的工具类中
*/
public final static String BEFORE_REQUEST_PREFIX = "### Before request[";
/**
* 接口 结束 请求前缀,建议放在公共的工具类中
*/
public final static String AFTER_REQUEST_PREFIX = "### After request[";
/**
* 重写父类方法:封装打印消息的格式
*/
@Override
protected String createMessage(HttpServletRequest request, String prefix, String suffix) {
final StringBuilder messageInfo = getMessageInfo(request, prefix, suffix);
// 请求开始还是结束
if (BEFORE_REQUEST_PREFIX.equals(prefix)) {
// 请求开始
MDC.put("logStartTime", String.valueOf(System.currentTimeMillis()));
} else {
// 请求结束,记录耗时
final Long logStartTime = Convert.toLong(MDC.get("logStartTime"), 0L);
messageInfo.append("\r\n接口耗时:").append(System.currentTimeMillis() - logStartTime).append("ms");
}
return messageInfo.toString();
}
/**
* 重写父类方法:请求前调用逻辑
*/
@Override
protected void beforeRequest(HttpServletRequest request, String message) {
doPrintLog(message);
}
/**
* 重写父类方法:请求后调用逻辑
*/
@Override
protected void afterRequest(HttpServletRequest request, String message) {
doPrintLog(message);
}
/**
* 重写父类方法:是否打印日志
*/
@Override
protected boolean shouldLog(HttpServletRequest request) {
// 父类中的逻辑是:logger.isDebugEnabled()
return true;
}
/**
* 统一封装打印的日志格式
*
* @param request javax.servlet.http.HttpServletRequest
* @param prefix 打印前缀
* @param suffix 打印后缀
* @return 封装好的日志格式
*/
private StringBuilder getMessageInfo(HttpServletRequest request, String prefix, String suffix) {
StringBuilder msg = new StringBuilder();
msg.append(prefix);
msg.append(StrUtil.format("method={}; ", request.getMethod().toLowerCase()));
msg.append("uri=").append(request.getRequestURI());
if (isIncludeClientInfo()) {
String client = request.getRemoteAddr();
if (StrUtil.isNotBlank(client)) {
msg.append("; client=").append(client);
}
HttpSession session = request.getSession(false);
if (session != null) {
msg.append("; session=").append(session.getId());
}
String user = request.getRemoteUser();
if (user != null) {
msg.append("; user=").append(user);
}
}
if (isIncludeQueryString()) {
String queryString = request.getQueryString();
if (queryString != null) {
msg.append("; ").append('?').append(queryString);
}
}
if (isIncludePayload()) {
String payload = getMessagePayload(request);
if (payload != null) {
msg.append("; payload=").append(payload);
}
}
msg.append(suffix);
return msg;
}
/**
* 具体打印的方法
*
* @param message 打印的消息
*/
private void doPrintLog(String message) {
// 生产环境打印debug级别
if (CommonConstant.IS_PRODUCT_ENVIRONMENT) {
log.debug(message);
} else {
log.info(message);
}
}
}
注册过滤器
package com.yp.basic.log;
import com.yp.basic.jackson.JsonUtil;
import com.yp.basic.log.aspect.SysLogAspect;
import com.yp.basic.log.event.SysLogListener;
import com.yp.basic.log.filter.RequestLogPrintFilter;
import com.yp.basic.log.monitor.PointUtil;
import com.yp.basic.log.properties.OptLogProperties;
import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.filter.CommonsRequestLoggingFilter;
/**
* 日志自动配置
*
* @author: wcong
* @date: 2022/11/25 15:17
*/
@EnableAsync
@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties(OptLogProperties.class)
public class LogAutoConfiguration {
@Bean
@ConditionalOnProperty(prefix = OptLogProperties.PREFIX, name = "enable-http-log", havingValue = "true", matchIfMissing = true)
public FilterRegistrationBean<CommonsRequestLoggingFilter> logFilterRegistration() {
CommonsRequestLoggingFilter filter = new RequestLogPrintFilter();
// 是否打印header中的内容,参数很多
filter.setIncludeHeaders(false);
// 是否打印查询字符串内容
filter.setIncludeQueryString(true);
// 是否打印 payLoad内容,内部使用的是ContentCachingRequestWrapper读取的request.getInputStream(),必须要先调用@RequstBody,才能取到值。
filter.setIncludePayload(true);
// 是否打印客户端信息(ip、session、remoteUser)
filter.setIncludeClientInfo(true);
// 1024字节(1kb),超出部分截取
// 在UTF-8编码方案中,一个英文字符占用一个字节,一个汉字字符占用三个字节的空间。
filter.setMaxPayloadLength(1024);
// 设置 before request 日志前缀,默认为:Before request [
filter.setBeforeMessagePrefix(RequestLogPrintFilter.BEFORE_REQUEST_PREFIX);
// 设置 before request 日志后缀,默认为:]
filter.setBeforeMessageSuffix("]");
// 设置 before request 日志前缀,默认为:After request [
filter.setAfterMessagePrefix(RequestLogPrintFilter.AFTER_REQUEST_PREFIX);
// 设置 after request 日志后缀,默认为:]
filter.setAfterMessageSuffix("]");
FilterRegistrationBean<CommonsRequestLoggingFilter> registration = new FilterRegistrationBean<>(filter);
registration.addUrlPatterns("/*");
registration.setOrder(0);
registration.setName("commonsRequestLoggingFilter");
return registration;
}
}